okstra 0.136.0 → 0.138.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/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +3 -2
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +32 -5
- package/package.json +2 -3
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
- package/runtime/bin/okstra-spawn-followups.py +4 -9
- package/runtime/prompts/lead/adapters/claude-code.md +1 -0
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/plan-body-verification.md +2 -2
- package/runtime/prompts/lead/report-writer.md +11 -10
- package/runtime/prompts/lead/team-contract.md +4 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +199 -681
- package/runtime/python/okstra_ctl/consumers.py +7 -5
- package/runtime/python/okstra_ctl/context_cost.py +4 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +146 -287
- package/runtime/python/okstra_ctl/dispatch_state.py +231 -0
- package/runtime/python/okstra_ctl/error_report.py +2 -7
- package/runtime/python/okstra_ctl/implementation_outcome.py +12 -23
- package/runtime/python/okstra_ctl/implementation_stage.py +7 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +1053 -0
- package/runtime/python/okstra_ctl/path_hints.py +3 -3
- package/runtime/python/okstra_ctl/paths.py +17 -2
- package/runtime/python/okstra_ctl/recap.py +5 -12
- package/runtime/python/okstra_ctl/render.py +18 -19
- package/runtime/python/okstra_ctl/report_finalize.py +8 -4
- package/runtime/python/okstra_ctl/report_language.py +83 -0
- package/runtime/python/okstra_ctl/run.py +262 -328
- package/runtime/python/okstra_ctl/set_work_status.py +2 -1
- package/runtime/python/okstra_ctl/stage_targets.py +330 -101
- package/runtime/python/okstra_ctl/time_report.py +4 -9
- package/runtime/python/okstra_ctl/wizard.py +47 -49
- package/runtime/python/okstra_ctl/work_categories.py +2 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +82 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +73 -13
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +7 -0
- package/runtime/python/okstra_ctl/worktree.py +37 -29
- package/runtime/python/okstra_project/__init__.py +4 -0
- package/runtime/python/okstra_project/dirs.py +7 -0
- package/runtime/python/okstra_project/state.py +78 -8
- package/runtime/validators/lib/fixtures.sh +2 -0
- package/runtime/validators/validate-run.py +3 -21
- package/src/commands/inspect/stage-map.mjs +12 -19
|
@@ -96,11 +96,7 @@ from .workflow import compute_workflow_state, load_phase_forbidden
|
|
|
96
96
|
from .locks import worktree_provision_mutex
|
|
97
97
|
from . import stage_targets as _stage_targets
|
|
98
98
|
from .plan_run_root import plan_run_root_from_approved_plan
|
|
99
|
-
from . import stage_integrate as _stage_integrate
|
|
100
99
|
from . import implementation_stage as _implementation_stage
|
|
101
|
-
from .stage_reconcile import (
|
|
102
|
-
auto_reconcile_best_effort as _stage_auto_reconcile_best_effort,
|
|
103
|
-
)
|
|
104
100
|
from .work_categories import resolve_work_category
|
|
105
101
|
from .worktree import (
|
|
106
102
|
WorktreeProvision,
|
|
@@ -437,117 +433,13 @@ def _validate_stage_structure(plan_path: str) -> None:
|
|
|
437
433
|
|
|
438
434
|
|
|
439
435
|
RUN_STEP_BUDGET = _stage_targets.RUN_STEP_BUDGET
|
|
440
|
-
_FVTarget = _stage_targets.FinalVerificationTarget
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def _stage_target_prepare_error(exc: _stage_targets.StageTargetError) -> PrepareError:
|
|
444
|
-
return PrepareError(str(exc))
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def _resolve_effective_stages(
|
|
448
|
-
stages: list,
|
|
449
|
-
done_stages: set,
|
|
450
|
-
requested: str,
|
|
451
|
-
budget: int = RUN_STEP_BUDGET,
|
|
452
|
-
started_stages: set = None,
|
|
453
|
-
reserved_stages: set = None,
|
|
454
|
-
) -> list:
|
|
455
|
-
"""Compatibility wrapper for the public stage-target policy module."""
|
|
456
|
-
try:
|
|
457
|
-
return _stage_targets.resolve_effective_stages(
|
|
458
|
-
stages,
|
|
459
|
-
done_stages,
|
|
460
|
-
requested,
|
|
461
|
-
budget=budget,
|
|
462
|
-
started_stages=started_stages,
|
|
463
|
-
reserved_stages=reserved_stages,
|
|
464
|
-
)
|
|
465
|
-
except _stage_targets.StageTargetError as exc:
|
|
466
|
-
raise _stage_target_prepare_error(exc) from exc
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
def _check_multi_dep_merged(project_root, plan_run_root, latest,
|
|
470
|
-
pred_commits: dict, candidate_base: str,
|
|
471
|
-
stage_n: int) -> None:
|
|
472
|
-
"""Compatibility wrapper for the stage-target dependency gate."""
|
|
473
|
-
try:
|
|
474
|
-
_stage_targets.check_multi_dep_merged(
|
|
475
|
-
project_root,
|
|
476
|
-
plan_run_root,
|
|
477
|
-
latest,
|
|
478
|
-
pred_commits,
|
|
479
|
-
candidate_base,
|
|
480
|
-
stage_n,
|
|
481
|
-
)
|
|
482
|
-
except _stage_targets.StageTargetError as exc:
|
|
483
|
-
raise _stage_target_prepare_error(exc) from exc
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def _resolve_stage_base_commit(
|
|
487
|
-
stage: dict,
|
|
488
|
-
consumer_done_rows: list,
|
|
489
|
-
anchor_base_commit: str,
|
|
490
|
-
candidate_base: str = "",
|
|
491
|
-
project_root=None,
|
|
492
|
-
plan_run_root=None,
|
|
493
|
-
) -> str:
|
|
494
|
-
"""Compatibility wrapper for stage worktree base selection."""
|
|
495
|
-
try:
|
|
496
|
-
return _stage_targets.resolve_stage_base_commit(
|
|
497
|
-
stage,
|
|
498
|
-
consumer_done_rows,
|
|
499
|
-
anchor_base_commit,
|
|
500
|
-
candidate_base=candidate_base,
|
|
501
|
-
project_root=project_root,
|
|
502
|
-
plan_run_root=plan_run_root,
|
|
503
|
-
)
|
|
504
|
-
except _stage_targets.StageTargetError as exc:
|
|
505
|
-
raise _stage_target_prepare_error(exc) from exc
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
def _resolve_whole_task_target(
|
|
509
|
-
*, stage_map: list, done_rows: list, anchor_base: str,
|
|
510
|
-
task_worktree_path: str, task_head: str, task_dirty: bool,
|
|
511
|
-
merged: dict,
|
|
512
|
-
) -> "_FVTarget":
|
|
513
|
-
"""Compatibility wrapper for whole-task verification target selection."""
|
|
514
|
-
try:
|
|
515
|
-
return _stage_targets.resolve_whole_task_target(
|
|
516
|
-
stage_map=stage_map,
|
|
517
|
-
done_rows=done_rows,
|
|
518
|
-
anchor_base=anchor_base,
|
|
519
|
-
task_worktree_path=task_worktree_path,
|
|
520
|
-
task_head=task_head,
|
|
521
|
-
task_dirty=task_dirty,
|
|
522
|
-
merged=merged,
|
|
523
|
-
)
|
|
524
|
-
except _stage_targets.StageTargetError as exc:
|
|
525
|
-
raise _stage_target_prepare_error(exc) from exc
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
def _resolve_single_stage_target(
|
|
529
|
-
*, requested_stage: str, done_rows: list, stage_base: str,
|
|
530
|
-
stage_worktree_path: str, stage_head: str, stage_dirty: bool,
|
|
531
|
-
) -> "_FVTarget":
|
|
532
|
-
"""Compatibility wrapper for single-stage verification target selection."""
|
|
533
|
-
try:
|
|
534
|
-
return _stage_targets.resolve_single_stage_target(
|
|
535
|
-
requested_stage=requested_stage,
|
|
536
|
-
done_rows=done_rows,
|
|
537
|
-
stage_base=stage_base,
|
|
538
|
-
stage_worktree_path=stage_worktree_path,
|
|
539
|
-
stage_head=stage_head,
|
|
540
|
-
stage_dirty=stage_dirty,
|
|
541
|
-
)
|
|
542
|
-
except _stage_targets.StageTargetError as exc:
|
|
543
|
-
raise _stage_target_prepare_error(exc) from exc
|
|
544
436
|
|
|
545
437
|
|
|
546
438
|
def _stage_map_reject_detail(stages, errs):
|
|
547
439
|
"""빈/손상 Stage Map 의 거부 사유 문자열을 돌려준다. 정상이면 None.
|
|
548
440
|
|
|
549
441
|
빈/손상 Stage Map 을 흘려보내면 whole-task 완료 게이트
|
|
550
|
-
(
|
|
442
|
+
(`_resolve_whole_task_target`의 `for stage in stage_map`)가 0회 순회로 vacuous
|
|
551
443
|
통과해 미완 task 를 '완성'으로 배포·검증한다. 빈 파싱(heading rename·표 손상)과
|
|
552
444
|
stage 번호 비단조(중간 행 누락 → S2)의 판정을 한 곳에 모아, run.py 의 prepare
|
|
553
445
|
게이트와 wizard 의 stage picker 가 같은 기준을 상속하게 한다(single-reference)."""
|
|
@@ -1509,20 +1401,6 @@ def _git_out(cwd, *args) -> str:
|
|
|
1509
1401
|
return r.stdout.strip() if r.returncode == 0 else ""
|
|
1510
1402
|
|
|
1511
1403
|
|
|
1512
|
-
def _auto_reconcile_best_effort(inp: "PrepareInputs", plan_run_root: Path) -> None:
|
|
1513
|
-
_stage_auto_reconcile_best_effort(inp, plan_run_root)
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
def _is_ancestor(cwd, commit, head) -> bool:
|
|
1517
|
-
from .worktree import is_ancestor
|
|
1518
|
-
return is_ancestor(cwd, commit, head)
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
def _is_dirty_excluding_okstra(cwd) -> bool:
|
|
1522
|
-
from .worktree import is_dirty_excluding_okstra
|
|
1523
|
-
return is_dirty_excluding_okstra(cwd)
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
1404
|
def _single_stage_final_verification_worktree(inp: "PrepareInputs") -> WorktreeProvision:
|
|
1527
1405
|
"""Placeholder until the selected stage registry row is resolved."""
|
|
1528
1406
|
return WorktreeProvision(
|
|
@@ -1552,57 +1430,46 @@ def _format_integration(integ) -> str:
|
|
|
1552
1430
|
return "\n".join(lines)
|
|
1553
1431
|
|
|
1554
1432
|
|
|
1555
|
-
def
|
|
1556
|
-
inp: "PrepareInputs",
|
|
1433
|
+
def _final_verification_target_request(
|
|
1434
|
+
inp: "PrepareInputs",
|
|
1435
|
+
ctx_stage_map: list[dict],
|
|
1436
|
+
) -> _stage_targets.FinalVerificationTargetRequest:
|
|
1437
|
+
stage = (
|
|
1438
|
+
int(inp.stage)
|
|
1439
|
+
if inp.stage and inp.stage != "auto"
|
|
1440
|
+
else None
|
|
1441
|
+
)
|
|
1442
|
+
return _stage_targets.FinalVerificationTargetRequest(
|
|
1443
|
+
project_root=Path(inp.project_root),
|
|
1444
|
+
project_id=inp.project_id,
|
|
1445
|
+
task_group=inp.task_group,
|
|
1446
|
+
task_id=inp.task_id,
|
|
1447
|
+
work_category=inp.work_category,
|
|
1448
|
+
approved_plan_path=Path(inp.approved_plan_path),
|
|
1449
|
+
stage=stage,
|
|
1450
|
+
stage_map=tuple(ctx_stage_map),
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def _apply_final_verification_target(
|
|
1455
|
+
ctx: dict,
|
|
1456
|
+
acquisition: _stage_targets.FinalVerificationTargetAcquisition,
|
|
1557
1457
|
) -> None:
|
|
1558
|
-
"""
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
# carry sidecars are the SSOT for stage completion — recover missing `done`
|
|
1566
|
-
# rows before the whole-task gate checks every stage.
|
|
1567
|
-
backfill_done_from_carry(plan_run_root)
|
|
1568
|
-
_auto_reconcile_best_effort(inp, plan_run_root)
|
|
1569
|
-
done_rows = read_stage_consumer_state(plan_run_root).done_rows
|
|
1570
|
-
|
|
1571
|
-
if inp.stage and inp.stage != "auto":
|
|
1572
|
-
n = int(inp.stage)
|
|
1573
|
-
row = _reg.get_stage_row(inp.project_id, inp.task_group, inp.task_id, n)
|
|
1574
|
-
wt_path = (row or {}).get("worktree_path", "")
|
|
1575
|
-
stage_base = (row or {}).get("base_ref", "")
|
|
1576
|
-
stage_branch = (row or {}).get("branch", "")
|
|
1577
|
-
head = _git_out(wt_path, "rev-parse", "HEAD") if wt_path else ""
|
|
1578
|
-
target = _resolve_single_stage_target(
|
|
1579
|
-
requested_stage=inp.stage, done_rows=done_rows,
|
|
1580
|
-
stage_base=stage_base, stage_worktree_path=wt_path,
|
|
1581
|
-
stage_head=head,
|
|
1582
|
-
stage_dirty=_is_dirty_excluding_okstra(wt_path) if wt_path else False,
|
|
1583
|
-
)
|
|
1584
|
-
ctx["EXECUTOR_WORKTREE_PATH"] = wt_path
|
|
1585
|
-
ctx["EXECUTOR_WORKTREE_BRANCH"] = stage_branch
|
|
1586
|
-
ctx["EXECUTOR_WORKTREE_BASE_REF"] = stage_base
|
|
1458
|
+
"""Translate acquired domain facts into render-context fields."""
|
|
1459
|
+
target = acquisition.target
|
|
1460
|
+
if target.scope == "single-stage":
|
|
1461
|
+
stage = target.stages[0]
|
|
1462
|
+
ctx["EXECUTOR_WORKTREE_PATH"] = target.worktree_path
|
|
1463
|
+
ctx["EXECUTOR_WORKTREE_BRANCH"] = acquisition.worktree_branch
|
|
1464
|
+
ctx["EXECUTOR_WORKTREE_BASE_REF"] = target.base
|
|
1587
1465
|
ctx["EXECUTOR_WORKTREE_STATUS"] = "reused-stage"
|
|
1588
1466
|
ctx["EXECUTOR_WORKTREE_NOTE"] = (
|
|
1589
|
-
f"final-verification uses implementation stage {
|
|
1467
|
+
f"final-verification uses implementation stage {stage} worktree"
|
|
1590
1468
|
)
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
inp.project_id, inp.task_group, inp.task_id) or ""
|
|
1595
|
-
# whole-task 통합(머지+teardown)+게이트 강제는 container 와 공유하는
|
|
1596
|
-
# 순수 헬퍼가 단일 책임. ctx 주입만 run.py 측에 남긴다.
|
|
1597
|
-
whole = _stage_targets.resolve_and_integrate_whole_task(
|
|
1598
|
-
project_id=inp.project_id, task_group=inp.task_group,
|
|
1599
|
-
task_id=inp.task_id, task_worktree_path=wt_path,
|
|
1600
|
-
stage_map=ctx_stage_map, done_rows=done_rows,
|
|
1601
|
-
anchor_base=anchor, teardown=True,
|
|
1469
|
+
if acquisition.integration_result is not None:
|
|
1470
|
+
ctx["STAGE_INTEGRATION"] = _format_integration(
|
|
1471
|
+
acquisition.integration_result
|
|
1602
1472
|
)
|
|
1603
|
-
ctx["STAGE_INTEGRATION"] = _format_integration(whole["integrate_result"])
|
|
1604
|
-
target = whole["target"]
|
|
1605
|
-
|
|
1606
1473
|
diff_stat = _git_out(target.worktree_path, "diff", "--stat",
|
|
1607
1474
|
f"{target.base}..{target.head}")
|
|
1608
1475
|
ctx["VERIFICATION_SCOPE"] = target.scope
|
|
@@ -1969,6 +1836,215 @@ def _provision_settings_symlink(inp: PrepareInputs) -> None:
|
|
|
1969
1836
|
)
|
|
1970
1837
|
|
|
1971
1838
|
|
|
1839
|
+
@dataclass
|
|
1840
|
+
class _ProvisionedTask:
|
|
1841
|
+
"""What one task-key's provisioning critical section hands to run-path compute."""
|
|
1842
|
+
|
|
1843
|
+
worktree: WorktreeProvision
|
|
1844
|
+
stage_run_claim: object | None
|
|
1845
|
+
stage_arg: int | None
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def _write_bundle_artifacts(
|
|
1849
|
+
inp: PrepareInputs,
|
|
1850
|
+
ctx: dict,
|
|
1851
|
+
project_root: Path,
|
|
1852
|
+
lead_runtime: str,
|
|
1853
|
+
claude_session_id: str,
|
|
1854
|
+
) -> None:
|
|
1855
|
+
"""Create the task/run directories and the artifacts the lead reads at launch.
|
|
1856
|
+
|
|
1857
|
+
Order is contractual. The fix-cycle ledger runs LAST here but still *before*
|
|
1858
|
+
the instruction-set build in the caller, so a cycle opened by this run shows
|
|
1859
|
+
up in the analysis packet's Fix History; finalize rewrites the manifest after
|
|
1860
|
+
that, which keeps the lazy-close invariant that the on-disk manifest holds
|
|
1861
|
+
pre-rewrite values when the cycle is judged.
|
|
1862
|
+
"""
|
|
1863
|
+
_ensure_task_directories(ctx)
|
|
1864
|
+
migrate_legacy_run_artifacts(ctx)
|
|
1865
|
+
cleanup_obsolete_generated_docs(
|
|
1866
|
+
project_root=project_root, instruction_set_dir=Path(ctx["INSTRUCTION_SET_PATH"]),
|
|
1867
|
+
)
|
|
1868
|
+
if lead_runtime == "claude-code":
|
|
1869
|
+
# Always materialise the resume command script for Claude Code. Even in
|
|
1870
|
+
# --render-only preparation flows the user (or a later non-interactive
|
|
1871
|
+
# runner) may invoke it manually; deferring its creation until
|
|
1872
|
+
# interactive launch leaves runs/<phase>/sessions/ empty and the
|
|
1873
|
+
# manifest pointing at a path that does not exist. Codex runs resume via
|
|
1874
|
+
# artifacts/checkpoints instead, so they intentionally leave this empty.
|
|
1875
|
+
write_claude_resume_command_file(
|
|
1876
|
+
resume_command_path=Path(ctx["CLAUDE_RESUME_COMMAND_PATH"]),
|
|
1877
|
+
project_root=project_root,
|
|
1878
|
+
claude_session_id=claude_session_id,
|
|
1879
|
+
task_key=ctx["TASK_KEY"],
|
|
1880
|
+
task_type=ctx["TASK_TYPE"],
|
|
1881
|
+
phase_state=ctx["CURRENT_RUN_STATUS"],
|
|
1882
|
+
worker_prompts_dir_relative=ctx["RUN_PROMPTS_RELATIVE_PATH"],
|
|
1883
|
+
prompt_seq=ctx["RUN_PROMPTS_SEQ"],
|
|
1884
|
+
)
|
|
1885
|
+
ctx["FIX_CYCLE_ID"] = _record_fix_cycle_events(inp, ctx)
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
def _record_run_in_central_index(
|
|
1889
|
+
inp: PrepareInputs,
|
|
1890
|
+
ctx: dict,
|
|
1891
|
+
workspace_root: Path,
|
|
1892
|
+
run_seq_override: int | None,
|
|
1893
|
+
) -> None:
|
|
1894
|
+
"""Publish this run to ~/.okstra/{recent,active}.jsonl. Never fatal.
|
|
1895
|
+
|
|
1896
|
+
A failure here leaves the central index incomplete but the bundle on disk is
|
|
1897
|
+
already usable, so prepare reports and continues. The rerun path pre-stamps a
|
|
1898
|
+
'reserving' row before spawning (OKSTRA_RUN_SEQ_OVERRIDE forces that seq); if
|
|
1899
|
+
the promotion to 'running' fails, that row would sit in active.jsonl forever
|
|
1900
|
+
and inflate activeCount, so it is cleaned up whenever the reserved seq is known.
|
|
1901
|
+
"""
|
|
1902
|
+
try:
|
|
1903
|
+
_record_start(
|
|
1904
|
+
workspace_root=workspace_root,
|
|
1905
|
+
ctx=ctx,
|
|
1906
|
+
initial_status="prepared" if inp.render_only else "running",
|
|
1907
|
+
canonical_argv=_canonical_argv(inp, ctx),
|
|
1908
|
+
cwd=os.getcwd(),
|
|
1909
|
+
brief_sha256=_brief_sha256(inp.brief_path),
|
|
1910
|
+
)
|
|
1911
|
+
except Exception as exc:
|
|
1912
|
+
print(
|
|
1913
|
+
f"okstra-central: record_start failed; central index will be incomplete ({exc})",
|
|
1914
|
+
file=sys.stderr,
|
|
1915
|
+
)
|
|
1916
|
+
if run_seq_override is not None:
|
|
1917
|
+
_remove_leaked_reservation(ctx, run_seq_override)
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
def _initial_workflow_ctx(
|
|
1921
|
+
inp: PrepareInputs, forbidden_by_phase: dict
|
|
1922
|
+
) -> dict[str, str]:
|
|
1923
|
+
"""Workflow tokens for a run that has been prepared but not started.
|
|
1924
|
+
|
|
1925
|
+
The run status is not known yet, so both statuses are the pre-launch
|
|
1926
|
+
constants; `_finalize_status_and_render_manifests` recomputes the block once
|
|
1927
|
+
the real status is settled.
|
|
1928
|
+
"""
|
|
1929
|
+
task_status, run_status = "ready-for-claude", "not-run"
|
|
1930
|
+
return {
|
|
1931
|
+
"CURRENT_TASK_STATUS": task_status,
|
|
1932
|
+
"CURRENT_RUN_STATUS": run_status,
|
|
1933
|
+
**compute_workflow_state(
|
|
1934
|
+
task_type=inp.task_type,
|
|
1935
|
+
current_run_status=run_status,
|
|
1936
|
+
current_task_status=task_status,
|
|
1937
|
+
render_only=inp.render_only,
|
|
1938
|
+
forbidden_by_phase=forbidden_by_phase,
|
|
1939
|
+
),
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
def _related_tasks_ctx(ctx: dict, inp: PrepareInputs) -> dict[str, str]:
|
|
1944
|
+
"""Render tokens for the tasks this run declares a relation to."""
|
|
1945
|
+
items = resolve_related_tasks(
|
|
1946
|
+
task_manifest_path=Path(ctx["TASK_MANIFEST_PATH"]),
|
|
1947
|
+
raw_related=inp.related_tasks_raw,
|
|
1948
|
+
)
|
|
1949
|
+
return {
|
|
1950
|
+
"RELATED_TASKS_JSON": json.dumps(items, ensure_ascii=False),
|
|
1951
|
+
"RELATED_TASKS_BULLETS": related_tasks_bullets(items),
|
|
1952
|
+
"RELATED_TASKS_INLINE": related_tasks_inline(items),
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
|
|
1956
|
+
def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
|
|
1957
|
+
"""Render tokens for every model binding this run resolved."""
|
|
1958
|
+
return {
|
|
1959
|
+
"LEAD_MODEL": models.lead.display,
|
|
1960
|
+
"LEAD_MODEL_EXECUTION_VALUE": models.lead.execution,
|
|
1961
|
+
"CLAUDE_WORKER_MODEL": models.cw.display,
|
|
1962
|
+
"CLAUDE_WORKER_MODEL_EXECUTION_VALUE": models.cw.execution,
|
|
1963
|
+
"CODEX_WORKER_MODEL": models.co.display,
|
|
1964
|
+
"CODEX_WORKER_MODEL_EXECUTION_VALUE": models.codex_worker_execution,
|
|
1965
|
+
"ANTIGRAVITY_WORKER_MODEL": models.ge.display,
|
|
1966
|
+
"ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": models.antigravity_worker_execution,
|
|
1967
|
+
"REPORT_WRITER_MODEL": models.rw.display,
|
|
1968
|
+
"REPORT_WRITER_MODEL_EXECUTION_VALUE": models.rw.execution,
|
|
1969
|
+
"EXECUTOR_PROVIDER": models.executor_provider,
|
|
1970
|
+
"EXECUTOR_DISPLAY_NAME": models.executor_display_name,
|
|
1971
|
+
"EXECUTOR_WORKER_AGENT": models.executor_worker_agent,
|
|
1972
|
+
"EXECUTOR_MODEL_DISPLAY": models.executor_model_meta.display,
|
|
1973
|
+
"EXECUTOR_MODEL_EXECUTION_VALUE": models.executor_execution,
|
|
1974
|
+
"CRITIC_CHOICE": models.critic_choice,
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
def _provision_task_and_stage(
|
|
1979
|
+
inp: PrepareInputs,
|
|
1980
|
+
project_root: Path,
|
|
1981
|
+
ctx_stage_map: list,
|
|
1982
|
+
task_group_segment: str,
|
|
1983
|
+
task_id_segment: str,
|
|
1984
|
+
task_key: str,
|
|
1985
|
+
) -> _ProvisionedTask:
|
|
1986
|
+
"""Reserve this run's worktree and, for implementation, its Stage Map stage.
|
|
1987
|
+
|
|
1988
|
+
One worktree per task-key: requirements-discovery, error-analysis,
|
|
1989
|
+
implementation-planning and implementation phases of the same task all share
|
|
1990
|
+
this directory and branch. Runs BEFORE run-path compute: the worktree's
|
|
1991
|
+
degrade status (skipped-*) feeds the implementation Stage Run Claim, and the
|
|
1992
|
+
resolved stage namespaces the run path.
|
|
1993
|
+
|
|
1994
|
+
The whole check→select→`git worktree add`→reserve sequence (task worktree AND
|
|
1995
|
+
implementation stage) is one critical section per task-key: the registry lock
|
|
1996
|
+
alone only covers the reserve row, so without this mutex two concurrent runs
|
|
1997
|
+
can pick the same stage or race the same path/branch at the git level (TOCTOU).
|
|
1998
|
+
"""
|
|
1999
|
+
with worktree_provision_mutex(
|
|
2000
|
+
okstra_home(), inp.project_id, task_group_segment, task_id_segment,
|
|
2001
|
+
):
|
|
2002
|
+
if inp.task_type == "final-verification" and inp.stage and inp.stage != "auto":
|
|
2003
|
+
worktree = _single_stage_final_verification_worktree(inp)
|
|
2004
|
+
# Single-stage final-verification namespaces its run path under
|
|
2005
|
+
# runs/final-verification/stage-<N> (same isolation as
|
|
2006
|
+
# implementation) so concurrent per-stage verifications never
|
|
2007
|
+
# share state/reports/worker-results.
|
|
2008
|
+
fv_stage_arg = int(inp.stage)
|
|
2009
|
+
else:
|
|
2010
|
+
fv_stage_arg = None
|
|
2011
|
+
try:
|
|
2012
|
+
worktree = provision_task_worktree(
|
|
2013
|
+
task_type=inp.task_type,
|
|
2014
|
+
project_root=project_root,
|
|
2015
|
+
project_id=inp.project_id,
|
|
2016
|
+
task_group_segment=task_group_segment,
|
|
2017
|
+
task_id_segment=task_id_segment,
|
|
2018
|
+
work_category=inp.work_category,
|
|
2019
|
+
base_ref=inp.base_ref,
|
|
2020
|
+
require_base_ref=True,
|
|
2021
|
+
)
|
|
2022
|
+
except RuntimeError as exc:
|
|
2023
|
+
raise PrepareError(
|
|
2024
|
+
f"task worktree provisioning failed: {exc}"
|
|
2025
|
+
) from exc
|
|
2026
|
+
|
|
2027
|
+
# ---- implementation Stage Run Claim (path-independent) ----
|
|
2028
|
+
# Resolve + provision the stage BEFORE run-path compute so RUN_DIR
|
|
2029
|
+
# lands in runs/implementation/stage-<N>. The registry stage-key is
|
|
2030
|
+
# reserved exactly once here (inside provision_stage_worktree), and
|
|
2031
|
+
# the surrounding mutex makes the registry read in the claim
|
|
2032
|
+
# and that reserve atomic. Other task-types skip this claim;
|
|
2033
|
+
# single-stage final-verification threads its explicit stage via
|
|
2034
|
+
# fv_stage_arg, everything else keeps stage_arg=None (flat paths).
|
|
2035
|
+
if inp.task_type == "implementation":
|
|
2036
|
+
stage_run_claim = _claim_implementation_stage_run(
|
|
2037
|
+
inp, ctx_stage_map, task_group_segment, task_id_segment,
|
|
2038
|
+
task_key, worktree.status,
|
|
2039
|
+
)
|
|
2040
|
+
# Drop any stale waiver on this stage so the run actually verifies
|
|
2041
|
+
# conformance (kept inside the per-task-key mutex so concurrent
|
|
2042
|
+
# same-task runs don't race the manifest write).
|
|
2043
|
+
_clear_stale_stage_waiver(inp, project_root, stage_run_claim.stage)
|
|
2044
|
+
return _ProvisionedTask(worktree, stage_run_claim, stage_run_claim.stage)
|
|
2045
|
+
return _ProvisionedTask(worktree, None, fv_stage_arg)
|
|
2046
|
+
|
|
2047
|
+
|
|
1972
2048
|
def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
1973
2049
|
"""Produce a complete okstra task bundle on disk. See module docstring."""
|
|
1974
2050
|
workspace_root = Path(inp.workspace_root)
|
|
@@ -2011,12 +2087,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2011
2087
|
pr_template_path_str, pr_template_source = _resolve_pr_template(inp)
|
|
2012
2088
|
|
|
2013
2089
|
models = _resolve_model_bindings(inp, workers)
|
|
2014
|
-
lead, cw, co, ge, rw = models.lead, models.cw, models.co, models.ge, models.rw
|
|
2015
|
-
critic_choice = models.critic_choice
|
|
2016
|
-
executor_provider = models.executor_provider
|
|
2017
|
-
executor_display_name = models.executor_display_name
|
|
2018
|
-
executor_worker_agent = models.executor_worker_agent
|
|
2019
|
-
executor_model_meta = models.executor_model_meta
|
|
2020
2090
|
|
|
2021
2091
|
# ---- paths under per-task mutex (writes run-context-*.json) ----
|
|
2022
2092
|
# OKSTRA_RUN_SEQ_OVERRIDE: okstra-ctl rerun / 테스트 hook 이 미리 reserve
|
|
@@ -2043,67 +2113,13 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2043
2113
|
task_id=inp.task_id,
|
|
2044
2114
|
)
|
|
2045
2115
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
# The whole check→select→`git worktree add`→reserve sequence (task
|
|
2054
|
-
# worktree AND implementation stage) is one critical section per
|
|
2055
|
-
# task-key: the registry lock alone only covers the reserve row, so
|
|
2056
|
-
# without this mutex two concurrent runs can pick the same stage or
|
|
2057
|
-
# race the same path/branch at the git level (TOCTOU).
|
|
2058
|
-
with worktree_provision_mutex(
|
|
2059
|
-
okstra_home(), inp.project_id, task_group_segment, task_id_segment,
|
|
2060
|
-
):
|
|
2061
|
-
if inp.task_type == "final-verification" and inp.stage and inp.stage != "auto":
|
|
2062
|
-
worktree = _single_stage_final_verification_worktree(inp)
|
|
2063
|
-
# Single-stage final-verification namespaces its run path under
|
|
2064
|
-
# runs/final-verification/stage-<N> (same isolation as
|
|
2065
|
-
# implementation) so concurrent per-stage verifications never
|
|
2066
|
-
# share state/reports/worker-results.
|
|
2067
|
-
fv_stage_arg = int(inp.stage)
|
|
2068
|
-
else:
|
|
2069
|
-
fv_stage_arg = None
|
|
2070
|
-
try:
|
|
2071
|
-
worktree = provision_task_worktree(
|
|
2072
|
-
task_type=inp.task_type,
|
|
2073
|
-
project_root=project_root,
|
|
2074
|
-
project_id=inp.project_id,
|
|
2075
|
-
task_group_segment=task_group_segment,
|
|
2076
|
-
task_id_segment=task_id_segment,
|
|
2077
|
-
work_category=inp.work_category,
|
|
2078
|
-
base_ref=inp.base_ref,
|
|
2079
|
-
require_base_ref=True,
|
|
2080
|
-
)
|
|
2081
|
-
except RuntimeError as exc:
|
|
2082
|
-
raise PrepareError(
|
|
2083
|
-
f"task worktree provisioning failed: {exc}"
|
|
2084
|
-
) from exc
|
|
2085
|
-
|
|
2086
|
-
# ---- implementation Stage Run Claim (path-independent) ----
|
|
2087
|
-
# Resolve + provision the stage BEFORE run-path compute so RUN_DIR
|
|
2088
|
-
# lands in runs/implementation/stage-<N>. The registry stage-key is
|
|
2089
|
-
# reserved exactly once here (inside provision_stage_worktree), and
|
|
2090
|
-
# the surrounding mutex makes the registry read in the claim
|
|
2091
|
-
# and that reserve atomic. Other task-types skip this claim;
|
|
2092
|
-
# single-stage final-verification threads its explicit stage via
|
|
2093
|
-
# fv_stage_arg, everything else keeps stage_arg=None (flat paths).
|
|
2094
|
-
if inp.task_type == "implementation":
|
|
2095
|
-
stage_run_claim = _claim_implementation_stage_run(
|
|
2096
|
-
inp, ctx_stage_map, task_group_segment, task_id_segment,
|
|
2097
|
-
task_key, worktree.status,
|
|
2098
|
-
)
|
|
2099
|
-
stage_arg = stage_run_claim.stage
|
|
2100
|
-
# Drop any stale waiver on this stage so the run actually verifies
|
|
2101
|
-
# conformance (kept inside the per-task-key mutex so concurrent
|
|
2102
|
-
# same-task runs don't race the manifest write).
|
|
2103
|
-
_clear_stale_stage_waiver(inp, project_root, stage_run_claim.stage)
|
|
2104
|
-
else:
|
|
2105
|
-
stage_run_claim = None
|
|
2106
|
-
stage_arg = fv_stage_arg
|
|
2116
|
+
provisioned = _provision_task_and_stage(
|
|
2117
|
+
inp, project_root, ctx_stage_map,
|
|
2118
|
+
task_group_segment, task_id_segment, task_key,
|
|
2119
|
+
)
|
|
2120
|
+
worktree = provisioned.worktree
|
|
2121
|
+
stage_run_claim = provisioned.stage_run_claim
|
|
2122
|
+
stage_arg = provisioned.stage_arg
|
|
2107
2123
|
|
|
2108
2124
|
ctx = compute_and_write_run_context(
|
|
2109
2125
|
workspace_root=workspace_root, project_root=project_root,
|
|
@@ -2154,13 +2170,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2154
2170
|
# ---- material + related-tasks ----
|
|
2155
2171
|
profile_content = _expand_profile_includes(profile_file)
|
|
2156
2172
|
review_material = build_analysis_material(inp.brief_path, inp.directive)
|
|
2157
|
-
related_items = resolve_related_tasks(
|
|
2158
|
-
task_manifest_path=Path(ctx["TASK_MANIFEST_PATH"]),
|
|
2159
|
-
raw_related=inp.related_tasks_raw,
|
|
2160
|
-
)
|
|
2161
|
-
related_tasks_json_str = json.dumps(related_items, ensure_ascii=False)
|
|
2162
|
-
bullets = related_tasks_bullets(related_items)
|
|
2163
|
-
inline = related_tasks_inline(related_items)
|
|
2164
2173
|
|
|
2165
2174
|
# ---- relative paths for brief + clarification ----
|
|
2166
2175
|
brief_relative = relative_to_project_root(inp.brief_path, project_root)
|
|
@@ -2169,17 +2178,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2169
2178
|
if inp.clarification_response_path else ""
|
|
2170
2179
|
)
|
|
2171
2180
|
|
|
2172
|
-
# ---- initial workflow state (current_run_status not yet known) ----
|
|
2173
|
-
initial_task_status = "ready-for-claude"
|
|
2174
|
-
initial_run_status = "not-run"
|
|
2175
2181
|
forbidden_by_phase = load_phase_forbidden(workspace_root)
|
|
2176
|
-
workflow_state = compute_workflow_state(
|
|
2177
|
-
task_type=inp.task_type,
|
|
2178
|
-
current_run_status=initial_run_status,
|
|
2179
|
-
current_task_status=initial_task_status,
|
|
2180
|
-
render_only=inp.render_only,
|
|
2181
|
-
forbidden_by_phase=forbidden_by_phase,
|
|
2182
|
-
)
|
|
2183
2182
|
|
|
2184
2183
|
# ---- assemble full ctx (the values render functions expect) ----
|
|
2185
2184
|
ctx.update({
|
|
@@ -2196,27 +2195,9 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2196
2195
|
"CLARIFICATION_RESPONSE_RELATIVE_PATH": clarification_relative,
|
|
2197
2196
|
"BRIEF_FILE_PATH": str(inp.brief_path),
|
|
2198
2197
|
"BRIEF_RELATIVE_PATH": brief_relative,
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
"CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
|
|
2203
|
-
"CODEX_WORKER_MODEL": co.display,
|
|
2204
|
-
"CODEX_WORKER_MODEL_EXECUTION_VALUE": models.codex_worker_execution,
|
|
2205
|
-
"ANTIGRAVITY_WORKER_MODEL": ge.display,
|
|
2206
|
-
"ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": models.antigravity_worker_execution,
|
|
2207
|
-
"REPORT_WRITER_MODEL": rw.display,
|
|
2208
|
-
"REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
|
|
2209
|
-
"EXECUTOR_PROVIDER": executor_provider,
|
|
2210
|
-
"EXECUTOR_DISPLAY_NAME": executor_display_name,
|
|
2211
|
-
"EXECUTOR_WORKER_AGENT": executor_worker_agent,
|
|
2212
|
-
"EXECUTOR_MODEL_DISPLAY": executor_model_meta.display,
|
|
2213
|
-
"EXECUTOR_MODEL_EXECUTION_VALUE": models.executor_execution,
|
|
2214
|
-
"CRITIC_CHOICE": critic_choice,
|
|
2215
|
-
"RELATED_TASKS_JSON": related_tasks_json_str,
|
|
2216
|
-
"RELATED_TASKS_BULLETS": bullets,
|
|
2217
|
-
"RELATED_TASKS_INLINE": inline,
|
|
2218
|
-
"CURRENT_TASK_STATUS": initial_task_status,
|
|
2219
|
-
"CURRENT_RUN_STATUS": initial_run_status,
|
|
2198
|
+
**_model_ctx(models),
|
|
2199
|
+
**_related_tasks_ctx(ctx, inp),
|
|
2200
|
+
**_initial_workflow_ctx(inp, forbidden_by_phase),
|
|
2220
2201
|
"VALIDATION_STATUS": "not-run",
|
|
2221
2202
|
"VALIDATION_UPDATED_AT": "",
|
|
2222
2203
|
"VALIDATION_FAILURES_JSON": "[]",
|
|
@@ -2224,45 +2205,20 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2224
2205
|
"LATEST_REPORT_RELATIVE_PATH": "",
|
|
2225
2206
|
"RENDER_ONLY": "true" if inp.render_only else "false",
|
|
2226
2207
|
"OKSTRA_VERSION": installed_version(),
|
|
2227
|
-
**workflow_state,
|
|
2228
2208
|
})
|
|
2229
2209
|
if lead_runtime == "codex":
|
|
2230
2210
|
ctx["CLAUDE_RESUME_COMMAND_PATH"] = ""
|
|
2231
2211
|
ctx["CLAUDE_RESUME_COMMAND_RELATIVE_PATH"] = ""
|
|
2232
2212
|
ctx["CLAUDE_RESUME_COMMAND_FILENAME"] = ""
|
|
2233
2213
|
if inp.task_type == "final-verification":
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
# ---- prepare directories + cleanup ----
|
|
2237
|
-
_ensure_task_directories(ctx)
|
|
2238
|
-
migrate_legacy_run_artifacts(ctx)
|
|
2239
|
-
cleanup_obsolete_generated_docs(
|
|
2240
|
-
project_root=project_root, instruction_set_dir=Path(ctx["INSTRUCTION_SET_PATH"]),
|
|
2241
|
-
)
|
|
2242
|
-
if lead_runtime == "claude-code":
|
|
2243
|
-
# Always materialise the resume command script for Claude Code. Even in
|
|
2244
|
-
# --render-only preparation flows the user (or a later non-interactive
|
|
2245
|
-
# runner) may invoke it manually; deferring its creation until
|
|
2246
|
-
# interactive launch leaves runs/<phase>/sessions/ empty and the
|
|
2247
|
-
# manifest pointing at a path that does not exist. Codex runs resume via
|
|
2248
|
-
# artifacts/checkpoints instead, so they intentionally leave this empty.
|
|
2249
|
-
write_claude_resume_command_file(
|
|
2250
|
-
resume_command_path=Path(ctx["CLAUDE_RESUME_COMMAND_PATH"]),
|
|
2251
|
-
project_root=project_root,
|
|
2252
|
-
claude_session_id=claude_session_id,
|
|
2253
|
-
task_key=ctx["TASK_KEY"],
|
|
2254
|
-
task_type=ctx["TASK_TYPE"],
|
|
2255
|
-
phase_state=ctx["CURRENT_RUN_STATUS"],
|
|
2256
|
-
worker_prompts_dir_relative=ctx["RUN_PROMPTS_RELATIVE_PATH"],
|
|
2257
|
-
prompt_seq=ctx["RUN_PROMPTS_SEQ"],
|
|
2214
|
+
acquisition = _stage_targets.acquire_final_verification_target(
|
|
2215
|
+
_final_verification_target_request(inp, ctx_stage_map)
|
|
2258
2216
|
)
|
|
2217
|
+
_apply_final_verification_target(ctx, acquisition)
|
|
2259
2218
|
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
# 여전히 이후에 manifest 를 재작성하므로 lazy-close 의 "디스크 manifest 는
|
|
2264
|
-
# prepare 재작성 전 값" 불변식은 유지된다.
|
|
2265
|
-
ctx["FIX_CYCLE_ID"] = _record_fix_cycle_events(inp, ctx)
|
|
2219
|
+
_write_bundle_artifacts(
|
|
2220
|
+
inp, ctx, project_root, lead_runtime, claude_session_id,
|
|
2221
|
+
)
|
|
2266
2222
|
|
|
2267
2223
|
# ---- write instruction-set scaffolding + lead prompt ----
|
|
2268
2224
|
instruction_set = _write_instruction_set_sources(
|
|
@@ -2281,29 +2237,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2281
2237
|
)
|
|
2282
2238
|
_record_artifact_runtime_render_only_event(inp, ctx)
|
|
2283
2239
|
|
|
2284
|
-
|
|
2285
|
-
initial_status = "prepared" if inp.render_only else "running"
|
|
2286
|
-
canonical_argv = _canonical_argv(inp, ctx)
|
|
2287
|
-
try:
|
|
2288
|
-
_record_start(
|
|
2289
|
-
workspace_root=workspace_root,
|
|
2290
|
-
ctx=ctx,
|
|
2291
|
-
initial_status=initial_status,
|
|
2292
|
-
canonical_argv=canonical_argv,
|
|
2293
|
-
cwd=os.getcwd(),
|
|
2294
|
-
brief_sha256=_brief_sha256(inp.brief_path),
|
|
2295
|
-
)
|
|
2296
|
-
except Exception as exc:
|
|
2297
|
-
print(
|
|
2298
|
-
f"okstra-central: record_start failed; central index will be incomplete ({exc})",
|
|
2299
|
-
file=sys.stderr,
|
|
2300
|
-
)
|
|
2301
|
-
# rerun 경로는 spawn 전에 'reserving' row 를 미리 박아 둔다
|
|
2302
|
-
# (OKSTRA_RUN_SEQ_OVERRIDE 가 그 seq 를 강제한다). record_start 가
|
|
2303
|
-
# 'reserving' → 'running' 승격에 실패하면 그 row 가 active.jsonl 에
|
|
2304
|
-
# 영구히 남아 activeCount 를 부풀린다. 예약 seq 를 알 때만 정리한다.
|
|
2305
|
-
if run_seq_override is not None:
|
|
2306
|
-
_remove_leaked_reservation(ctx, run_seq_override)
|
|
2240
|
+
_record_run_in_central_index(inp, ctx, workspace_root, run_seq_override)
|
|
2307
2241
|
|
|
2308
2242
|
if not inp.render_only:
|
|
2309
2243
|
_provision_settings_symlink(inp)
|