okstra 0.50.0 → 0.51.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 +8 -7
- package/README.md +8 -7
- package/bin/okstra +2 -0
- package/docs/kr/architecture.md +15 -16
- package/docs/kr/cli.md +5 -5
- package/docs/project-structure-overview.md +10 -6
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +15 -11
- package/runtime/agents/workers/claude-worker.md +3 -3
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/agents/workers/gemini-worker.md +2 -2
- package/runtime/bin/lib/okstra/cli.sh +8 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +14 -12
- package/runtime/bin/lib/okstra/usage.sh +6 -0
- package/runtime/bin/okstra-team-reconcile.sh +28 -0
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/launch.template.md +3 -1
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/implementation.md +1 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +259 -0
- package/runtime/python/okstra_ctl/context_cost.py +308 -0
- package/runtime/python/okstra_ctl/migrate.py +2 -12
- package/runtime/python/okstra_ctl/paths.py +22 -0
- package/runtime/python/okstra_ctl/render.py +284 -125
- package/runtime/python/okstra_ctl/render_final_report.py +31 -0
- package/runtime/python/okstra_ctl/run.py +507 -245
- package/runtime/python/okstra_ctl/sequence.py +2 -5
- package/runtime/python/okstra_ctl/team_reconcile.py +131 -0
- package/runtime/python/okstra_ctl/wizard.py +129 -133
- package/runtime/python/okstra_ctl/worktree.py +13 -5
- package/runtime/schemas/final-report-v1.0.schema.json +4 -0
- package/runtime/skills/okstra-coding-preflight/SKILL.md +69 -0
- package/runtime/skills/okstra-coding-preflight/architecture/hexagonal.md +116 -0
- package/runtime/skills/okstra-coding-preflight/clean-code.md +254 -0
- package/runtime/skills/okstra-coding-preflight/languages/java.md +64 -0
- package/runtime/skills/okstra-coding-preflight/languages/javascript-typescript.md +87 -0
- package/runtime/skills/okstra-coding-preflight/languages/kotlin.md +69 -0
- package/runtime/skills/okstra-coding-preflight/languages/nodejs.md +66 -0
- package/runtime/skills/okstra-coding-preflight/languages/python.md +179 -0
- package/runtime/skills/okstra-coding-preflight/languages/rust.md +105 -0
- package/runtime/skills/okstra-coding-preflight/languages/sql.md +68 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +12 -6
- package/runtime/skills/okstra-inspect/SKILL.md +100 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +5 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +7 -4
- package/runtime/templates/reports/final-report.template.md +1 -0
- package/runtime/templates/worker-prompt-preamble.md +3 -3
- package/src/_python-helper.mjs +3 -3
- package/src/context-cost.mjs +27 -0
- package/src/install.mjs +1 -0
- package/src/uninstall.mjs +1 -0
|
@@ -27,6 +27,7 @@ from datetime import datetime, timezone
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
29
|
from okstra_project import project_json_path, upsert_project_json
|
|
30
|
+
from .analysis_packet import build_analysis_packet
|
|
30
31
|
from .clarification_items import unresolved_approval_blockers
|
|
31
32
|
from .qa_commands import format_errors as _format_qa_errors, validate_qa_commands
|
|
32
33
|
from .material import (
|
|
@@ -36,12 +37,13 @@ from .material import (
|
|
|
36
37
|
resolve_related_tasks,
|
|
37
38
|
)
|
|
38
39
|
from .final_report_schema import load_schema
|
|
39
|
-
from .models import resolve_model_metadata
|
|
40
|
+
from .models import ModelAssignment, resolve_model_metadata
|
|
40
41
|
from .schema_excerpt import build_schema_excerpt
|
|
41
42
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
42
43
|
from .render import (
|
|
43
44
|
apply_lead_prompt_defaults,
|
|
44
45
|
inject_lead_prompt_computed_tokens,
|
|
46
|
+
render_active_run_context,
|
|
45
47
|
render_latest_task_discovery,
|
|
46
48
|
render_reference_expectations,
|
|
47
49
|
render_run_manifest,
|
|
@@ -86,6 +88,17 @@ APPROVED_FRONTMATTER_PATTERN = re.compile(
|
|
|
86
88
|
re.IGNORECASE | re.MULTILINE,
|
|
87
89
|
)
|
|
88
90
|
|
|
91
|
+
# Frontmatter implementation-option matcher.
|
|
92
|
+
#
|
|
93
|
+
# `approved:` 바로 아래 줄의 `implementation-option:` 한 줄을 식별한다.
|
|
94
|
+
# report-writer 가 빈 값으로 항상 emit 하므로 라인은 존재하되 값은 비어 있을
|
|
95
|
+
# 수 있다. `--implementation-option <name>` CLI 가 이 줄의 값만 치환한다.
|
|
96
|
+
# 값이 비면 implementation 은 plan 의 `Recommended Option` 으로 폴백한다.
|
|
97
|
+
IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN = re.compile(
|
|
98
|
+
r"^implementation-option:[ \t]*(.*)$",
|
|
99
|
+
re.MULTILINE,
|
|
100
|
+
)
|
|
101
|
+
|
|
89
102
|
_FRONTMATTER_BLOCK_PATTERN = re.compile(r"\A---\n(.*?)\n---\n", re.DOTALL)
|
|
90
103
|
|
|
91
104
|
|
|
@@ -132,6 +145,11 @@ class PrepareInputs:
|
|
|
132
145
|
pr_template_path: str = ""
|
|
133
146
|
render_only: bool = False
|
|
134
147
|
approve_plan_ack: bool = False
|
|
148
|
+
# implementation 전용: 유저가 implementation-planning final-report 에서 고른
|
|
149
|
+
# Option Candidate 이름. `--implementation-option <name>` 으로 전달되어
|
|
150
|
+
# approved-plan frontmatter 의 `implementation-option:` 라인을 채운다. 빈
|
|
151
|
+
# 문자열이면 implementation 은 plan 의 `Recommended Option` 으로 폴백한다.
|
|
152
|
+
implementation_option: str = ""
|
|
135
153
|
# Phase 6 plan-body verification opt-out. Default True (round runs after
|
|
136
154
|
# report-writer draft). Flipped to False by CLI `--no-plan-verification`.
|
|
137
155
|
# Only meaningful for `--task-type implementation-planning`; the manifest
|
|
@@ -345,6 +363,62 @@ def _apply_cli_approval(path: str) -> str:
|
|
|
345
363
|
return "frontmatter-flipped"
|
|
346
364
|
|
|
347
365
|
|
|
366
|
+
def _apply_cli_implementation_option(path: str, option_name: str) -> str:
|
|
367
|
+
"""`--implementation-option <name>` 이 지정된 경우 approved-plan frontmatter 의
|
|
368
|
+
`implementation-option:` 라인 값을 `option_name` 으로 치환한다.
|
|
369
|
+
|
|
370
|
+
동작 요약:
|
|
371
|
+
- frontmatter 의 `implementation-option:` 라인을
|
|
372
|
+
`implementation-option: <option_name>` 으로 교체 → `"frontmatter-set"`.
|
|
373
|
+
- audit 라인을 한 번 append (idempotent: 동일 audit 라인은 한 번만).
|
|
374
|
+
- frontmatter 가 없거나 `implementation-option:` 라인이 없으면 PrepareError
|
|
375
|
+
(report-writer 가 빈 값으로 이 라인을 항상 emit 하므로 라인은 존재해야 한다).
|
|
376
|
+
|
|
377
|
+
`--approve` (`_apply_cli_approval`) 의 frontmatter-치환 + audit-append 패턴을
|
|
378
|
+
그대로 미러한다.
|
|
379
|
+
"""
|
|
380
|
+
p = Path(path)
|
|
381
|
+
if not p.is_file():
|
|
382
|
+
raise PrepareError(f"approved plan file not found: {path}")
|
|
383
|
+
body = p.read_text(encoding="utf-8", errors="replace")
|
|
384
|
+
frontmatter = _extract_frontmatter_block(body)
|
|
385
|
+
if frontmatter is None:
|
|
386
|
+
raise PrepareError(
|
|
387
|
+
f"--implementation-option was given but the approved-plan file has no YAML frontmatter: {path}\n"
|
|
388
|
+
" expected the report to begin with `---\\n...\\n---\\n`."
|
|
389
|
+
)
|
|
390
|
+
if not IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN.search(frontmatter):
|
|
391
|
+
raise PrepareError(
|
|
392
|
+
f"--implementation-option was given but the approved-plan frontmatter has no "
|
|
393
|
+
f"`implementation-option:` field: {path}\n"
|
|
394
|
+
" expected a single line of the form `implementation-option:` "
|
|
395
|
+
"(report-writer worker emits this empty by default)."
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
audit_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
399
|
+
audit_line = (
|
|
400
|
+
f"- 선택 옵션 (CLI): {audit_iso} — `{option_name}` recorded by "
|
|
401
|
+
"`okstra --implementation-option` (user-selected Option Candidate)"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# 렌더 경로(`final-report.template.md` 의 `yaml_scalar`)와 동일하게 값을
|
|
405
|
+
# YAML 따옴표 처리한다. 옵션명은 보통 `: `(콜론+공백)를 포함하므로 따옴표
|
|
406
|
+
# 없이 쓰면 frontmatter 가 invalid YAML 이 되고 CLI/렌더 출력이 어긋난다.
|
|
407
|
+
# 함수 치환을 써서 quoted 값 안의 백슬래시/그룹 참조가 re 치환 문법으로
|
|
408
|
+
# 재해석되는 것까지 막는다 (리터럴 치환).
|
|
409
|
+
from .render_final_report import _yaml_scalar
|
|
410
|
+
|
|
411
|
+
quoted = _yaml_scalar(option_name)
|
|
412
|
+
set_frontmatter = IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN.sub(
|
|
413
|
+
lambda _m: f"implementation-option: {quoted}", frontmatter, count=1,
|
|
414
|
+
)
|
|
415
|
+
new_body = body.replace(frontmatter, set_frontmatter, 1)
|
|
416
|
+
if audit_line.split(" — ")[1] not in new_body:
|
|
417
|
+
new_body = new_body.rstrip("\n") + "\n" + audit_line + "\n"
|
|
418
|
+
p.write_text(new_body, encoding="utf-8")
|
|
419
|
+
return "frontmatter-set"
|
|
420
|
+
|
|
421
|
+
|
|
348
422
|
def _ensure_task_directories(ctx: dict) -> None:
|
|
349
423
|
for key in (
|
|
350
424
|
"TASK_ROOT", "INSTRUCTION_SET_PATH", "RUNS_DIR", "HISTORY_DIR",
|
|
@@ -492,6 +566,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
492
566
|
("--task-brief", str(inp.brief_path)),
|
|
493
567
|
("--directive", inp.directive),
|
|
494
568
|
("--approved-plan", inp.approved_plan_path),
|
|
569
|
+
("--implementation-option", inp.implementation_option),
|
|
495
570
|
("--clarification-response", inp.clarification_response_path),
|
|
496
571
|
("--workers", workers),
|
|
497
572
|
("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL", "")),
|
|
@@ -546,23 +621,42 @@ def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
|
|
|
546
621
|
return _INCLUDE_DIRECTIVE.sub(_sub, text)
|
|
547
622
|
|
|
548
623
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
624
|
+
# ---------------------------------------------------------------------------
|
|
625
|
+
# prepare_task_bundle 의 단계(phase) 헬퍼.
|
|
626
|
+
#
|
|
627
|
+
# 왜 분리하는가: prepare_task_bundle 은 input 검증 → 에셋 해소 → 프로젝트 등록
|
|
628
|
+
# → roster/model/executor 해소 → run-context/worktree → stage 예약 →
|
|
629
|
+
# instruction-set/manifest 렌더 → central record 까지 8개 독립 책임을 순차로
|
|
630
|
+
# 수행한다. 각 책임을 명시적 입력/반환을 갖는 헬퍼로 빼내면 (a) 개별 단위
|
|
631
|
+
# 테스트가 가능해지고 (b) prepare_task_bundle 본문이 "무엇을 어떤 순서로
|
|
632
|
+
# 엮는가" 만 드러내는 thin orchestrator 로 남는다. ctx dict 조립은 의도적으로
|
|
633
|
+
# orchestrator 에 남겨 데이터 흐름을 한눈에 보이게 한다.
|
|
634
|
+
# ---------------------------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
_INSTALL_HINT = (
|
|
637
|
+
" This file ships with the okstra package; its absence usually means a stale "
|
|
638
|
+
"or partial install. Run 'okstra ensure-installed' (or 'okstra install' again) "
|
|
639
|
+
"to repair the runtime. If the problem persists, run 'okstra doctor' for a "
|
|
640
|
+
"fuller diagnostic."
|
|
641
|
+
)
|
|
553
642
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
643
|
+
|
|
644
|
+
@dataclass
|
|
645
|
+
class _ResolvedAssets:
|
|
646
|
+
"""런타임 에셋 경로 묶음. 모든 파일은 패키지와 함께 배포되며, 누락은
|
|
647
|
+
user-content 문제가 아니라 설치 불완전을 뜻한다 (_INSTALL_HINT)."""
|
|
648
|
+
|
|
649
|
+
profile_file: Path
|
|
650
|
+
prompt_template: Path
|
|
651
|
+
task_index_template: Path
|
|
652
|
+
final_report_template: Path
|
|
653
|
+
source_skill: Path
|
|
654
|
+
run_validator: Path
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _ResolvedAssets:
|
|
658
|
+
"""task-type 별 profile + 공통 템플릿/스킬/validator 경로를 해소하고 존재를 확인한다."""
|
|
659
|
+
profile_file = workspace_root / "prompts" / "profiles" / f"{inp.task_type}.md"
|
|
566
660
|
if not profile_file.is_file():
|
|
567
661
|
raise PrepareError(
|
|
568
662
|
f"analysis profile file not found for task-type {inp.task_type}: "
|
|
@@ -586,10 +680,24 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
586
680
|
raise PrepareError(
|
|
587
681
|
f"required okstra template or source skill missing: {required}.{_INSTALL_HINT}"
|
|
588
682
|
)
|
|
683
|
+
return _ResolvedAssets(
|
|
684
|
+
profile_file=profile_file,
|
|
685
|
+
prompt_template=prompt_template,
|
|
686
|
+
task_index_template=task_index_template,
|
|
687
|
+
final_report_template=final_report_template,
|
|
688
|
+
source_skill=source_skill,
|
|
689
|
+
run_validator=run_validator,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
694
|
+
"""project_root/brief 존재와 task-type 별 입력 의미(plan 승인·stage·clarification)
|
|
695
|
+
를 검증하고, implementation 일 때 stage map 을 파싱해 돌려준다 (그 외엔 빈 리스트)."""
|
|
589
696
|
if not project_root.is_dir():
|
|
590
697
|
raise PrepareError(f"project root not found: {project_root}")
|
|
591
698
|
if not inp.brief_path.is_file():
|
|
592
699
|
raise PrepareError(f"task brief not found: {inp.brief_path}")
|
|
700
|
+
ctx_stage_map: list = []
|
|
593
701
|
if inp.task_type == "implementation":
|
|
594
702
|
if not inp.approved_plan_path:
|
|
595
703
|
raise PrepareError(
|
|
@@ -600,6 +708,13 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
600
708
|
# 파일의 frontmatter approved 를 true 로 toggle 한 뒤 동일한 검증
|
|
601
709
|
# 경로(`_validate_approved_plan`) 를 그대로 통과시킨다.
|
|
602
710
|
_apply_cli_approval(inp.approved_plan_path)
|
|
711
|
+
if inp.implementation_option:
|
|
712
|
+
# 유저가 고른 Option Candidate 이름을 approved-plan frontmatter 의
|
|
713
|
+
# `implementation-option:` 라인에 기록한다. 빈 값이면 implementation 이
|
|
714
|
+
# plan 의 `Recommended Option` 으로 폴백하므로 호출하지 않는다.
|
|
715
|
+
_apply_cli_implementation_option(
|
|
716
|
+
inp.approved_plan_path, inp.implementation_option
|
|
717
|
+
)
|
|
603
718
|
_validate_approved_plan(inp.approved_plan_path)
|
|
604
719
|
_validate_stage_structure(inp.approved_plan_path)
|
|
605
720
|
ctx_stage_map = _parse_stage_map_into_ctx(inp.approved_plan_path)
|
|
@@ -611,6 +726,13 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
611
726
|
"--approve is only meaningful with --task-type implementation "
|
|
612
727
|
"and --approved-plan <path>"
|
|
613
728
|
)
|
|
729
|
+
if inp.implementation_option:
|
|
730
|
+
# implementation 외 task-type 에서 `--implementation-option` 도 의미가
|
|
731
|
+
# 없다 (`--approve` 와 동일). 조용히 무시하지 않고 즉시 거부한다.
|
|
732
|
+
raise PrepareError(
|
|
733
|
+
"--implementation-option is only meaningful with --task-type "
|
|
734
|
+
"implementation and --approved-plan <path>"
|
|
735
|
+
)
|
|
614
736
|
if inp.stage != "auto":
|
|
615
737
|
raise PrepareError(
|
|
616
738
|
f"--stage is only meaningful with --task-type implementation; "
|
|
@@ -620,11 +742,11 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
620
742
|
raise PrepareError(
|
|
621
743
|
f"clarification response file not found: {inp.clarification_response_path}"
|
|
622
744
|
)
|
|
745
|
+
return ctx_stage_map
|
|
623
746
|
|
|
624
|
-
# ---- installation check ----
|
|
625
|
-
verify_installation(workspace_root)
|
|
626
747
|
|
|
627
|
-
|
|
748
|
+
def _register_and_check_project(project_root: Path, inp: PrepareInputs) -> None:
|
|
749
|
+
"""project.json self-registration + (implementation 한정) qaCommands gate 검증."""
|
|
628
750
|
from okstra_project import ResolverError
|
|
629
751
|
|
|
630
752
|
try:
|
|
@@ -652,95 +774,375 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
652
774
|
if qa_errors:
|
|
653
775
|
raise PrepareError(_format_qa_errors(qa_errors))
|
|
654
776
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
else:
|
|
663
|
-
profile_workers = resolve_profile_workers(profile_file)
|
|
664
|
-
profile_workers_csv = ",".join(profile_workers)
|
|
665
|
-
workers = normalize_workers(inp.workers_override or profile_workers_csv)
|
|
666
|
-
if inp.workers_override.strip():
|
|
667
|
-
validate_workers_against_profile(workers, profile_workers)
|
|
668
|
-
if not workers:
|
|
669
|
-
raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
|
|
670
|
-
selected_reviewers = ",".join(workers)
|
|
671
|
-
|
|
672
|
-
# ---- PR template resolution (release-handoff only) ----
|
|
673
|
-
pr_template_path_str = ""
|
|
674
|
-
pr_template_source = ""
|
|
777
|
+
|
|
778
|
+
def _resolve_roster(inp: PrepareInputs, profile_file: Path) -> tuple[list[str], str]:
|
|
779
|
+
"""profile 의 `Required workers:` 블록을 권위로 roster 를 해소한다.
|
|
780
|
+
|
|
781
|
+
release-handoff 은 의도적으로 single-lead (worker dispatch / TeamCreate /
|
|
782
|
+
convergence 없음). profile 에 `- Required workers:` 블록이 없으므로 어떤
|
|
783
|
+
override 가 와도 빈 roster 를 강제해 profile 계약과 일관성을 유지한다."""
|
|
675
784
|
if inp.task_type == "release-handoff":
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
785
|
+
return [], ""
|
|
786
|
+
profile_workers = resolve_profile_workers(profile_file)
|
|
787
|
+
profile_workers_csv = ",".join(profile_workers)
|
|
788
|
+
workers = normalize_workers(inp.workers_override or profile_workers_csv)
|
|
789
|
+
if inp.workers_override.strip():
|
|
790
|
+
validate_workers_against_profile(workers, profile_workers)
|
|
791
|
+
if not workers:
|
|
792
|
+
raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
|
|
793
|
+
return workers, ",".join(workers)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _resolve_pr_template(inp: PrepareInputs) -> tuple[str, str]:
|
|
797
|
+
"""release-handoff 전용 PR 본문 템플릿 경로 + 출처를 해소한다 (그 외엔 빈 값)."""
|
|
798
|
+
if inp.task_type != "release-handoff":
|
|
799
|
+
return "", ""
|
|
800
|
+
try:
|
|
801
|
+
resolved_tpl = resolve_pr_template_path(Path(inp.project_root), inp.pr_template_path)
|
|
802
|
+
except PrTemplateError as exc:
|
|
803
|
+
raise PrepareError(f"PR template resolution failed: {exc}") from exc
|
|
804
|
+
return str(resolved_tpl.path), resolved_tpl.source
|
|
684
805
|
|
|
685
|
-
|
|
806
|
+
|
|
807
|
+
@dataclass
|
|
808
|
+
class _ModelBindings:
|
|
809
|
+
"""이 run 의 lead/worker 모델 + critic + executor 바인딩."""
|
|
810
|
+
|
|
811
|
+
lead: ModelAssignment
|
|
812
|
+
cw: ModelAssignment
|
|
813
|
+
co: ModelAssignment
|
|
814
|
+
ge: ModelAssignment
|
|
815
|
+
rw: ModelAssignment
|
|
816
|
+
critic_choice: str
|
|
817
|
+
executor_provider: str
|
|
818
|
+
executor_display_name: str
|
|
819
|
+
executor_worker_agent: str
|
|
820
|
+
executor_model_meta: ModelAssignment
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _resolve_worker_models(inp: PrepareInputs) -> dict:
|
|
824
|
+
"""lead/claude/codex/gemini/report-writer 모델을 alias → (display, execution) 로 해소."""
|
|
686
825
|
lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
|
|
687
826
|
claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "opus")
|
|
688
827
|
codex_default = _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.5")
|
|
689
828
|
gemini_default = _default("OKSTRA_DEFAULT_GEMINI_MODEL", "auto")
|
|
690
829
|
report_writer_default = _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
830
|
+
return {
|
|
831
|
+
"lead": resolve_model_metadata(
|
|
832
|
+
provider="claude", raw_value=inp.lead_model,
|
|
833
|
+
default_display=lead_default, default_execution=lead_default,
|
|
834
|
+
),
|
|
835
|
+
"cw": resolve_model_metadata(
|
|
836
|
+
provider="claude", raw_value=inp.claude_model,
|
|
837
|
+
default_display=claude_default, default_execution=claude_default,
|
|
838
|
+
),
|
|
839
|
+
"co": resolve_model_metadata(
|
|
840
|
+
provider="codex", raw_value=inp.codex_model,
|
|
841
|
+
default_display=codex_default, default_execution=codex_default,
|
|
842
|
+
),
|
|
843
|
+
"ge": resolve_model_metadata(
|
|
844
|
+
provider="gemini", raw_value=inp.gemini_model,
|
|
845
|
+
default_display=gemini_default, default_execution=gemini_default,
|
|
846
|
+
),
|
|
847
|
+
"rw": resolve_model_metadata(
|
|
848
|
+
provider="claude", raw_value=inp.report_writer_model,
|
|
849
|
+
default_display=report_writer_default, default_execution=report_writer_default,
|
|
850
|
+
),
|
|
851
|
+
}
|
|
852
|
+
|
|
711
853
|
|
|
712
|
-
|
|
854
|
+
def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBindings:
|
|
855
|
+
"""worker 모델 + critic 선택 + executor 바인딩을 한 묶음으로 해소·검증한다."""
|
|
856
|
+
m = _resolve_worker_models(inp)
|
|
713
857
|
critic_choice = (inp.critic or "").strip().lower()
|
|
714
858
|
if critic_choice not in ("", "off", "claude", "codex", "gemini"):
|
|
715
859
|
raise PrepareError(
|
|
716
860
|
f"--critic must be one of: off, claude, codex, gemini (got: {critic_choice!r})"
|
|
717
861
|
)
|
|
718
|
-
|
|
719
|
-
# ---- executor binding (implementation phase only; recorded universally for manifest consistency) ----
|
|
720
862
|
executor_default = _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
|
|
721
863
|
executor_provider = (inp.executor or executor_default).strip().lower()
|
|
722
864
|
if executor_provider not in ("claude", "codex", "gemini"):
|
|
723
865
|
raise PrepareError(
|
|
724
866
|
f"--executor must be one of: claude, codex, gemini (got: {executor_provider!r})"
|
|
725
867
|
)
|
|
726
|
-
if
|
|
727
|
-
inp.task_type == "implementation"
|
|
728
|
-
and executor_provider not in workers
|
|
729
|
-
):
|
|
868
|
+
if inp.task_type == "implementation" and executor_provider not in workers:
|
|
730
869
|
raise PrepareError(
|
|
731
870
|
f"--executor {executor_provider} requires {executor_provider!r} in "
|
|
732
871
|
f"--workers, but resolved roster is {workers!r}. "
|
|
733
872
|
f"Add it explicitly, e.g. --workers {','.join(sorted(set(workers + [executor_provider])))}."
|
|
734
873
|
)
|
|
735
|
-
|
|
736
|
-
"claude": ("Claude executor", "claude-worker", cw),
|
|
737
|
-
"codex": ("Codex executor", "codex-worker", co),
|
|
738
|
-
"gemini": ("Gemini executor", "gemini-worker", ge),
|
|
874
|
+
provider_to_meta = {
|
|
875
|
+
"claude": ("Claude executor", "claude-worker", m["cw"]),
|
|
876
|
+
"codex": ("Codex executor", "codex-worker", m["co"]),
|
|
877
|
+
"gemini": ("Gemini executor", "gemini-worker", m["ge"]),
|
|
739
878
|
}
|
|
740
|
-
|
|
741
|
-
|
|
879
|
+
display_name, worker_agent, model_meta = provider_to_meta[executor_provider]
|
|
880
|
+
return _ModelBindings(
|
|
881
|
+
lead=m["lead"], cw=m["cw"], co=m["co"], ge=m["ge"], rw=m["rw"],
|
|
882
|
+
critic_choice=critic_choice,
|
|
883
|
+
executor_provider=executor_provider,
|
|
884
|
+
executor_display_name=display_name,
|
|
885
|
+
executor_worker_agent=worker_agent,
|
|
886
|
+
executor_model_meta=model_meta,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _reserve_implementation_stages(inp: PrepareInputs, ctx: dict, ctx_stage_map: list) -> None:
|
|
891
|
+
"""implementation run 의 ready-set 배치를 선택하고 consumers.jsonl 에 stage 별
|
|
892
|
+
`started` 행을 예약한다. ctx 에 effective_stages / STAGE_BATCH_DIRECTIVE 를 채우고
|
|
893
|
+
inp.stage 를 확정된 배치 CSV 로 덮어쓴다."""
|
|
894
|
+
from .consumers import read_consumers, append_consumer
|
|
895
|
+
import datetime as _dt
|
|
896
|
+
|
|
897
|
+
ctx["parsed_stage_map"] = ctx_stage_map
|
|
898
|
+
plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
|
|
899
|
+
consumed = read_consumers(plan_run_root)
|
|
900
|
+
done_stages = {r["stage"] for r in consumed if r.get("status") == "done"}
|
|
901
|
+
effective = _resolve_effective_stages(ctx["parsed_stage_map"], done_stages, inp.stage)
|
|
902
|
+
ctx["effective_stages"] = effective
|
|
903
|
+
csv = ",".join(str(n) for n in effective)
|
|
904
|
+
ctx["EFFECTIVE_STAGES"] = csv
|
|
905
|
+
ctx["STAGE_BATCH_DIRECTIVE"] = (
|
|
906
|
+
f"- **Stage batch for this implementation run:** `{csv}` "
|
|
907
|
+
"(comma-separated stage numbers, ascending). Execute exactly these "
|
|
908
|
+
"Stage Map stages in this order — this is the authoritative scope. "
|
|
909
|
+
"Do NOT recompute the start stage from `consumers.jsonl`; the runtime "
|
|
910
|
+
"already selected and reserved this batch."
|
|
911
|
+
)
|
|
912
|
+
inp.stage = csv
|
|
913
|
+
print(f"selected stages: {csv}", file=sys.stdout)
|
|
914
|
+
head_proc = _subprocess.run(
|
|
915
|
+
["git", "rev-parse", "HEAD"],
|
|
916
|
+
cwd=inp.project_root, capture_output=True, text=True,
|
|
917
|
+
)
|
|
918
|
+
head_sha = head_proc.stdout.strip() if head_proc.returncode == 0 else ""
|
|
919
|
+
now = _dt.datetime.now(_dt.timezone.utc).isoformat()
|
|
920
|
+
for stage_n in effective:
|
|
921
|
+
append_consumer(
|
|
922
|
+
plan_run_root,
|
|
923
|
+
impl_task_key=ctx["TASK_KEY"],
|
|
924
|
+
stage=stage_n,
|
|
925
|
+
status="started",
|
|
926
|
+
started_at=now,
|
|
927
|
+
head_commit=head_sha,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _write_instruction_set_sources(
|
|
932
|
+
inp: PrepareInputs, ctx: dict, profile_content: str, review_material: str
|
|
933
|
+
) -> Path:
|
|
934
|
+
"""instruction-set 디렉터리에 profile/material/brief/clarification/directive 와
|
|
935
|
+
reference-expectations 를 기록하고 디렉터리 경로를 돌려준다."""
|
|
936
|
+
instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
|
|
937
|
+
instruction_set.mkdir(parents=True, exist_ok=True)
|
|
938
|
+
profile_rendered = profile_content
|
|
939
|
+
for key in (
|
|
940
|
+
"EXECUTOR_PROVIDER",
|
|
941
|
+
"EXECUTOR_DISPLAY_NAME",
|
|
942
|
+
"EXECUTOR_WORKER_AGENT",
|
|
943
|
+
"EXECUTOR_MODEL_DISPLAY",
|
|
944
|
+
"EXECUTOR_MODEL_EXECUTION_VALUE",
|
|
945
|
+
"EXECUTOR_WORKTREE_PATH",
|
|
946
|
+
"EXECUTOR_WORKTREE_BRANCH",
|
|
947
|
+
"EXECUTOR_WORKTREE_BASE_REF",
|
|
948
|
+
"EXECUTOR_WORKTREE_STATUS",
|
|
949
|
+
"EXECUTOR_WORKTREE_NOTE",
|
|
950
|
+
):
|
|
951
|
+
profile_rendered = profile_rendered.replace("{{" + key + "}}", ctx.get(key, ""))
|
|
952
|
+
(instruction_set / "analysis-profile.md").write_text(profile_rendered, encoding="utf-8")
|
|
953
|
+
(instruction_set / "analysis-material.md").write_text(review_material, encoding="utf-8")
|
|
954
|
+
shutil.copyfile(inp.brief_path, instruction_set / "task-brief.md")
|
|
955
|
+
if inp.clarification_response_path:
|
|
956
|
+
shutil.copyfile(
|
|
957
|
+
inp.clarification_response_path,
|
|
958
|
+
instruction_set / "clarification-response.md",
|
|
959
|
+
)
|
|
960
|
+
if inp.directive:
|
|
961
|
+
(instruction_set / "directive.txt").write_text(inp.directive + "\n", encoding="utf-8")
|
|
962
|
+
render_reference_expectations(
|
|
963
|
+
str(inp.brief_path), str(instruction_set / "reference-expectations.md"), ctx,
|
|
964
|
+
)
|
|
965
|
+
packet = build_analysis_packet(
|
|
966
|
+
task_key=ctx["TASK_KEY"],
|
|
967
|
+
task_type=ctx["TASK_TYPE"],
|
|
968
|
+
task_brief_path=instruction_set / "task-brief.md",
|
|
969
|
+
analysis_profile_path=instruction_set / "analysis-profile.md",
|
|
970
|
+
reference_expectations_path=instruction_set / "reference-expectations.md",
|
|
971
|
+
clarification_response_path=(
|
|
972
|
+
instruction_set / "clarification-response.md"
|
|
973
|
+
if inp.clarification_response_path else None
|
|
974
|
+
),
|
|
975
|
+
directive=inp.directive,
|
|
976
|
+
instruction_set_relative_path=ctx["INSTRUCTION_SET_RELATIVE_PATH"],
|
|
977
|
+
)
|
|
978
|
+
(instruction_set / "analysis-packet.md").write_text(packet, encoding="utf-8")
|
|
979
|
+
return instruction_set
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _render_lead_prompt_and_snapshot(
|
|
983
|
+
inp: PrepareInputs,
|
|
984
|
+
ctx: dict,
|
|
985
|
+
instruction_set: Path,
|
|
986
|
+
final_report_template: Path,
|
|
987
|
+
prompt_template: Path,
|
|
988
|
+
) -> str:
|
|
989
|
+
"""compute/default 토큰을 ctx 에 주입한 뒤 final-report 템플릿 사본과 lead 실행
|
|
990
|
+
프롬프트를 렌더하고, 프롬프트 스냅샷을 기록한 후 prompt_text 를 돌려준다."""
|
|
991
|
+
# inject populates ctx with compute + default tokens consumed by the lead
|
|
992
|
+
# prompt render below (claude-execution-prompt.md). The final-report
|
|
993
|
+
# template render is effectively a copy (Jinja2 `{{ var }}` syntax does
|
|
994
|
+
# not match `_TOKEN_RE`); routed through render_template_with_ctx for SOT
|
|
995
|
+
# consistency.
|
|
996
|
+
inject_lead_prompt_computed_tokens(ctx)
|
|
997
|
+
apply_lead_prompt_defaults(ctx)
|
|
998
|
+
render_template_with_ctx(
|
|
999
|
+
str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
|
|
1000
|
+
)
|
|
1001
|
+
# Per-task-type schema excerpt for the report-writer worker. The full
|
|
1002
|
+
# schema validates the data.json post-hoc (load_schema); the worker only
|
|
1003
|
+
# needs the common structure + this run's task-type block, so we write a
|
|
1004
|
+
# scoped excerpt into the instruction-set rather than make the worker read
|
|
1005
|
+
# the whole 44 KB / all-task-types schema (whose repo `schemas/...` path is
|
|
1006
|
+
# not resolvable from a consumer project's task bundle anyway).
|
|
1007
|
+
#
|
|
1008
|
+
# Guarded: a missing/unreadable schema must NOT break bundle preparation.
|
|
1009
|
+
# If the excerpt cannot be produced (e.g. an older install that predates
|
|
1010
|
+
# the schemas/ copy step), prep proceeds without it — the report-writer
|
|
1011
|
+
# still has the phase-stripped template + skill structure guide, and
|
|
1012
|
+
# validation runs against the full schema regardless.
|
|
1013
|
+
try:
|
|
1014
|
+
_excerpt = build_schema_excerpt(load_schema(), inp.task_type)
|
|
1015
|
+
Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
|
|
1016
|
+
json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
|
|
1017
|
+
encoding="utf-8",
|
|
1018
|
+
)
|
|
1019
|
+
except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
|
|
1020
|
+
pass
|
|
1021
|
+
render_template_with_ctx(
|
|
1022
|
+
str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
|
|
1023
|
+
)
|
|
1024
|
+
prompt_text = (instruction_set / "claude-execution-prompt.md").read_text(encoding="utf-8")
|
|
1025
|
+
Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).parent.mkdir(parents=True, exist_ok=True)
|
|
1026
|
+
Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).write_text(prompt_text, encoding="utf-8")
|
|
1027
|
+
return prompt_text
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def _persist_run_inputs(
|
|
1031
|
+
inp: PrepareInputs,
|
|
1032
|
+
ctx: dict,
|
|
1033
|
+
models: _ModelBindings,
|
|
1034
|
+
selected_reviewers: str,
|
|
1035
|
+
brief_relative: str,
|
|
1036
|
+
) -> None:
|
|
1037
|
+
"""이 run 의 입력 스냅샷(run-inputs-*.json)을 run-manifests 디렉터리에 기록."""
|
|
1038
|
+
run_inputs_path = write_run_inputs(
|
|
1039
|
+
project_root=Path(inp.project_root),
|
|
1040
|
+
run_manifests_dir=Path(ctx["RUN_MANIFESTS_DIR"]),
|
|
1041
|
+
task_type_segment=ctx["TASK_TYPE_SEGMENT"],
|
|
1042
|
+
seq=ctx["RUN_MANIFESTS_SEQ"],
|
|
1043
|
+
inputs={
|
|
1044
|
+
"taskBriefPath": brief_relative,
|
|
1045
|
+
"taskBriefAbsolutePath": str(inp.brief_path),
|
|
1046
|
+
"directive": inp.directive,
|
|
1047
|
+
"workers": selected_reviewers,
|
|
1048
|
+
"leadModel": models.lead.display,
|
|
1049
|
+
"claudeModel": models.cw.display,
|
|
1050
|
+
"codexModel": models.co.display,
|
|
1051
|
+
"geminiModel": models.ge.display,
|
|
1052
|
+
"reportWriterModel": models.rw.display,
|
|
1053
|
+
"executor": models.executor_provider,
|
|
1054
|
+
"relatedTasks": inp.related_tasks_raw,
|
|
1055
|
+
"approvedPlanPath": inp.approved_plan_path,
|
|
1056
|
+
"clarificationResponsePath": inp.clarification_response_path,
|
|
1057
|
+
"renderOnly": inp.render_only,
|
|
1058
|
+
},
|
|
1059
|
+
)
|
|
1060
|
+
ctx["RUN_INPUTS_PATH"] = str(run_inputs_path)
|
|
1061
|
+
ctx["RUN_INPUTS_RELATIVE_PATH"] = relative_to_project_root(
|
|
1062
|
+
run_inputs_path, Path(inp.project_root)
|
|
742
1063
|
)
|
|
743
1064
|
|
|
1065
|
+
|
|
1066
|
+
def _finalize_status_and_render_manifests(
|
|
1067
|
+
inp: PrepareInputs, ctx: dict, task_index_template: Path
|
|
1068
|
+
) -> None:
|
|
1069
|
+
"""최종 task/run status 를 확정하고 workflow state 를 재계산한 뒤 team-state,
|
|
1070
|
+
task-manifest, task-index, run-manifest, timeline, discovery 산출물을 렌더한다."""
|
|
1071
|
+
if inp.render_only:
|
|
1072
|
+
ctx["CURRENT_TASK_STATUS"] = "instruction-set-generated"
|
|
1073
|
+
ctx["CURRENT_RUN_STATUS"] = "prepared"
|
|
1074
|
+
else:
|
|
1075
|
+
ctx["CURRENT_TASK_STATUS"] = "claude-session-started"
|
|
1076
|
+
ctx["CURRENT_RUN_STATUS"] = "in-progress"
|
|
1077
|
+
ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
|
|
1078
|
+
ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
|
|
1079
|
+
ctx.update(compute_workflow_state(
|
|
1080
|
+
task_type=inp.task_type,
|
|
1081
|
+
current_run_status=ctx["CURRENT_RUN_STATUS"],
|
|
1082
|
+
current_task_status=ctx["CURRENT_TASK_STATUS"],
|
|
1083
|
+
render_only=inp.render_only,
|
|
1084
|
+
work_category=inp.work_category,
|
|
1085
|
+
))
|
|
1086
|
+
render_team_state(ctx["TEAM_STATE_PATH"], ctx)
|
|
1087
|
+
render_active_run_context(ctx["ACTIVE_RUN_CONTEXT_PATH"], ctx)
|
|
1088
|
+
render_task_manifest(ctx["TASK_MANIFEST_PATH"], ctx)
|
|
1089
|
+
render_task_index(str(task_index_template), ctx["TASK_INDEX_PATH"], ctx)
|
|
1090
|
+
render_run_manifest(ctx["RUN_MANIFEST_PATH"], ctx)
|
|
1091
|
+
render_timeline(ctx["TIMELINE_PATH"], ctx)
|
|
1092
|
+
render_task_catalog_discovery(ctx["OKSTRA_TASK_CATALOG_FILE"], ctx)
|
|
1093
|
+
render_latest_task_discovery(ctx["OKSTRA_LATEST_TASK_FILE"], ctx)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _provision_settings_symlink(inp: PrepareInputs) -> None:
|
|
1097
|
+
"""non-render-only run 에서 project settings symlink 를 idempotent 하게 보장한다.
|
|
1098
|
+
실패해도 prepare 를 막지 않고 경고만 출력한다 (worker dispatch 권한 이슈)."""
|
|
1099
|
+
try:
|
|
1100
|
+
link = ensure_project_settings_symlink(project_root=Path(inp.project_root))
|
|
1101
|
+
except SettingsLinkError as exc:
|
|
1102
|
+
print(
|
|
1103
|
+
f"okstra-settings: failed to provision project settings symlink — "
|
|
1104
|
+
f"worker dispatch may be blocked by Claude Code permissions. ({exc})",
|
|
1105
|
+
file=sys.stderr,
|
|
1106
|
+
)
|
|
1107
|
+
else:
|
|
1108
|
+
if link is None:
|
|
1109
|
+
print(
|
|
1110
|
+
"okstra-settings: ~/.okstra/templates/settings.local.json missing — "
|
|
1111
|
+
"re-run 'npx okstra@latest install' (0.14.0+) to provision the symlink target.",
|
|
1112
|
+
file=sys.stderr,
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
1117
|
+
"""Produce a complete okstra task bundle on disk. See module docstring."""
|
|
1118
|
+
workspace_root = Path(inp.workspace_root)
|
|
1119
|
+
project_root = Path(inp.project_root)
|
|
1120
|
+
|
|
1121
|
+
# ---- input validation + asset resolution + roster/model 해소 ----
|
|
1122
|
+
# 각 단계는 명시적 입력/반환을 갖는 헬퍼로 분리되어 있다 (상단 phase 헬퍼
|
|
1123
|
+
# 블록 참조). 아래 orchestrator 는 그 결과를 지역 변수로 언팩해 이후 ctx
|
|
1124
|
+
# 조립 코드가 단일 흐름으로 읽히도록 유지한다.
|
|
1125
|
+
assets = _resolve_runtime_assets(workspace_root, inp)
|
|
1126
|
+
profile_file = assets.profile_file
|
|
1127
|
+
prompt_template = assets.prompt_template
|
|
1128
|
+
task_index_template = assets.task_index_template
|
|
1129
|
+
final_report_template = assets.final_report_template
|
|
1130
|
+
ctx_stage_map = _validate_prepare_inputs(project_root, inp)
|
|
1131
|
+
|
|
1132
|
+
verify_installation(workspace_root)
|
|
1133
|
+
_register_and_check_project(project_root, inp)
|
|
1134
|
+
|
|
1135
|
+
workers, selected_reviewers = _resolve_roster(inp, profile_file)
|
|
1136
|
+
pr_template_path_str, pr_template_source = _resolve_pr_template(inp)
|
|
1137
|
+
|
|
1138
|
+
models = _resolve_model_bindings(inp, workers)
|
|
1139
|
+
lead, cw, co, ge, rw = models.lead, models.cw, models.co, models.ge, models.rw
|
|
1140
|
+
critic_choice = models.critic_choice
|
|
1141
|
+
executor_provider = models.executor_provider
|
|
1142
|
+
executor_display_name = models.executor_display_name
|
|
1143
|
+
executor_worker_agent = models.executor_worker_agent
|
|
1144
|
+
executor_model_meta = models.executor_model_meta
|
|
1145
|
+
|
|
744
1146
|
# ---- paths under per-task mutex (writes run-context-*.json) ----
|
|
745
1147
|
# OKSTRA_RUN_SEQ_OVERRIDE: okstra-ctl rerun / 테스트 hook 이 미리 reserve
|
|
746
1148
|
# 한 seq 를 강제하는 user-knob 환경 변수.
|
|
@@ -867,43 +1269,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
867
1269
|
**workflow_state,
|
|
868
1270
|
})
|
|
869
1271
|
if inp.task_type == "implementation":
|
|
870
|
-
ctx
|
|
871
|
-
# Resolve the ready-set batch and append a `started` row per batched stage.
|
|
872
|
-
from .consumers import read_consumers, append_consumer
|
|
873
|
-
import datetime as _dt
|
|
874
|
-
plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
|
|
875
|
-
consumed = read_consumers(plan_run_root)
|
|
876
|
-
done_stages = {r["stage"] for r in consumed if r.get("status") == "done"}
|
|
877
|
-
effective = _resolve_effective_stages(
|
|
878
|
-
ctx["parsed_stage_map"], done_stages, inp.stage
|
|
879
|
-
)
|
|
880
|
-
ctx["effective_stages"] = effective
|
|
881
|
-
csv = ",".join(str(n) for n in effective)
|
|
882
|
-
ctx["EFFECTIVE_STAGES"] = csv
|
|
883
|
-
ctx["STAGE_BATCH_DIRECTIVE"] = (
|
|
884
|
-
f"- **Stage batch for this implementation run:** `{csv}` "
|
|
885
|
-
"(comma-separated stage numbers, ascending). Execute exactly these "
|
|
886
|
-
"Stage Map stages in this order — this is the authoritative scope. "
|
|
887
|
-
"Do NOT recompute the start stage from `consumers.jsonl`; the runtime "
|
|
888
|
-
"already selected and reserved this batch."
|
|
889
|
-
)
|
|
890
|
-
inp.stage = csv
|
|
891
|
-
print(f"selected stages: {csv}", file=sys.stdout)
|
|
892
|
-
head_proc = _subprocess.run(
|
|
893
|
-
["git", "rev-parse", "HEAD"],
|
|
894
|
-
cwd=inp.project_root, capture_output=True, text=True,
|
|
895
|
-
)
|
|
896
|
-
head_sha = head_proc.stdout.strip() if head_proc.returncode == 0 else ""
|
|
897
|
-
now = _dt.datetime.now(_dt.timezone.utc).isoformat()
|
|
898
|
-
for stage_n in effective:
|
|
899
|
-
append_consumer(
|
|
900
|
-
plan_run_root,
|
|
901
|
-
impl_task_key=ctx["TASK_KEY"],
|
|
902
|
-
stage=stage_n,
|
|
903
|
-
status="started",
|
|
904
|
-
started_at=now,
|
|
905
|
-
head_commit=head_sha,
|
|
906
|
-
)
|
|
1272
|
+
_reserve_implementation_stages(inp, ctx, ctx_stage_map)
|
|
907
1273
|
|
|
908
1274
|
# ---- prepare directories + cleanup ----
|
|
909
1275
|
_ensure_task_directories(ctx)
|
|
@@ -927,122 +1293,19 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
927
1293
|
prompt_seq=ctx["RUN_PROMPTS_SEQ"],
|
|
928
1294
|
)
|
|
929
1295
|
|
|
930
|
-
# ---- write instruction-set scaffolding ----
|
|
931
|
-
instruction_set =
|
|
932
|
-
|
|
933
|
-
profile_rendered = profile_content
|
|
934
|
-
for key in (
|
|
935
|
-
"EXECUTOR_PROVIDER",
|
|
936
|
-
"EXECUTOR_DISPLAY_NAME",
|
|
937
|
-
"EXECUTOR_WORKER_AGENT",
|
|
938
|
-
"EXECUTOR_MODEL_DISPLAY",
|
|
939
|
-
"EXECUTOR_MODEL_EXECUTION_VALUE",
|
|
940
|
-
"EXECUTOR_WORKTREE_PATH",
|
|
941
|
-
"EXECUTOR_WORKTREE_BRANCH",
|
|
942
|
-
"EXECUTOR_WORKTREE_BASE_REF",
|
|
943
|
-
"EXECUTOR_WORKTREE_STATUS",
|
|
944
|
-
"EXECUTOR_WORKTREE_NOTE",
|
|
945
|
-
):
|
|
946
|
-
profile_rendered = profile_rendered.replace("{{" + key + "}}", ctx.get(key, ""))
|
|
947
|
-
(instruction_set / "analysis-profile.md").write_text(profile_rendered, encoding="utf-8")
|
|
948
|
-
(instruction_set / "analysis-material.md").write_text(review_material, encoding="utf-8")
|
|
949
|
-
shutil.copyfile(inp.brief_path, instruction_set / "task-brief.md")
|
|
950
|
-
if inp.clarification_response_path:
|
|
951
|
-
shutil.copyfile(
|
|
952
|
-
inp.clarification_response_path,
|
|
953
|
-
instruction_set / "clarification-response.md",
|
|
954
|
-
)
|
|
955
|
-
if inp.directive:
|
|
956
|
-
(instruction_set / "directive.txt").write_text(inp.directive + "\n", encoding="utf-8")
|
|
957
|
-
render_reference_expectations(
|
|
958
|
-
str(inp.brief_path), str(instruction_set / "reference-expectations.md"), ctx,
|
|
959
|
-
)
|
|
960
|
-
# inject populates ctx with compute + default tokens consumed by the lead
|
|
961
|
-
# prompt render below (claude-execution-prompt.md). The final-report
|
|
962
|
-
# template render is effectively a copy (Jinja2 `{{ var }}` syntax does
|
|
963
|
-
# not match `_TOKEN_RE`); routed through render_template_with_ctx for SOT
|
|
964
|
-
# consistency.
|
|
965
|
-
inject_lead_prompt_computed_tokens(ctx)
|
|
966
|
-
apply_lead_prompt_defaults(ctx)
|
|
967
|
-
render_template_with_ctx(
|
|
968
|
-
str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
|
|
1296
|
+
# ---- write instruction-set scaffolding + lead prompt ----
|
|
1297
|
+
instruction_set = _write_instruction_set_sources(
|
|
1298
|
+
inp, ctx, profile_content, review_material
|
|
969
1299
|
)
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
# needs the common structure + this run's task-type block, so we write a
|
|
973
|
-
# scoped excerpt into the instruction-set rather than make the worker read
|
|
974
|
-
# the whole 44 KB / all-task-types schema (whose repo `schemas/...` path is
|
|
975
|
-
# not resolvable from a consumer project's task bundle anyway).
|
|
976
|
-
#
|
|
977
|
-
# Guarded: a missing/unreadable schema must NOT break bundle preparation.
|
|
978
|
-
# If the excerpt cannot be produced (e.g. an older install that predates
|
|
979
|
-
# the schemas/ copy step), prep proceeds without it — the report-writer
|
|
980
|
-
# still has the phase-stripped template + skill structure guide, and
|
|
981
|
-
# validation runs against the full schema regardless.
|
|
982
|
-
try:
|
|
983
|
-
_excerpt = build_schema_excerpt(load_schema(), inp.task_type)
|
|
984
|
-
Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
|
|
985
|
-
json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
|
|
986
|
-
encoding="utf-8",
|
|
987
|
-
)
|
|
988
|
-
except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
|
|
989
|
-
pass
|
|
990
|
-
render_template_with_ctx(
|
|
991
|
-
str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
|
|
1300
|
+
prompt_text = _render_lead_prompt_and_snapshot(
|
|
1301
|
+
inp, ctx, instruction_set, final_report_template, prompt_template
|
|
992
1302
|
)
|
|
993
|
-
prompt_text = (instruction_set / "claude-execution-prompt.md").read_text(encoding="utf-8")
|
|
994
|
-
Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).parent.mkdir(parents=True, exist_ok=True)
|
|
995
|
-
Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).write_text(prompt_text, encoding="utf-8")
|
|
996
1303
|
|
|
997
1304
|
# ---- run-inputs persistence ----
|
|
998
|
-
|
|
999
|
-
project_root=project_root,
|
|
1000
|
-
run_manifests_dir=Path(ctx["RUN_MANIFESTS_DIR"]),
|
|
1001
|
-
task_type_segment=ctx["TASK_TYPE_SEGMENT"],
|
|
1002
|
-
seq=ctx["RUN_MANIFESTS_SEQ"],
|
|
1003
|
-
inputs={
|
|
1004
|
-
"taskBriefPath": brief_relative,
|
|
1005
|
-
"taskBriefAbsolutePath": str(inp.brief_path),
|
|
1006
|
-
"directive": inp.directive,
|
|
1007
|
-
"workers": selected_reviewers,
|
|
1008
|
-
"leadModel": lead.display,
|
|
1009
|
-
"claudeModel": cw.display,
|
|
1010
|
-
"codexModel": co.display,
|
|
1011
|
-
"geminiModel": ge.display,
|
|
1012
|
-
"reportWriterModel": rw.display,
|
|
1013
|
-
"executor": executor_provider,
|
|
1014
|
-
"relatedTasks": inp.related_tasks_raw,
|
|
1015
|
-
"approvedPlanPath": inp.approved_plan_path,
|
|
1016
|
-
"clarificationResponsePath": inp.clarification_response_path,
|
|
1017
|
-
"renderOnly": inp.render_only,
|
|
1018
|
-
},
|
|
1019
|
-
)
|
|
1305
|
+
_persist_run_inputs(inp, ctx, models, selected_reviewers, brief_relative)
|
|
1020
1306
|
|
|
1021
|
-
# ---- final status
|
|
1022
|
-
|
|
1023
|
-
ctx["CURRENT_TASK_STATUS"] = "instruction-set-generated"
|
|
1024
|
-
ctx["CURRENT_RUN_STATUS"] = "prepared"
|
|
1025
|
-
ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
|
|
1026
|
-
ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
|
|
1027
|
-
else:
|
|
1028
|
-
ctx["CURRENT_TASK_STATUS"] = "claude-session-started"
|
|
1029
|
-
ctx["CURRENT_RUN_STATUS"] = "in-progress"
|
|
1030
|
-
ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
|
|
1031
|
-
ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
|
|
1032
|
-
ctx.update(compute_workflow_state(
|
|
1033
|
-
task_type=inp.task_type,
|
|
1034
|
-
current_run_status=ctx["CURRENT_RUN_STATUS"],
|
|
1035
|
-
current_task_status=ctx["CURRENT_TASK_STATUS"],
|
|
1036
|
-
render_only=inp.render_only,
|
|
1037
|
-
work_category=inp.work_category,
|
|
1038
|
-
))
|
|
1039
|
-
render_team_state(ctx["TEAM_STATE_PATH"], ctx)
|
|
1040
|
-
render_task_manifest(ctx["TASK_MANIFEST_PATH"], ctx)
|
|
1041
|
-
render_task_index(str(task_index_template), ctx["TASK_INDEX_PATH"], ctx)
|
|
1042
|
-
render_run_manifest(ctx["RUN_MANIFEST_PATH"], ctx)
|
|
1043
|
-
render_timeline(ctx["TIMELINE_PATH"], ctx)
|
|
1044
|
-
render_task_catalog_discovery(ctx["OKSTRA_TASK_CATALOG_FILE"], ctx)
|
|
1045
|
-
render_latest_task_discovery(ctx["OKSTRA_LATEST_TASK_FILE"], ctx)
|
|
1307
|
+
# ---- final status + manifest/discovery renders ----
|
|
1308
|
+
_finalize_status_and_render_manifests(inp, ctx, task_index_template)
|
|
1046
1309
|
|
|
1047
1310
|
# ---- central index ----
|
|
1048
1311
|
initial_status = "prepared" if inp.render_only else "running"
|
|
@@ -1063,21 +1326,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
1063
1326
|
)
|
|
1064
1327
|
|
|
1065
1328
|
if not inp.render_only:
|
|
1066
|
-
|
|
1067
|
-
link = ensure_project_settings_symlink(project_root=Path(inp.project_root))
|
|
1068
|
-
except SettingsLinkError as exc:
|
|
1069
|
-
print(
|
|
1070
|
-
f"okstra-settings: failed to provision project settings symlink — "
|
|
1071
|
-
f"worker dispatch may be blocked by Claude Code permissions. ({exc})",
|
|
1072
|
-
file=__import__("sys").stderr,
|
|
1073
|
-
)
|
|
1074
|
-
else:
|
|
1075
|
-
if link is None:
|
|
1076
|
-
print(
|
|
1077
|
-
"okstra-settings: ~/.okstra/templates/settings.local.json missing — "
|
|
1078
|
-
"re-run 'npx okstra@latest install' (0.14.0+) to provision the symlink target.",
|
|
1079
|
-
file=__import__("sys").stderr,
|
|
1080
|
-
)
|
|
1329
|
+
_provision_settings_symlink(inp)
|
|
1081
1330
|
|
|
1082
1331
|
return PrepareOutputs(
|
|
1083
1332
|
ctx=ctx,
|
|
@@ -1129,6 +1378,18 @@ def main(argv: list[str]) -> int:
|
|
|
1129
1378
|
"YAML frontmatter and appends an audit line."
|
|
1130
1379
|
),
|
|
1131
1380
|
)
|
|
1381
|
+
p.add_argument(
|
|
1382
|
+
"--implementation-option",
|
|
1383
|
+
default="",
|
|
1384
|
+
dest="implementation_option",
|
|
1385
|
+
help=(
|
|
1386
|
+
"implementation task only. Name of the Option Candidate the user "
|
|
1387
|
+
"chose from the implementation-planning final-report. Written into "
|
|
1388
|
+
"the --approved-plan file's `implementation-option:` frontmatter "
|
|
1389
|
+
"line. When omitted, implementation falls back to the plan's "
|
|
1390
|
+
"`Recommended Option`."
|
|
1391
|
+
),
|
|
1392
|
+
)
|
|
1132
1393
|
p.add_argument("--clarification-response", default="", dest="clarification_response_path")
|
|
1133
1394
|
p.add_argument(
|
|
1134
1395
|
"--pr-template-path",
|
|
@@ -1219,6 +1480,7 @@ def main(argv: list[str]) -> int:
|
|
|
1219
1480
|
pr_template_path=args.pr_template_path,
|
|
1220
1481
|
render_only=args.render_only,
|
|
1221
1482
|
approve_plan_ack=args.approve_plan_ack,
|
|
1483
|
+
implementation_option=args.implementation_option,
|
|
1222
1484
|
plan_verification_enabled=args.plan_verification_enabled,
|
|
1223
1485
|
)
|
|
1224
1486
|
try:
|