okstra 0.201.3 → 0.204.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.md +3 -3
- package/dist/cli-registry.mjs +7 -7
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +50 -124
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/setup.mjs +15 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/citation-guidance.d.mts +21 -0
- package/dist/lib/citation-guidance.mjs +79 -0
- package/dist/lib/citation-guidance.mjs.map +1 -0
- package/dist/lib/install-assets.mjs +3 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/runtime-manifest.mjs +2 -1
- package/dist/lib/runtime-manifest.mjs.map +1 -1
- package/dist/lib/types.d.mts +2 -1
- package/docs/architecture/storage-model.md +17 -10
- package/docs/architecture.md +26 -20
- package/docs/cli.md +16 -13
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +2 -3
- package/docs/project-structure-overview.md +38 -9
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/final-verification.md +3 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +36 -39
- package/package.json +1 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/common.json +28 -0
- package/runtime/agents/operations/code-review.json +6 -0
- package/runtime/agents/operations/report-translation.json +6 -0
- package/runtime/agents/operations/schedule-verification.json +6 -0
- package/runtime/agents/roles/analyser.json +18 -0
- package/runtime/agents/roles/critic.json +18 -0
- package/runtime/agents/roles/designer.json +18 -0
- package/runtime/agents/roles/implementer.json +20 -0
- package/runtime/agents/roles/leader.json +20 -0
- package/runtime/agents/roles/planner.json +18 -0
- package/runtime/agents/roles/report-writer.json +19 -0
- package/runtime/agents/roles/translator.json +19 -0
- package/runtime/agents/roles/verifier.json +18 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/prompts/duties/acceptance-critic.json +32 -0
- package/runtime/prompts/duties/acceptance-verifier.json +32 -0
- package/runtime/prompts/duties/analysis-worker.json +32 -0
- package/runtime/prompts/duties/code-reviewer.json +32 -0
- package/runtime/prompts/duties/diagnosis-worker.json +32 -0
- package/runtime/prompts/duties/direction-selection-worker.json +32 -0
- package/runtime/prompts/duties/discovery-worker.json +32 -0
- package/runtime/prompts/duties/implementation-executor.json +32 -0
- package/runtime/prompts/duties/implementation-verifier.json +32 -0
- package/runtime/prompts/duties/lead.json +32 -0
- package/runtime/prompts/duties/planning-worker.json +36 -0
- package/runtime/prompts/duties/report-writer.json +32 -0
- package/runtime/prompts/duties/reverification-worker.json +32 -0
- package/runtime/prompts/duties/schedule-verifier.json +32 -0
- package/runtime/prompts/duties/scope-critic.json +32 -0
- package/runtime/prompts/duties/technical-verification-worker.json +32 -0
- package/runtime/prompts/duties/translator.json +32 -0
- package/runtime/prompts/launch.template.md +3 -2
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
- package/runtime/prompts/lead/plan-body-verification.md +6 -6
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
- package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
- package/runtime/prompts/profiles/error-analysis.json +39 -0
- package/runtime/prompts/profiles/error-analysis.md +0 -25
- package/runtime/prompts/profiles/feature-analysis.json +31 -0
- package/runtime/prompts/profiles/feature-analysis.md +0 -20
- package/runtime/prompts/profiles/final-verification.json +30 -0
- package/runtime/prompts/profiles/final-verification.md +3 -22
- package/runtime/prompts/profiles/forbidden-actions.json +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
- package/runtime/prompts/profiles/implementation-planning.json +40 -0
- package/runtime/prompts/profiles/implementation-planning.md +6 -29
- package/runtime/prompts/profiles/implementation.json +30 -0
- package/runtime/prompts/profiles/implementation.md +1 -20
- package/runtime/prompts/profiles/improvement-discovery.json +31 -0
- package/runtime/prompts/profiles/improvement-discovery.md +0 -20
- package/runtime/prompts/profiles/project-analysis.json +31 -0
- package/runtime/prompts/profiles/project-analysis.md +0 -20
- package/runtime/prompts/profiles/release-handoff.json +5 -0
- package/runtime/prompts/profiles/release-handoff.md +71 -73
- package/runtime/prompts/profiles/requirements-discovery.json +39 -0
- package/runtime/prompts/profiles/requirements-discovery.md +0 -25
- package/runtime/prompts/profiles/technical-verification.json +39 -0
- package/runtime/prompts/profiles/technical-verification.md +0 -25
- package/runtime/prompts/wizard/prompts.ko.json +12 -17
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
- package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
- package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
- package/runtime/python/okstra_ctl/cmux.py +69 -20
- package/runtime/python/okstra_ctl/code_review_target.py +16 -8
- package/runtime/python/okstra_ctl/conformance.py +43 -0
- package/runtime/python/okstra_ctl/consumers.py +6 -3
- package/runtime/python/okstra_ctl/container.py +31 -8
- package/runtime/python/okstra_ctl/context_cost.py +11 -15
- package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -6
- package/runtime/python/okstra_ctl/convergence_provenance.py +81 -18
- package/runtime/python/okstra_ctl/design_prep.py +34 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
- package/runtime/python/okstra_ctl/error_report.py +4 -3
- package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +21 -21
- package/runtime/python/okstra_ctl/handoff.py +167 -277
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
- package/runtime/python/okstra_ctl/lead_progress.py +1 -1
- package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
- package/runtime/python/okstra_ctl/manager_cli.py +175 -14
- package/runtime/python/okstra_ctl/manager_launch.py +41 -19
- package/runtime/python/okstra_ctl/manager_paths.py +22 -3
- package/runtime/python/okstra_ctl/manager_split.py +474 -0
- package/runtime/python/okstra_ctl/manager_store.py +331 -21
- package/runtime/python/okstra_ctl/manager_sync.py +37 -16
- package/runtime/python/okstra_ctl/manager_view.py +217 -0
- package/runtime/python/okstra_ctl/model_discovery.py +30 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
- package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
- package/runtime/python/okstra_ctl/models.py +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +16 -6
- package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
- package/runtime/python/okstra_ctl/option_comparison.py +168 -0
- package/runtime/python/okstra_ctl/path_hints.py +9 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +6 -1
- package/runtime/python/okstra_ctl/profile_show.py +42 -1
- package/runtime/python/okstra_ctl/qa_commands.py +15 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
- package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
- package/runtime/python/okstra_ctl/render.py +50 -0
- package/runtime/python/okstra_ctl/report_contract.py +1 -1
- package/runtime/python/okstra_ctl/report_finalize.py +13 -6
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -21
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -5
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
- package/runtime/python/okstra_ctl/report_translation.py +2 -1
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
- package/runtime/python/okstra_ctl/role_requirements.py +142 -129
- package/runtime/python/okstra_ctl/rollup.py +3 -1
- package/runtime/python/okstra_ctl/run.py +76 -29
- package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
- package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
- package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
- package/runtime/python/okstra_ctl/stage_map.py +16 -2
- package/runtime/python/okstra_ctl/stage_targets.py +209 -43
- package/runtime/python/okstra_ctl/team.py +22 -13
- package/runtime/python/okstra_ctl/time_report.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +3 -1
- package/runtime/python/okstra_ctl/verification_target.py +13 -2
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/state.py +3 -5
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -23
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
- package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
- package/runtime/python/okstra_token_usage/pricing.py +6 -4
- package/runtime/schemas/agent-common-v1.schema.json +34 -0
- package/runtime/schemas/agent-duty-v1.schema.json +38 -0
- package/runtime/schemas/agent-operation-v1.schema.json +11 -0
- package/runtime/schemas/agent-profile-v1.schema.json +46 -0
- package/runtime/schemas/agent-role-v1.schema.json +29 -0
- package/runtime/schemas/final-report-v2.0.schema.json +118 -97
- package/runtime/schemas/final-report-v3.0.schema.json +118 -97
- package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
- package/runtime/skills/okstra-chat/SKILL.md +2 -2
- package/runtime/skills/okstra-code-review/SKILL.md +23 -9
- package/runtime/skills/okstra-container-build/SKILL.md +10 -10
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
- package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
- package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
- package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +4 -3
- package/runtime/skills/okstra-inspect/facets/time.md +11 -10
- package/runtime/skills/okstra-manager/SKILL.md +70 -5
- package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
- package/runtime/skills/okstra-rollup/SKILL.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +32 -13
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +21 -10
- package/runtime/skills/okstra-setup/references/project-config.md +7 -6
- package/runtime/skills/okstra-usage/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/manager/view.template.html +109 -0
- package/runtime/templates/report-writer-prompt-preamble.md +8 -0
- package/runtime/templates/reports/brief.template.md +14 -4
- package/runtime/templates/reports/html/i18n/en.json +7 -4
- package/runtime/templates/reports/html/i18n/ko.json +7 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +6 -4
- package/runtime/templates/translator-prompt-preamble.md +36 -0
- package/runtime/validators/checks/validate-assets-01.py +7 -8
- package/runtime/validators/validate-brief.py +77 -2
- package/runtime/validators/validate-implementation-plan-stages.py +2 -1
- package/runtime/validators/validate-run.py +59 -9
- package/runtime/validators/validate-schedule.py +9 -0
- package/docs/for-ai/README.md +0 -68
- package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
- package/docs/for-ai/skills/okstra-chat.md +0 -34
- package/docs/for-ai/skills/okstra-code-review.md +0 -57
- package/docs/for-ai/skills/okstra-container-build.md +0 -129
- package/docs/for-ai/skills/okstra-inspect.md +0 -262
- package/docs/for-ai/skills/okstra-manager.md +0 -69
- package/docs/for-ai/skills/okstra-memory.md +0 -126
- package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
- package/docs/for-ai/skills/okstra-rollup.md +0 -114
- package/docs/for-ai/skills/okstra-run.md +0 -250
- package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
- package/docs/for-ai/skills/okstra-setup.md +0 -158
- package/docs/for-ai/skills/okstra-usage.md +0 -29
- package/docs/for-ai/skills/okstra-user-response.md +0 -72
- package/runtime/agents/workers/claude-worker.md +0 -128
- package/runtime/agents/workers/report-writer-worker.md +0 -37
- package/runtime/agents/workers/translator-worker.md +0 -63
- package/runtime/prompts/duties/acceptance-critic.md +0 -44
- package/runtime/prompts/duties/acceptance-verifier.md +0 -44
- package/runtime/prompts/duties/analysis-worker.md +0 -44
- package/runtime/prompts/duties/code-reviewer.md +0 -44
- package/runtime/prompts/duties/common.md +0 -39
- package/runtime/prompts/duties/diagnosis-worker.md +0 -44
- package/runtime/prompts/duties/direction-selection-worker.md +0 -44
- package/runtime/prompts/duties/discovery-worker.md +0 -44
- package/runtime/prompts/duties/implementation-executor.md +0 -44
- package/runtime/prompts/duties/implementation-verifier.md +0 -44
- package/runtime/prompts/duties/lead.md +0 -44
- package/runtime/prompts/duties/planning-worker.md +0 -52
- package/runtime/prompts/duties/report-writer.md +0 -44
- package/runtime/prompts/duties/reverification-worker.md +0 -44
- package/runtime/prompts/duties/schedule-verifier.md +0 -44
- package/runtime/prompts/duties/scope-critic.md +0 -44
- package/runtime/prompts/duties/technical-verification-worker.md +0 -44
- package/runtime/prompts/duties/translator.md +0 -44
- package/runtime/python/okstra_ctl/pane_title.py +0 -154
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""release-handoff stage
|
|
1
|
+
"""release-handoff 의 강제 지점 — stage 하나가 PR 하나다.
|
|
2
2
|
|
|
3
|
-
자격 판정(eligible) ·
|
|
3
|
+
자격 판정(eligible) · stage 별 PR head/base 계산(pr-plan) · verified/pr 행 기록을
|
|
4
4
|
단일 모듈로 강제한다. lead 는 `okstra handoff <sub>` 로 호출만 한다.
|
|
5
|
-
설계: docs/
|
|
5
|
+
설계: .project-docs/specs/2026-09-24-release-handoff-per-stage-pr-design.md
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -10,13 +10,13 @@ from __future__ import annotations
|
|
|
10
10
|
import json
|
|
11
11
|
import subprocess
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any,
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
14
|
|
|
15
15
|
from . import consumers, stage_targets, worktree_registry
|
|
16
|
-
from .final_report_paths import
|
|
16
|
+
from .final_report_paths import final_report_data_path
|
|
17
17
|
from .handoff_verification import (
|
|
18
18
|
VerificationEvidenceError, load_latest_verification,
|
|
19
|
-
|
|
19
|
+
verification_row_is_current,
|
|
20
20
|
)
|
|
21
21
|
from .paths import RunRef
|
|
22
22
|
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
@@ -25,11 +25,12 @@ from .release_gate import (
|
|
|
25
25
|
release_handoff_allowed,
|
|
26
26
|
verdict_token,
|
|
27
27
|
)
|
|
28
|
+
from .stage_integrate import (IntegrateError, MergeBranchConflict,
|
|
29
|
+
ensure_stage_merge_branch)
|
|
28
30
|
from .stage_map import StageMapError, parse_stage_map_file, stage_map_records
|
|
29
|
-
from .worktree import (compute_branch_name,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
remove_worktree_force, MergeError, _git)
|
|
31
|
+
from .worktree import (compute_branch_name, main_worktree_path,
|
|
32
|
+
is_dirty_excluding_okstra, nested_worktree_excludes,
|
|
33
|
+
is_ancestor, remove_worktree_force, _git)
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class HandoffError(Exception):
|
|
@@ -37,7 +38,7 @@ class HandoffError(Exception):
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class HandoffConflict(Exception):
|
|
40
|
-
"""stage
|
|
41
|
+
"""선행 stage 들 사이의 merge 충돌 — exit 2, 충돌 경로 동봉."""
|
|
41
42
|
|
|
42
43
|
def __init__(self, stage: int, branch: str, paths: List[str]):
|
|
43
44
|
self.stage = stage
|
|
@@ -48,14 +49,11 @@ class HandoffConflict(Exception):
|
|
|
48
49
|
f"{', '.join(paths)}")
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def group_id_for(stages: List[int]) -> str:
|
|
52
|
-
return "g" + "-".join(str(n) for n in sorted(stages))
|
|
53
|
-
|
|
54
|
-
|
|
55
52
|
def compute_eligibility(stage_map: List[Dict[str, Any]],
|
|
56
53
|
rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
57
|
-
"""stage 별 PR 가능 여부와 차단 사유.
|
|
58
|
-
|
|
54
|
+
"""stage 별 PR 가능 여부와 차단 사유. 의존이 어느 브랜치로 내려앉는지는
|
|
55
|
+
선택 집합과 origin 의 상태가 정하므로 여기서는 판정하지 않는다
|
|
56
|
+
(`pr_plan` 의 base 해소가 담당)."""
|
|
59
57
|
eligibility = stage_targets.stage_lifecycle_snapshot_from_rows(
|
|
60
58
|
stage_map,
|
|
61
59
|
rows,
|
|
@@ -68,50 +66,6 @@ def compute_eligibility(stage_map: List[Dict[str, Any]],
|
|
|
68
66
|
return eligibility
|
|
69
67
|
|
|
70
68
|
|
|
71
|
-
def latest_whole_task_fv_release_ready(project_root, project_id: str,
|
|
72
|
-
task_group: str, task_id: str) -> str:
|
|
73
|
-
"""release-handoff 로 넘어가도 되는 whole-task 검증 보고서 경로. 없으면 ''.
|
|
74
|
-
|
|
75
|
-
최신 실행을 먼저 고르고 대상·보고서·완료 판정을 확인한다.
|
|
76
|
-
실패한 최신 실행을 건너뛰어 과거 통과 보고서를 사용하지 않는다."""
|
|
77
|
-
from okstra_project.state import find_task_root
|
|
78
|
-
root = find_task_root(Path(project_root),
|
|
79
|
-
f"{project_id}:{task_group}:{task_id}")
|
|
80
|
-
if root is None:
|
|
81
|
-
return ""
|
|
82
|
-
try:
|
|
83
|
-
report, manifest, data = load_latest_verification(
|
|
84
|
-
RunRef.from_task_root(root, "final-verification").run_dir)
|
|
85
|
-
require_finished_verification(manifest, data)
|
|
86
|
-
if manifest.get("taskKey") != f"{project_id}:{task_group}:{task_id}":
|
|
87
|
-
return ""
|
|
88
|
-
return str(report)
|
|
89
|
-
except VerificationEvidenceError:
|
|
90
|
-
# expected-miss: 최신 검증 미완료·차단은 전체 인계 선택지가 없는 정상 분기다.
|
|
91
|
-
return ""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def check_dependency_closure(
|
|
95
|
-
selected: List[int],
|
|
96
|
-
stage_map: List[Dict[str, Any]],
|
|
97
|
-
done_by_stage: Dict[int, Dict[str, Any]],
|
|
98
|
-
is_merged_to_base: Callable[[str], bool],
|
|
99
|
-
) -> List[Tuple[int, int]]:
|
|
100
|
-
"""선택 집합의 의존 폐포 위반 목록 [(stage, 미충족 선행)].
|
|
101
|
-
선행이 같은 그룹에 선택됐거나 이미 base 에 머지된 경우만 허용."""
|
|
102
|
-
sel = set(selected)
|
|
103
|
-
by_n = {s["stage_number"]: s for s in stage_map}
|
|
104
|
-
violations = []
|
|
105
|
-
for n in sorted(selected):
|
|
106
|
-
for d in by_n[n]["depends_on"]:
|
|
107
|
-
if d in sel:
|
|
108
|
-
continue
|
|
109
|
-
head = (done_by_stage.get(d) or {}).get("head_commit", "")
|
|
110
|
-
if not head or not is_merged_to_base(head):
|
|
111
|
-
violations.append((n, d))
|
|
112
|
-
return violations
|
|
113
|
-
|
|
114
|
-
|
|
115
69
|
def _run_git(args: List[str], cwd, check: bool = True) -> subprocess.CompletedProcess:
|
|
116
70
|
"""worktree._git(공용 러너) 위에 check=True 시 HandoffError 변환만 얹는다."""
|
|
117
71
|
r = _git(Path(cwd), *args)
|
|
@@ -131,128 +85,132 @@ def _require_eligible(stage_map, rows, stages) -> Dict[int, Dict[str, Any]]:
|
|
|
131
85
|
return elig
|
|
132
86
|
|
|
133
87
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
violations = check_dependency_closure(stages, stage_map, done, merged)
|
|
138
|
-
if violations:
|
|
139
|
-
lines = [
|
|
140
|
-
f"stage {n} depends on stage {d} which is neither selected nor "
|
|
141
|
-
f"merged into origin/{base_branch} — include stage {d} in the "
|
|
142
|
-
"group or PR-merge it first"
|
|
143
|
-
for n, d in violations
|
|
144
|
-
]
|
|
145
|
-
raise HandoffError("dependency closure violated:\n" + "\n".join(lines))
|
|
88
|
+
def _branch_exists(project_root, branch: str) -> bool:
|
|
89
|
+
return _git(Path(project_root), "rev-parse", "--verify", "--quiet",
|
|
90
|
+
f"refs/heads/{branch}").returncode == 0
|
|
146
91
|
|
|
147
92
|
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
93
|
+
def _stage_branch(work_category: str, task_id: str, stage: int) -> str:
|
|
94
|
+
return compute_branch_name(work_category=work_category,
|
|
95
|
+
task_id_segment=task_id, stage_number=stage)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _done_head(done: Dict[int, Dict[str, Any]], stage: int) -> str:
|
|
99
|
+
head = (done.get(stage) or {}).get("head_commit", "")
|
|
100
|
+
if not head:
|
|
101
|
+
raise HandoffError(
|
|
102
|
+
f"stage {stage} has no done row with head_commit in "
|
|
103
|
+
"consumers.jsonl — finish that implementation stage first")
|
|
104
|
+
return head
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _merge_base_for(*, project_root, project_id, task_group, task_id,
|
|
108
|
+
work_category, heads, anchor_base):
|
|
109
|
+
"""선행들을 담은 바닥 브랜치. 원시연산은 잠금 없이 돌므로 여기서 task-key
|
|
110
|
+
락을 잡는다 — implementation 경로는 이미 그 락 안에서 직접 부른다.
|
|
111
|
+
기존 브랜치에 빠진 commit 이 있으면 거부한다: 그 브랜치는 이미 열린 PR 의
|
|
112
|
+
base 일 수 있고, 말없이 옮기면 리뷰어가 본 diff 가 바뀐다."""
|
|
113
|
+
from okstra_project.dirs import okstra_home
|
|
114
|
+
|
|
115
|
+
from .locks import worktree_provision_mutex
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
with worktree_provision_mutex(okstra_home(), project_id, task_group,
|
|
119
|
+
task_id):
|
|
120
|
+
return ensure_stage_merge_branch(
|
|
121
|
+
project_root=project_root, project_id=project_id,
|
|
122
|
+
task_group=task_group, task_id=task_id,
|
|
123
|
+
work_category=work_category, heads=heads,
|
|
124
|
+
anchor_base=anchor_base)
|
|
125
|
+
except MergeBranchConflict as exc:
|
|
126
|
+
raise HandoffConflict(stage=exc.stage, branch=exc.branch,
|
|
127
|
+
paths=exc.paths) from exc
|
|
128
|
+
except IntegrateError as exc:
|
|
129
|
+
raise HandoffError(str(exc)) from exc
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _resolve_stage_base(
|
|
133
|
+
*, stage: int, deps: List[int], selected: List[int], pr_covered: set,
|
|
134
|
+
done: Dict[int, Dict[str, Any]], project_root, base_branch: str,
|
|
135
|
+
work_category: str, task_id: str, project_id: str, task_group: str,
|
|
136
|
+
anchor_base: str,
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""한 stage 의 PR base. 이미 릴리스 base 에 들어간 선행은 base 를 올리지
|
|
139
|
+
않으므로 제외하고, 남은 선행의 개수가 모양을 정한다."""
|
|
140
|
+
live = [d for d in sorted(deps)
|
|
141
|
+
if not is_ancestor(project_root, _done_head(done, d),
|
|
142
|
+
f"origin/{base_branch}")]
|
|
143
|
+
if not live:
|
|
144
|
+
commit = _run_git(["rev-parse", f"origin/{base_branch}"],
|
|
145
|
+
project_root).stdout.strip()
|
|
146
|
+
return {"base_kind": "release-base", "base_branch": base_branch,
|
|
147
|
+
"base_commit": commit, "base_stages": []}
|
|
148
|
+
|
|
149
|
+
undelivered = [d for d in live if d not in selected and d not in pr_covered]
|
|
150
|
+
if undelivered:
|
|
151
|
+
raise HandoffError(
|
|
152
|
+
f"stage {stage} depends on stage(s) {undelivered}, which are "
|
|
153
|
+
f"neither selected in this run nor already PR'd nor merged into "
|
|
154
|
+
f"origin/{base_branch} — a stacked PR needs its base branch to "
|
|
155
|
+
"exist on origin, so include them in this handoff or merge them "
|
|
156
|
+
"first")
|
|
157
|
+
|
|
158
|
+
if len(live) == 1:
|
|
159
|
+
dep = live[0]
|
|
160
|
+
branch = _stage_branch(work_category, task_id, dep)
|
|
161
|
+
if not _branch_exists(project_root, branch):
|
|
188
162
|
raise HandoffError(
|
|
189
|
-
f"stage {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
163
|
+
f"stage {stage} needs branch {branch} (stage {dep}) as its PR "
|
|
164
|
+
"base, but that branch no longer exists in this repository")
|
|
165
|
+
return {"base_kind": "stage", "base_branch": branch,
|
|
166
|
+
"base_commit": _done_head(done, dep), "base_stages": [dep]}
|
|
167
|
+
|
|
168
|
+
merged = _merge_base_for(
|
|
169
|
+
project_root=project_root, project_id=project_id,
|
|
170
|
+
task_group=task_group, task_id=task_id, work_category=work_category,
|
|
171
|
+
heads={n: _done_head(done, n) for n in live}, anchor_base=anchor_base)
|
|
172
|
+
return {"base_kind": "merge-base", "base_branch": merged.branch,
|
|
173
|
+
"base_commit": merged.commit, "base_stages": list(merged.stages),
|
|
174
|
+
"base_merge_commits": list(merged.merge_commits),
|
|
175
|
+
"base_branch_created": merged.created}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def pr_plan(*, project_root, plan_run_root, stage_map, stages, base_branch,
|
|
179
|
+
work_category, project_id, task_group, task_id) -> Dict[str, Any]:
|
|
180
|
+
"""선택한 stage 각각의 PR head/base 를 정하고, 필요한 선행 머지 브랜치를
|
|
181
|
+
만든다. PR 은 stage 당 하나이고 head 는 그 stage 의 스택 브랜치다 — 커밋
|
|
182
|
+
히스토리를 접지 않으므로 이 경로는 stage 브랜치를 고쳐 쓰지 않는다."""
|
|
203
183
|
stages = sorted(set(stages))
|
|
204
184
|
rows = consumers.read_consumers(Path(plan_run_root))
|
|
205
185
|
_require_eligible(stage_map, rows, stages)
|
|
206
186
|
_run_git(["fetch", "origin", base_branch], project_root)
|
|
207
187
|
done = consumers.latest_done_by_stage(rows)
|
|
208
|
-
|
|
188
|
+
pr_covered = consumers.pr_covered_stages(rows)
|
|
189
|
+
by_n = {s["stage_number"]: s for s in stage_map}
|
|
190
|
+
anchor_base = worktree_registry.get_implementation_base(
|
|
191
|
+
project_id, task_group, task_id) or ""
|
|
209
192
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# 분리해 두어, 예약 생성 실패 시 자신이 만들지도 않은 예약을 되돌리지 않는다.
|
|
232
|
-
try:
|
|
233
|
-
worktree_registry.reserve(
|
|
234
|
-
project_id=project_id, task_group=task_group, task_id=task_id,
|
|
235
|
-
worktree_path=str(wt_path), branch=branch, base_ref=base_commit,
|
|
236
|
-
phase="release-handoff", group_id=gid, stages=stages)
|
|
237
|
-
except RuntimeError as exc:
|
|
238
|
-
raise HandoffError(str(exc))
|
|
239
|
-
try:
|
|
240
|
-
_run_git(["worktree", "add", "-b", branch, str(wt_path), base_commit],
|
|
241
|
-
project_root)
|
|
242
|
-
merge_commits = _merge_stages(wt_path, stages, work_category, task_id,
|
|
243
|
-
project_root, project_id, task_group, gid,
|
|
244
|
-
branch, done)
|
|
245
|
-
except HandoffConflict:
|
|
246
|
-
raise # _merge_stages 가 이미 _cleanup_group 으로 예약·worktree 를 되돌림
|
|
247
|
-
except Exception:
|
|
248
|
-
# worktree add 실패 등 비-충돌 경로 — 예약이 orphan 으로 남지 않도록 역전
|
|
249
|
-
_cleanup_group(project_root, wt_path, branch, project_id, task_group,
|
|
250
|
-
task_id, gid)
|
|
251
|
-
raise
|
|
252
|
-
head = merge_commits[-1] if merge_commits else base_commit
|
|
253
|
-
return {"ok": True, "reused": False, "group_id": gid, "branch": branch,
|
|
254
|
-
"worktree_path": str(wt_path), "head": head, "stages": stages,
|
|
255
|
-
"merge_commits": merge_commits}
|
|
193
|
+
planned: List[Dict[str, Any]] = []
|
|
194
|
+
for n in stages:
|
|
195
|
+
head_branch = _stage_branch(work_category, task_id, n)
|
|
196
|
+
if not _branch_exists(project_root, head_branch):
|
|
197
|
+
raise HandoffError(
|
|
198
|
+
f"stage {n} branch {head_branch} does not exist in this "
|
|
199
|
+
"repository — it was deleted, so there is nothing to open a "
|
|
200
|
+
"PR from")
|
|
201
|
+
row = {
|
|
202
|
+
"stage": n,
|
|
203
|
+
"head_branch": head_branch,
|
|
204
|
+
"head_commit": _done_head(done, n),
|
|
205
|
+
**_resolve_stage_base(
|
|
206
|
+
stage=n, deps=list(by_n[n]["depends_on"]), selected=stages,
|
|
207
|
+
pr_covered=pr_covered, done=done, project_root=project_root,
|
|
208
|
+
base_branch=base_branch, work_category=work_category,
|
|
209
|
+
task_id=task_id, project_id=project_id, task_group=task_group,
|
|
210
|
+
anchor_base=anchor_base),
|
|
211
|
+
}
|
|
212
|
+
planned.append(row)
|
|
213
|
+
return {"ok": True, "release_base": base_branch, "stages": planned}
|
|
256
214
|
|
|
257
215
|
|
|
258
216
|
def _remove_task_worktree(main_wt, target: str) -> Optional[str]:
|
|
@@ -273,70 +231,17 @@ def _remove_task_worktree(main_wt, target: str) -> Optional[str]:
|
|
|
273
231
|
return None
|
|
274
232
|
|
|
275
233
|
|
|
276
|
-
def
|
|
277
|
-
|
|
278
|
-
"""
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
from .paths import task_runs_dir
|
|
286
|
-
|
|
287
|
-
runs = task_runs_dir(Path(project_root), task_group, task_id)
|
|
288
|
-
if not runs.is_dir():
|
|
289
|
-
return []
|
|
290
|
-
live: List[str] = []
|
|
291
|
-
for manifest in sorted(runs.rglob("run-manifest-*.json")):
|
|
292
|
-
if manifest.parent.name != "manifests":
|
|
293
|
-
continue
|
|
294
|
-
try:
|
|
295
|
-
data = load_owned_object(manifest, artifact="run manifest")
|
|
296
|
-
except JsonBoundaryError as exc:
|
|
297
|
-
raise HandoffError(
|
|
298
|
-
f"run 매니페스트를 읽을 수 없어 점유 여부를 판정할 수 없습니다 "
|
|
299
|
-
f"({manifest}): {exc}") from exc
|
|
300
|
-
if not isinstance(data, dict) or data.get("status") != "in-progress":
|
|
301
|
-
continue
|
|
302
|
-
if data.get("taskType") == exclude_task_type:
|
|
303
|
-
continue
|
|
304
|
-
live.append(f"{data.get('taskType')} seq {manifest.stem.rsplit('-', 1)[-1]}")
|
|
305
|
-
return live
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def _local_checkout_target(project_root, project_id, task_group, task_id,
|
|
309
|
-
stage: Optional[int]) -> Tuple[str, str]:
|
|
310
|
-
"""Resolve (branch, worktree_path) for a local checkout. `stage` selects
|
|
311
|
-
the stage-key row; None selects the task-key row. The returned path is ""
|
|
312
|
-
only when the row carries no `worktree_path`; teardown does not clear it,
|
|
313
|
-
so a torn-down stage yields a stale path to a directory that no longer
|
|
314
|
-
exists. The caller must probe the path before acting on it.
|
|
315
|
-
|
|
316
|
-
Occupancy is judged differently for the two row kinds. A stage row's
|
|
317
|
-
`status == "active"` is a live reservation: done-time release flips it
|
|
318
|
-
(`consumers._release_stage_occupancy_keeping_branch`,
|
|
234
|
+
def _local_checkout_target(project_id, task_group, task_id,
|
|
235
|
+
stage: int) -> tuple:
|
|
236
|
+
"""Resolve (branch, worktree_path) for a stage's local checkout. The
|
|
237
|
+
returned path is "" only when the row carries no `worktree_path`; teardown
|
|
238
|
+
does not clear it, so a torn-down stage yields a stale path to a directory
|
|
239
|
+
that no longer exists. The caller must probe the path before acting on it.
|
|
240
|
+
|
|
241
|
+
A stage row's `status == "active"` is a live reservation: done-time release
|
|
242
|
+
flips it (`consumers._release_stage_occupancy_keeping_branch`,
|
|
319
243
|
`stage_integrate._teardown_stage`), and `list_active_stage_numbers` feeds the
|
|
320
|
-
stage resolver, so an active stage row means a run holds it.
|
|
321
|
-
row has no such lifecycle — `reserve` writes `active` when the worktree is
|
|
322
|
-
created and nothing but this checkout ever releases it (observed 2026-09-07,
|
|
323
|
-
fontsninja-v3-site dev-10626: the row stayed `active` from 09-02 through five
|
|
324
|
-
completed phases, so whole-task local checkout could never be reached and
|
|
325
|
-
its "retry after the run ends" message could never come true). For the
|
|
326
|
-
task-key row the occupancy question is therefore asked of the runs
|
|
327
|
-
themselves: another run of this task still `in-progress` refuses the
|
|
328
|
-
checkout; the release-handoff run that is asking does not count."""
|
|
329
|
-
if stage is None:
|
|
330
|
-
entry = worktree_registry.lookup(project_id, task_group, task_id)
|
|
331
|
-
if not entry or not entry.branch or not entry.worktree_path:
|
|
332
|
-
raise HandoffError(
|
|
333
|
-
"task-key worktree registry 엔트리가 없습니다 — local checkout 대상 없음")
|
|
334
|
-
live = _runs_in_progress(project_root, task_group, task_id)
|
|
335
|
-
if live:
|
|
336
|
-
raise HandoffError(
|
|
337
|
-
"task 워크트리를 다른 run 이 사용 중입니다 (in-progress: "
|
|
338
|
-
f"{', '.join(live)}) — 그 run 이 끝난 뒤 다시 시도하세요")
|
|
339
|
-
return entry.branch, entry.worktree_path
|
|
244
|
+
stage resolver, so an active stage row means a run holds it."""
|
|
340
245
|
row = worktree_registry.get_stage_row(project_id, task_group, task_id, stage)
|
|
341
246
|
if not row or not row.get("branch"):
|
|
342
247
|
raise HandoffError(
|
|
@@ -350,13 +255,12 @@ def _local_checkout_target(project_root, project_id, task_group, task_id,
|
|
|
350
255
|
|
|
351
256
|
|
|
352
257
|
def local_checkout(*, project_root, project_id, task_group, task_id,
|
|
353
|
-
stage:
|
|
354
|
-
"""okstra 워크트리를 제거하고
|
|
355
|
-
(브랜치 보존).
|
|
356
|
-
브랜치가 대상이다. 메인·대상 워크트리가 dirty 면 거부. base 브랜치는
|
|
258
|
+
stage: int) -> Dict[str, Any]:
|
|
259
|
+
"""그 stage 의 okstra 워크트리를 제거하고 stage 브랜치를 메인 워크트리에
|
|
260
|
+
checkout 한다(브랜치 보존). 메인·대상 워크트리가 dirty 면 거부. base 브랜치는
|
|
357
261
|
건드리지 않는다."""
|
|
358
262
|
branch, wt_path = _local_checkout_target(
|
|
359
|
-
|
|
263
|
+
project_id, task_group, task_id, stage)
|
|
360
264
|
main_wt = main_worktree_path(Path(project_root))
|
|
361
265
|
if is_dirty_excluding_okstra(main_wt):
|
|
362
266
|
raise HandoffError(
|
|
@@ -407,26 +311,11 @@ def _impl_task_key_for(rows: List[Dict[str, Any]], stage: int) -> str:
|
|
|
407
311
|
return row.get("impl_task_key", "")
|
|
408
312
|
|
|
409
313
|
|
|
410
|
-
def _impl_task_key_for_any(rows: List[Dict[str, Any]], stages: List[int]) -> str:
|
|
411
|
-
"""task 를 식별하는 키 — stages 중 done 행이 있는 아무 stage 에서나 승계한다.
|
|
412
|
-
PR 기록은 특정 stage 의 done 에 묶일 이유가 없으므로 최저 stage 가 비어도
|
|
413
|
-
다른 stage 의 done 으로 충족한다."""
|
|
414
|
-
done = consumers.latest_done_by_stage(rows)
|
|
415
|
-
for n in sorted(stages):
|
|
416
|
-
row = done.get(n)
|
|
417
|
-
if row:
|
|
418
|
-
return row.get("impl_task_key", "")
|
|
419
|
-
raise HandoffError(
|
|
420
|
-
f"none of stages {sorted(stages)} have a done row in consumers.jsonl — "
|
|
421
|
-
"finish at least one implementation stage first")
|
|
422
|
-
|
|
423
|
-
|
|
424
314
|
def record_verified(*, plan_run_root, stage: int, report_path: str,
|
|
425
315
|
data_json) -> Dict[str, Any]:
|
|
426
316
|
"""릴리스로 넘어갈 수 있는 단독-stage 판정만 기록. data.json 의
|
|
427
317
|
taskType/scope/verdict 를 검증해 lead 가 임의 보고서를 verified 로 올리는 것을
|
|
428
|
-
막는다.
|
|
429
|
-
보고서가 실제로 실은 토큰이다."""
|
|
318
|
+
막는다. 기록되는 verdict 는 보고서가 실제로 실은 토큰이다."""
|
|
430
319
|
try:
|
|
431
320
|
data = load_owned_object(Path(data_json), artifact="final verification report")
|
|
432
321
|
except JsonBoundaryError as exc:
|
|
@@ -478,15 +367,15 @@ def _record_verified_target(plan_run_root: Path, stage: int, key: str,
|
|
|
478
367
|
raise HandoffError(str(exc)) from exc
|
|
479
368
|
|
|
480
369
|
|
|
481
|
-
def record_pr(*, plan_run_root,
|
|
370
|
+
def record_pr(*, plan_run_root, stage: int, branch: str, base: str,
|
|
482
371
|
url: str) -> Dict[str, Any]:
|
|
483
|
-
|
|
484
|
-
raise HandoffError("record-pr requires at least one stage")
|
|
372
|
+
"""한 stage 의 PR 기록. PR 은 stage 당 하나이므로 행도 stage 당 하나다."""
|
|
485
373
|
rows = consumers.read_consumers(Path(plan_run_root))
|
|
486
|
-
key =
|
|
374
|
+
key = _impl_task_key_for(rows, stage)
|
|
487
375
|
consumers.append_pr(Path(plan_run_root), impl_task_key=key,
|
|
488
|
-
stages=
|
|
489
|
-
return {"ok": True, "
|
|
376
|
+
stages=[stage], branch=branch, base=base, url=url)
|
|
377
|
+
return {"ok": True, "stage": stage, "branch": branch, "base": base,
|
|
378
|
+
"url": url}
|
|
490
379
|
|
|
491
380
|
|
|
492
381
|
def _parse_stages_csv(raw: str) -> List[int]:
|
|
@@ -503,19 +392,19 @@ _CLI_EPILOG = r"""JSON 출력.
|
|
|
503
392
|
|
|
504
393
|
Usage:
|
|
505
394
|
okstra handoff eligible --plan-run-root <dir> --approved-plan <md>
|
|
506
|
-
okstra handoff
|
|
395
|
+
okstra handoff pr-plan --plan-run-root <dir> --approved-plan <md> \
|
|
507
396
|
--project-root <dir> --project-id <id> --task-group <g> --task-id <t> \
|
|
508
397
|
--work-category <c> --stages 2,3 --base <branch>
|
|
509
398
|
okstra handoff record-verified --plan-run-root <dir> --stage <N> \
|
|
510
399
|
--report-path <md> --data-json <json>
|
|
511
|
-
okstra handoff record-pr --plan-run-root <dir> --
|
|
512
|
-
--branch <b> --url <u>
|
|
400
|
+
okstra handoff record-pr --plan-run-root <dir> --stage <N> \
|
|
401
|
+
--branch <b> --base <b> --url <u>
|
|
513
402
|
okstra handoff local-checkout --project-root <dir> --project-id <id> \
|
|
514
|
-
--task-group <g> --task-id <t>
|
|
403
|
+
--task-group <g> --task-id <t> --stage <N>
|
|
515
404
|
|
|
516
|
-
Exit codes: 0 ok / 1 자격·전제 위반 / 2 stage 간 merge 충돌(conflicts 동봉)
|
|
405
|
+
Exit codes: 0 ok / 1 자격·전제 위반 / 2 선행 stage 간 merge 충돌(conflicts 동봉)
|
|
517
406
|
"""
|
|
518
|
-
_CLI_DESCRIPTION = "Release-handoff stage
|
|
407
|
+
_CLI_DESCRIPTION = "Release-handoff stage 단위 보조 (자격/PR 계획/기록)."
|
|
519
408
|
|
|
520
409
|
|
|
521
410
|
def main(argv: Optional[list] = None) -> int:
|
|
@@ -536,7 +425,7 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
536
425
|
common(sp)
|
|
537
426
|
sp.add_argument("--approved-plan", required=True)
|
|
538
427
|
|
|
539
|
-
sp = sub.add_parser("
|
|
428
|
+
sp = sub.add_parser("pr-plan")
|
|
540
429
|
common(sp)
|
|
541
430
|
sp.add_argument("--approved-plan", required=True)
|
|
542
431
|
sp.add_argument("--project-root", default=".")
|
|
@@ -555,8 +444,9 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
555
444
|
|
|
556
445
|
sp = sub.add_parser("record-pr")
|
|
557
446
|
common(sp)
|
|
558
|
-
sp.add_argument("--
|
|
447
|
+
sp.add_argument("--stage", type=int, required=True)
|
|
559
448
|
sp.add_argument("--branch", required=True)
|
|
449
|
+
sp.add_argument("--base", required=True)
|
|
560
450
|
sp.add_argument("--url", required=True)
|
|
561
451
|
|
|
562
452
|
sp = sub.add_parser("local-checkout")
|
|
@@ -565,7 +455,7 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
565
455
|
sp.add_argument("--project-id", required=True)
|
|
566
456
|
sp.add_argument("--task-group", required=True)
|
|
567
457
|
sp.add_argument("--task-id", required=True)
|
|
568
|
-
sp.add_argument("--stage", type=int,
|
|
458
|
+
sp.add_argument("--stage", type=int, required=True)
|
|
569
459
|
|
|
570
460
|
a = p.parse_args(argv)
|
|
571
461
|
try:
|
|
@@ -573,8 +463,8 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
573
463
|
stage_map = stage_map_records(parse_stage_map_file(Path(a.approved_plan)))
|
|
574
464
|
rows = consumers.read_consumers(Path(a.plan_run_root))
|
|
575
465
|
out = {"stages": compute_eligibility(stage_map, rows)}
|
|
576
|
-
elif a.cmd == "
|
|
577
|
-
out =
|
|
466
|
+
elif a.cmd == "pr-plan":
|
|
467
|
+
out = pr_plan(
|
|
578
468
|
project_root=Path(a.project_root).resolve(),
|
|
579
469
|
plan_run_root=Path(a.plan_run_root),
|
|
580
470
|
stage_map=stage_map_records(parse_stage_map_file(Path(a.approved_plan))),
|
|
@@ -592,8 +482,8 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
592
482
|
task_id=a.task_id, stage=a.stage)
|
|
593
483
|
else:
|
|
594
484
|
out = record_pr(plan_run_root=Path(a.plan_run_root),
|
|
595
|
-
|
|
596
|
-
|
|
485
|
+
stage=a.stage, branch=a.branch, base=a.base,
|
|
486
|
+
url=a.url)
|
|
597
487
|
except HandoffConflict as exc:
|
|
598
488
|
print(json.dumps({"error": str(exc), "stage": exc.stage,
|
|
599
489
|
"branch": exc.branch, "conflicts": exc.paths},
|
|
@@ -177,6 +177,15 @@ def claim_implementation_stage_run(
|
|
|
177
177
|
candidate_base=head_sha,
|
|
178
178
|
project_root=Path(task_worktree_path),
|
|
179
179
|
plan_run_root=plan_run_root,
|
|
180
|
+
# 선행이 둘 이상인 stage 는 그 선행들만 담은 브랜치에서 시작한다.
|
|
181
|
+
# 그 브랜치를 만들 수 있는 것은 이 경로뿐이고, run.py 가 이미 task-key
|
|
182
|
+
# provision 락을 쥐고 있어 원시연산은 락을 다시 잡지 않는다.
|
|
183
|
+
identity=stage_targets.StageWorktreeIdentity(
|
|
184
|
+
project_id=inp.project_id,
|
|
185
|
+
task_group=task_group_segment,
|
|
186
|
+
task_id=task_id_segment,
|
|
187
|
+
work_category=inp.work_category,
|
|
188
|
+
),
|
|
180
189
|
)
|
|
181
190
|
except stage_targets.StageTargetError as exc:
|
|
182
191
|
raise _as_implementation_stage_error(exc) from exc
|