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,10 +1,15 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""stage done commit 을 한 곳에 모으는 두 가지 방법.
|
|
2
2
|
|
|
3
|
-
Phase A: stage done commit 을 task 브랜치에
|
|
4
|
-
skip, 충돌이면 해당 머지만 abort 후 IntegrateError).
|
|
5
|
-
stage 의 worktree 디렉터리와 registry 키를 정리(dirty 면 skip).
|
|
6
|
-
스택 보존을 위해 삭제하지 않는다.
|
|
3
|
+
`integrate_stages` — whole-task 통합. Phase A: stage done commit 을 task 브랜치에
|
|
4
|
+
위상순 --no-ff 머지(이미 머지면 skip, 충돌이면 해당 머지만 abort 후 IntegrateError).
|
|
5
|
+
Phase B: 머지/skip 된 stage 의 worktree 디렉터리와 registry 키를 정리(dirty 면 skip).
|
|
6
|
+
stage 브랜치는 스택 보존을 위해 삭제하지 않는다.
|
|
7
7
|
설계: docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md
|
|
8
|
+
|
|
9
|
+
`ensure_stage_merge_branch` — 여러 stage 의 done commit 만 담은 브랜치를 만든다.
|
|
10
|
+
선행이 둘 이상인 stage 의 바닥(구현 워크트리의 base, PR 의 base)이 필요할 때 쓰며,
|
|
11
|
+
task 브랜치를 건드리지 않는다.
|
|
12
|
+
설계: .project-docs/specs/2026-09-24-release-handoff-per-stage-pr-design.md §4
|
|
8
13
|
"""
|
|
9
14
|
from __future__ import annotations
|
|
10
15
|
|
|
@@ -28,6 +33,141 @@ class IntegrateResult:
|
|
|
28
33
|
warnings: list[str] = field(default_factory=list)
|
|
29
34
|
|
|
30
35
|
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class StageMergeBranch:
|
|
38
|
+
branch: str
|
|
39
|
+
commit: str
|
|
40
|
+
created: bool
|
|
41
|
+
stages: list[int]
|
|
42
|
+
merge_commits: list[str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MergeBranchConflict(IntegrateError):
|
|
46
|
+
"""선행 stage 들끼리 내용이 충돌해 공통 바닥을 만들 수 없다."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, stage: int, branch: str, paths: list[str]):
|
|
49
|
+
self.stage = stage
|
|
50
|
+
self.branch = branch
|
|
51
|
+
self.paths = paths
|
|
52
|
+
super().__init__(
|
|
53
|
+
f"merge conflict while merging stage {stage} ({branch}): "
|
|
54
|
+
f"{', '.join(paths)}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ensure_stage_merge_branch(
|
|
58
|
+
*, project_root, project_id: str, task_group: str, task_id: str,
|
|
59
|
+
work_category: str, heads: dict[int, str], anchor_base: str,
|
|
60
|
+
advance_existing: bool = False,
|
|
61
|
+
) -> StageMergeBranch:
|
|
62
|
+
"""`heads` 의 done commit 을 모두 담은 브랜치를 보장한다. 멱등.
|
|
63
|
+
|
|
64
|
+
이름은 stage 집합이 정한다(`<namespace>/<task>-g<n1>-<n2>`) — 같은 집합은
|
|
65
|
+
언제나 같은 브랜치다. 브랜치는 anchor 에서 떼어 각 done commit 을 `--no-ff`
|
|
66
|
+
로 머지해 만든다. 머지 대상이 브랜치 tip 이 아니라 기록된 done commit 인 이유는
|
|
67
|
+
`_merge_stage` 와 같다: reconcile 로 둘이 갈라져 있을 수 있다.
|
|
68
|
+
|
|
69
|
+
워크트리는 머지하는 동안만 존재하고 끝나면 지운다 — 이 브랜치는 누가 체크아웃해
|
|
70
|
+
작업하는 곳이 아니라 다른 브랜치가 올라앉는 바닥이다.
|
|
71
|
+
|
|
72
|
+
이미 있는 브랜치에 빠진 commit 이 있을 때 `advance_existing` 이 갈린다.
|
|
73
|
+
거짓(기본)이면 거부한다 — 열려 있는 PR 의 base 를 말없이 옮기지 않기 위해서다.
|
|
74
|
+
참이면 빠진 commit 만 추가로 머지한다 — 구현 워크트리의 base 는 언제나 현재
|
|
75
|
+
선행 위에 있어야 하기 때문이다.
|
|
76
|
+
|
|
77
|
+
**호출자가 task-key `worktree_provision_mutex` 를 쥐고 들어와야 한다.** flock 은
|
|
78
|
+
재진입이 안 되므로 여기서 다시 잡으면 이미 락을 쥔 implementation 경로가
|
|
79
|
+
교착한다 — `_resolve_and_integrate_whole_task_unlocked` 와 같은 분리다.
|
|
80
|
+
"""
|
|
81
|
+
from .worktree import (compute_branch_name, compute_worktree_path,
|
|
82
|
+
is_ancestor, remove_worktree_force, stage_group_id)
|
|
83
|
+
|
|
84
|
+
if not heads:
|
|
85
|
+
raise IntegrateError("ensure_stage_merge_branch requires at least one stage")
|
|
86
|
+
stages = sorted(heads)
|
|
87
|
+
branch = compute_branch_name(work_category=work_category,
|
|
88
|
+
task_id_segment=task_id,
|
|
89
|
+
group_id=stage_group_id(stages))
|
|
90
|
+
root = Path(project_root)
|
|
91
|
+
if _branch_exists(root, branch):
|
|
92
|
+
tip = _rev_parse(root, branch)
|
|
93
|
+
missing = [n for n in stages if not is_ancestor(root, heads[n], tip)]
|
|
94
|
+
if not missing:
|
|
95
|
+
return StageMergeBranch(branch=branch, commit=tip, created=False,
|
|
96
|
+
stages=stages, merge_commits=[])
|
|
97
|
+
if not advance_existing:
|
|
98
|
+
raise IntegrateError(
|
|
99
|
+
f"merge-base branch {branch} exists but stage(s) {missing} "
|
|
100
|
+
"are not merged into it — delete the branch and retry so "
|
|
101
|
+
"okstra can rebuild it")
|
|
102
|
+
# 기존 브랜치를 그대로 체크아웃해 빠진 commit 만 얹는다.
|
|
103
|
+
creating, start = False, branch
|
|
104
|
+
to_merge = {n: heads[n] for n in missing}
|
|
105
|
+
else:
|
|
106
|
+
if not anchor_base:
|
|
107
|
+
raise IntegrateError(
|
|
108
|
+
f"cannot create {branch}: the implementation base commit is not "
|
|
109
|
+
"recorded in the worktree registry — run at least one "
|
|
110
|
+
"implementation stage first")
|
|
111
|
+
creating, start = True, anchor_base
|
|
112
|
+
to_merge = dict(heads)
|
|
113
|
+
|
|
114
|
+
wt_path = compute_worktree_path(
|
|
115
|
+
project_id=project_id, task_group_segment=task_group,
|
|
116
|
+
task_id_segment=task_id, group_id=stage_group_id(stages))
|
|
117
|
+
wt_path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
add = _git(root, "worktree", "add",
|
|
119
|
+
*(["-b", branch] if creating else []), str(wt_path), start)
|
|
120
|
+
if add.returncode != 0:
|
|
121
|
+
raise IntegrateError(
|
|
122
|
+
f"git worktree add for {branch} failed: "
|
|
123
|
+
f"{(add.stderr or add.stdout).strip()}")
|
|
124
|
+
try:
|
|
125
|
+
merge_commits = _merge_heads_into(wt_path, to_merge, work_category,
|
|
126
|
+
task_id)
|
|
127
|
+
commit = _rev_parse(wt_path, "HEAD")
|
|
128
|
+
except Exception:
|
|
129
|
+
remove_worktree_force(root, wt_path)
|
|
130
|
+
if creating:
|
|
131
|
+
_git(root, "branch", "-D", branch)
|
|
132
|
+
raise
|
|
133
|
+
remove_worktree_force(root, wt_path)
|
|
134
|
+
return StageMergeBranch(branch=branch, commit=commit, created=creating,
|
|
135
|
+
stages=stages, merge_commits=merge_commits)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _merge_heads_into(wt_path, heads: dict[int, str], work_category: str,
|
|
139
|
+
task_id: str) -> list[str]:
|
|
140
|
+
"""stage 번호 순서로 done commit 을 `--no-ff` 머지한다. 이미 담긴 commit 은
|
|
141
|
+
git 이 `Already up to date` 로 지나가므로 빈 머지 커밋이 생기지 않는다."""
|
|
142
|
+
from .worktree import MergeError, compute_branch_name, merge_branch
|
|
143
|
+
|
|
144
|
+
merge_commits: list[str] = []
|
|
145
|
+
for n in sorted(heads):
|
|
146
|
+
stage_branch = compute_branch_name(work_category=work_category,
|
|
147
|
+
task_id_segment=task_id,
|
|
148
|
+
stage_number=n)
|
|
149
|
+
try:
|
|
150
|
+
conflicts = merge_branch(wt_path, heads[n], no_ff=True)
|
|
151
|
+
except MergeError as exc:
|
|
152
|
+
raise IntegrateError(
|
|
153
|
+
f"stage {n} ({stage_branch}) 머지 실패(내용 충돌 아님): {exc}"
|
|
154
|
+
) from exc
|
|
155
|
+
if conflicts is not None:
|
|
156
|
+
raise MergeBranchConflict(stage=n, branch=stage_branch,
|
|
157
|
+
paths=conflicts)
|
|
158
|
+
merge_commits.append(_rev_parse(wt_path, "HEAD"))
|
|
159
|
+
return merge_commits
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _branch_exists(project_root, branch: str) -> bool:
|
|
163
|
+
return _git(project_root, "rev-parse", "--verify", "--quiet",
|
|
164
|
+
f"refs/heads/{branch}").returncode == 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _rev_parse(cwd, ref: str) -> str:
|
|
168
|
+
return _git(cwd, "rev-parse", ref).stdout.strip()
|
|
169
|
+
|
|
170
|
+
|
|
31
171
|
def _merge_stage(task_wt: str, stage_n: int, branch: str, done_commit: str) -> str:
|
|
32
172
|
"""한 stage 의 done commit 을 task 브랜치에 머지. 'merged'|'already_merged'.
|
|
33
173
|
충돌 시 abort 후 IntegrateError.
|
|
@@ -62,12 +202,20 @@ def integrate_stages(
|
|
|
62
202
|
task_worktree_path: str, stage_map: list[dict[str, Any]],
|
|
63
203
|
done_rows: list[dict[str, Any]],
|
|
64
204
|
teardown: bool = True,
|
|
205
|
+
merge: bool = True,
|
|
206
|
+
verified_head: str = "",
|
|
65
207
|
) -> IntegrateResult:
|
|
66
208
|
"""whole-task 통합: Phase A 머지 → Phase B teardown.
|
|
67
209
|
|
|
68
210
|
teardown=False 면 Phase B(stage worktree 디렉터리·registry 키 정리)를 건너뛴다 —
|
|
69
|
-
container 는 머지만 하고 stage 트리를 보존한다(스펙 §10).
|
|
70
|
-
|
|
211
|
+
container 는 머지만 하고 stage 트리를 보존한다(스펙 §10).
|
|
212
|
+
merge=False 면 Phase A 를 건너뛴다 — 한 stage 브랜치가 이미 전체를 담고 있어
|
|
213
|
+
검증이 거기서 돌았다면 task 브랜치로의 머지는 아무도 읽지 않는 산출물이고,
|
|
214
|
+
정리 시점에 충돌만 만들 수 있다(`stage_targets.containing_stage`). 정리 대상은
|
|
215
|
+
그때도 완료된 stage 전부다 — stage 브랜치는 어느 경우에도 남는다.
|
|
216
|
+
verified_head 는 "검증이 실제로 본 커밋"이다. 비면 task worktree HEAD 를 쓴다
|
|
217
|
+
(Phase A 가 만든 통합 commit). merge=False 로 들어올 때는 검증이 돈 stage
|
|
218
|
+
브랜치의 head 를 넘겨야 브랜치 tip 경고가 옳은 기준으로 판정된다."""
|
|
71
219
|
from .consumers import latest_done_by_stage
|
|
72
220
|
|
|
73
221
|
res = IntegrateResult()
|
|
@@ -75,6 +223,12 @@ def integrate_stages(
|
|
|
75
223
|
return res # non-git / degraded — no-op
|
|
76
224
|
done = latest_done_by_stage(done_rows)
|
|
77
225
|
ordered = sorted((s["stage_number"] for s in stage_map))
|
|
226
|
+
if not merge:
|
|
227
|
+
res.already_merged.extend(n for n in ordered if done.get(n))
|
|
228
|
+
if teardown:
|
|
229
|
+
_teardown_all(project_id, task_group, task_id,
|
|
230
|
+
task_worktree_path, res, verified_head)
|
|
231
|
+
return res
|
|
78
232
|
|
|
79
233
|
for n in ordered:
|
|
80
234
|
row = done.get(n)
|
|
@@ -98,7 +252,8 @@ def integrate_stages(
|
|
|
98
252
|
(res.merged if outcome == "merged" else res.already_merged).append(n)
|
|
99
253
|
|
|
100
254
|
if teardown:
|
|
101
|
-
_teardown_all(project_id, task_group, task_id, task_worktree_path, res
|
|
255
|
+
_teardown_all(project_id, task_group, task_id, task_worktree_path, res,
|
|
256
|
+
verified_head)
|
|
102
257
|
return res
|
|
103
258
|
|
|
104
259
|
|
|
@@ -111,13 +266,13 @@ def _stage_branch_name(project_id, task_group, task_id, stage_n) -> str | None:
|
|
|
111
266
|
|
|
112
267
|
|
|
113
268
|
def _teardown_all(project_id, task_group, task_id, task_wt: str,
|
|
114
|
-
res: IntegrateResult) -> None:
|
|
269
|
+
res: IntegrateResult, verified_head: str = "") -> None:
|
|
115
270
|
from . import worktree_registry
|
|
116
271
|
|
|
117
272
|
for n in [*res.merged, *res.already_merged]:
|
|
118
273
|
row = worktree_registry.get_stage_row(project_id, task_group, task_id, n)
|
|
119
274
|
outcome, detail = _teardown_stage(project_id, task_group, task_id,
|
|
120
|
-
task_wt, n, row)
|
|
275
|
+
task_wt, n, row, verified_head)
|
|
121
276
|
if outcome == "torn_down":
|
|
122
277
|
res.torn_down.append(n)
|
|
123
278
|
elif outcome == "skipped":
|
|
@@ -138,9 +293,12 @@ def _remove_clean_worktree(task_wt: str, wt: str, stage_n: int) -> tuple[str, st
|
|
|
138
293
|
return None
|
|
139
294
|
|
|
140
295
|
|
|
141
|
-
def _unmerged_branch_tip_warning(task_wt: str, stage_n: int, branch: str
|
|
142
|
-
|
|
143
|
-
|
|
296
|
+
def _unmerged_branch_tip_warning(task_wt: str, stage_n: int, branch: str,
|
|
297
|
+
verified_head: str = "") -> str:
|
|
298
|
+
"""Warn text when the stage branch tip is not an ancestor of the commit
|
|
299
|
+
verification actually inspected, or "" when it is (or cannot be resolved).
|
|
300
|
+
`verified_head` defaults to the task worktree HEAD — the integration commit
|
|
301
|
+
Phase A builds. A tip-branch verification passes its own head instead.
|
|
144
302
|
|
|
145
303
|
Phase A merges `done.head_commit`, never the branch ref, so a stage branch
|
|
146
304
|
can carry commits that whole-task verification never saw. `local-checkout
|
|
@@ -152,14 +310,15 @@ def _unmerged_branch_tip_warning(task_wt: str, stage_n: int, branch: str) -> str
|
|
|
152
310
|
if tip.returncode != 0:
|
|
153
311
|
return ""
|
|
154
312
|
sha = tip.stdout.strip()
|
|
155
|
-
head = _git(task_wt, "rev-parse", "HEAD").stdout.strip()
|
|
313
|
+
head = verified_head or _git(task_wt, "rev-parse", "HEAD").stdout.strip()
|
|
156
314
|
if is_ancestor(task_wt, sha, head):
|
|
157
315
|
return ""
|
|
158
|
-
return (f"stage {stage_n} 브랜치 {branch} 의 tip {sha[:12]} 이 검증된
|
|
159
|
-
f"
|
|
316
|
+
return (f"stage {stage_n} 브랜치 {branch} 의 tip {sha[:12]} 이 검증된 "
|
|
317
|
+
f"{head[:12]} 에 포함되지 않습니다 — 검증에 포함되지 않은 커밋이 있습니다")
|
|
160
318
|
|
|
161
319
|
|
|
162
|
-
def _teardown_stage(project_id, task_group, task_id, task_wt, stage_n, row
|
|
320
|
+
def _teardown_stage(project_id, task_group, task_id, task_wt, stage_n, row,
|
|
321
|
+
verified_head: str = ""):
|
|
163
322
|
"""Remove the clean stage worktree directory and mark the registry row
|
|
164
323
|
released. The stage branch is deliberately kept: the per-stage stack
|
|
165
324
|
branches must survive whole-task verification so that
|
|
@@ -181,7 +340,8 @@ def _teardown_stage(project_id, task_group, task_id, task_wt, stage_n, row):
|
|
|
181
340
|
return outcome
|
|
182
341
|
worktree_registry.release_status(project_id, task_group, task_id, stage_number=stage_n)
|
|
183
342
|
if branch:
|
|
184
|
-
warning = _unmerged_branch_tip_warning(task_wt, stage_n, branch
|
|
343
|
+
warning = _unmerged_branch_tip_warning(task_wt, stage_n, branch,
|
|
344
|
+
verified_head)
|
|
185
345
|
if warning:
|
|
186
346
|
return ("warn", warning)
|
|
187
347
|
return ("torn_down", "")
|
|
@@ -223,6 +223,21 @@ def _parse_stage_map_row(
|
|
|
223
223
|
)
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
def stage_number_gap_message(stage_number: int, row_number: int) -> str:
|
|
227
|
+
"""행 위치와 stage 번호가 어긋났을 때의 사유. 두 검증기가 같은 문장을 쓴다.
|
|
228
|
+
|
|
229
|
+
"1..N monotonic" 만 적던 동안 이어지는 계획의 작성자는 이 거절을 "검증기가
|
|
230
|
+
첫 계획만 상정한다" 로 읽었고, 우회한 계획은 `okstra prepare` 에서 다시
|
|
231
|
+
막혔다(2026-09-24, dev-10860). 무엇을 고쳐야 하는지 문장이 말한다.
|
|
232
|
+
"""
|
|
233
|
+
return (
|
|
234
|
+
f"stage numbers must be 1..N with no gap, got {stage_number} at row "
|
|
235
|
+
f"{row_number}: a plan that continues an earlier one re-declares every "
|
|
236
|
+
"earlier stage — copy each `done` stage row unchanged — instead of "
|
|
237
|
+
"starting above them"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
226
241
|
def _validate_stage_numbers(
|
|
227
242
|
stages: list[StageMapStage], source_plan_path: str,
|
|
228
243
|
) -> None:
|
|
@@ -238,8 +253,7 @@ def _validate_stage_numbers(
|
|
|
238
253
|
if stage.stage_number != row_number:
|
|
239
254
|
raise StageMapError(
|
|
240
255
|
"stage_map",
|
|
241
|
-
|
|
242
|
-
f"got {stage.stage_number} at row {row_number}",
|
|
256
|
+
stage_number_gap_message(stage.stage_number, row_number),
|
|
243
257
|
source_plan_path,
|
|
244
258
|
)
|
|
245
259
|
|
|
@@ -427,39 +427,114 @@ def commit_is_ancestor(project_root: Path, ancestor: str, descendant: str) -> bo
|
|
|
427
427
|
return is_ancestor(project_root, ancestor, descendant)
|
|
428
428
|
|
|
429
429
|
|
|
430
|
-
def
|
|
430
|
+
def reconcile_pred_commits(
|
|
431
431
|
project_root: Path,
|
|
432
432
|
plan_run_root: Path | None,
|
|
433
433
|
latest: dict[int, dict[str, Any]],
|
|
434
434
|
pred_commits: dict[int, str],
|
|
435
435
|
candidate_base: str,
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
436
|
+
) -> dict[int, str]:
|
|
437
|
+
"""선행의 기록 commit 을 실제로 머지할 commit 으로 옮긴다.
|
|
438
|
+
|
|
439
|
+
기록된 done commit 이 task 브랜치에서 rebase·squash 로 다시 쓰였다면, 그
|
|
440
|
+
commit 을 그대로 머지하는 것은 이미 다른 모양으로 들어간 내용을 한 번 더
|
|
441
|
+
얹는 일이다. patch-id 로 동치 commit 을 찾으면 그쪽으로 옮기고 원장에도
|
|
442
|
+
보정 row 를 남긴다(`git-reconcile` 과 같은 기준).
|
|
443
|
+
|
|
444
|
+
찾지 못하는 것은 오류가 아니다 — 아직 어디에도 머지되지 않은 선행이 오히려
|
|
445
|
+
기본이고, 그런 선행은 기록된 commit 그대로 공통 바닥에 들어간다. 종전에는
|
|
446
|
+
이 자리가 게이트였고, task 브랜치에 선행을 손으로 머지하기 전에는 다중 의존
|
|
447
|
+
stage 가 시작할 수 없었다."""
|
|
439
448
|
from .git_reconcile import content_merged, _record_reconciled
|
|
440
449
|
|
|
450
|
+
if not candidate_base:
|
|
451
|
+
return dict(pred_commits)
|
|
452
|
+
resolved: dict[int, str] = {}
|
|
441
453
|
for dep_stage, head in pred_commits.items():
|
|
454
|
+
resolved[dep_stage] = head
|
|
442
455
|
if commit_is_ancestor(project_root, head, candidate_base):
|
|
443
456
|
continue
|
|
444
457
|
match = content_merged(project_root, head, candidate_base)
|
|
445
|
-
if match.status
|
|
446
|
-
if plan_run_root is not None:
|
|
447
|
-
_record_reconciled(
|
|
448
|
-
plan_run_root,
|
|
449
|
-
impl_task_key=(latest.get(dep_stage) or {}).get("impl_task_key", ""),
|
|
450
|
-
stage=dep_stage,
|
|
451
|
-
new_commit=match.matched_commit,
|
|
452
|
-
replaced=head,
|
|
453
|
-
reason="auto-patch-id",
|
|
454
|
-
)
|
|
458
|
+
if match.status != "patch-equivalent" or not match.matched_commit:
|
|
455
459
|
continue
|
|
460
|
+
resolved[dep_stage] = match.matched_commit
|
|
461
|
+
if plan_run_root is not None:
|
|
462
|
+
_record_reconciled(
|
|
463
|
+
plan_run_root,
|
|
464
|
+
impl_task_key=(latest.get(dep_stage) or {}).get("impl_task_key", ""),
|
|
465
|
+
stage=dep_stage,
|
|
466
|
+
new_commit=match.matched_commit,
|
|
467
|
+
replaced=head,
|
|
468
|
+
reason="auto-patch-id",
|
|
469
|
+
)
|
|
470
|
+
return resolved
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@dataclass(frozen=True)
|
|
474
|
+
class StageWorktreeIdentity:
|
|
475
|
+
"""선행 머지 브랜치를 만들 때 필요한 이름 조각. 읽기 전용 호출자는 넘기지
|
|
476
|
+
않으며, 그때 다중 의존 base 는 만들어야 하는 순간에 거부된다."""
|
|
477
|
+
|
|
478
|
+
project_id: str
|
|
479
|
+
task_group: str
|
|
480
|
+
task_id: str
|
|
481
|
+
work_category: str
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _multi_dep_base(
|
|
485
|
+
stage_number: int,
|
|
486
|
+
deps: list[int],
|
|
487
|
+
latest: dict[int, dict[str, Any]],
|
|
488
|
+
anchor_base_commit: str,
|
|
489
|
+
candidate_base: str,
|
|
490
|
+
project_root: Path | None,
|
|
491
|
+
plan_run_root: Path | None,
|
|
492
|
+
identity: "StageWorktreeIdentity | None",
|
|
493
|
+
) -> str:
|
|
494
|
+
"""선행이 둘 이상인 stage 의 base — 그 선행들만 담은 공통 바닥.
|
|
495
|
+
|
|
496
|
+
종전에는 task 워크트리 HEAD 를 base 로 쓰고, 선행이 거기 머지돼 있지 않으면
|
|
497
|
+
거부했다. 그 base 는 선행이 아닌 stage 의 작업까지 담고 있어서 stage 경계가
|
|
498
|
+
흐려졌고, 사람이 손으로 머지하기 전에는 stage 가 시작조차 못 했다. 이제
|
|
499
|
+
선행들의 done commit 만 담은 브랜치를 만들어 그 위에서 시작한다."""
|
|
500
|
+
from .stage_integrate import IntegrateError, ensure_stage_merge_branch
|
|
501
|
+
|
|
502
|
+
pred_commits: dict[int, str] = {}
|
|
503
|
+
for dep_stage in deps:
|
|
504
|
+
head = (latest.get(dep_stage) or {}).get("head_commit")
|
|
505
|
+
if not head:
|
|
506
|
+
raise StageTargetError(
|
|
507
|
+
f"predecessor stage {dep_stage} has no done row with head_commit "
|
|
508
|
+
f"in consumers.jsonl; multi-dependency stage {stage_number} cannot start"
|
|
509
|
+
)
|
|
510
|
+
pred_commits[dep_stage] = head
|
|
511
|
+
if project_root is None:
|
|
456
512
|
raise StageTargetError(
|
|
457
|
-
f"multi-dependency stage {
|
|
458
|
-
|
|
459
|
-
f"({candidate_base[:8]}). Merge stage branches "
|
|
460
|
-
f"(e.g. the `-s{dep_stage}` branches) into the task worktree "
|
|
461
|
-
"(or into main, then refresh the worktree) and retry."
|
|
513
|
+
f"multi-dependency stage {stage_number} needs a git worktree to "
|
|
514
|
+
"resolve its base; the task-key worktree could not be resolved"
|
|
462
515
|
)
|
|
516
|
+
heads = reconcile_pred_commits(
|
|
517
|
+
project_root, plan_run_root, latest, pred_commits, candidate_base)
|
|
518
|
+
contained = single_containing_head(project_root, heads)
|
|
519
|
+
if contained is not None:
|
|
520
|
+
# 한 선행이 나머지를 이미 담고 있으면 그 commit 이 곧 공통 바닥이다.
|
|
521
|
+
return heads[contained]
|
|
522
|
+
if identity is None:
|
|
523
|
+
raise StageTargetError(
|
|
524
|
+
f"multi-dependency stage {stage_number} has no single predecessor "
|
|
525
|
+
f"commit containing the others ({sorted(heads)}), so its base is an "
|
|
526
|
+
"integration branch that only an implementation run may create"
|
|
527
|
+
)
|
|
528
|
+
try:
|
|
529
|
+
merged = ensure_stage_merge_branch(
|
|
530
|
+
project_root=project_root, project_id=identity.project_id,
|
|
531
|
+
task_group=identity.task_group, task_id=identity.task_id,
|
|
532
|
+
work_category=identity.work_category, heads=heads,
|
|
533
|
+
anchor_base=anchor_base_commit, advance_existing=True)
|
|
534
|
+
except IntegrateError as exc:
|
|
535
|
+
raise StageTargetError(
|
|
536
|
+
f"multi-dependency stage {stage_number}: {exc}") from exc
|
|
537
|
+
return merged.commit
|
|
463
538
|
|
|
464
539
|
|
|
465
540
|
def resolve_stage_base_commit(
|
|
@@ -469,6 +544,7 @@ def resolve_stage_base_commit(
|
|
|
469
544
|
candidate_base: str = "",
|
|
470
545
|
project_root: Path | None = None,
|
|
471
546
|
plan_run_root: Path | None = None,
|
|
547
|
+
identity: "StageWorktreeIdentity | None" = None,
|
|
472
548
|
) -> str:
|
|
473
549
|
"""Pick the git base commit a stage's isolated worktree branches from."""
|
|
474
550
|
from .consumers import latest_done_by_stage
|
|
@@ -476,30 +552,9 @@ def resolve_stage_base_commit(
|
|
|
476
552
|
latest = latest_done_by_stage(consumer_done_rows)
|
|
477
553
|
deps = stage.get("depends_on") or []
|
|
478
554
|
if len(deps) >= 2:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
head = (latest.get(dep_stage) or {}).get("head_commit")
|
|
483
|
-
if not head:
|
|
484
|
-
raise StageTargetError(
|
|
485
|
-
f"predecessor stage {dep_stage} has no done row with head_commit "
|
|
486
|
-
f"in consumers.jsonl; multi-dependency stage {stage_number} cannot start"
|
|
487
|
-
)
|
|
488
|
-
pred_commits[dep_stage] = head
|
|
489
|
-
if not candidate_base or project_root is None:
|
|
490
|
-
raise StageTargetError(
|
|
491
|
-
f"candidate base missing for multi-dependency stage {stage_number}; "
|
|
492
|
-
"task-key worktree HEAD could not be resolved"
|
|
493
|
-
)
|
|
494
|
-
check_multi_dep_merged(
|
|
495
|
-
project_root,
|
|
496
|
-
plan_run_root,
|
|
497
|
-
latest,
|
|
498
|
-
pred_commits,
|
|
499
|
-
candidate_base,
|
|
500
|
-
stage_number,
|
|
501
|
-
)
|
|
502
|
-
return candidate_base
|
|
555
|
+
return _multi_dep_base(
|
|
556
|
+
stage["stage_number"], list(deps), latest, anchor_base_commit,
|
|
557
|
+
candidate_base, project_root, plan_run_root, identity)
|
|
503
558
|
if not deps:
|
|
504
559
|
if not anchor_base_commit:
|
|
505
560
|
raise StageTargetError(
|
|
@@ -677,9 +732,22 @@ def _resolve_and_integrate_whole_task_unlocked(
|
|
|
677
732
|
done_rows: list[dict[str, Any]], anchor_base: str, teardown: bool,
|
|
678
733
|
) -> dict[str, Any]:
|
|
679
734
|
"""Integrate and resolve a whole-task target while the caller owns the lock."""
|
|
735
|
+
from .consumers import latest_done_by_stage
|
|
680
736
|
from .stage_integrate import IntegrateError, integrate_stages
|
|
681
737
|
from .worktree import _git, is_dirty_excluding_okstra
|
|
682
738
|
|
|
739
|
+
# 끝나지 않은 stage 가 있으면 _resolve_whole_task_target 이 어차피 거부한다.
|
|
740
|
+
# 그 전에 끝난 stage 를 머지해 두면 거부된 요청이 task worktree 에 머지를
|
|
741
|
+
# 남긴다(되돌리지 않는다). 머지하기 전에 같은 조건으로 거부한다.
|
|
742
|
+
done_by_stage = latest_done_by_stage(done_rows)
|
|
743
|
+
for stage in stage_map:
|
|
744
|
+
n = stage["stage_number"]
|
|
745
|
+
if n not in done_by_stage:
|
|
746
|
+
raise PrepareError(
|
|
747
|
+
f"final-verification(whole-task): stage {n} not done — "
|
|
748
|
+
f"run implementation --stage {n} first"
|
|
749
|
+
)
|
|
750
|
+
|
|
683
751
|
try:
|
|
684
752
|
integration = integrate_stages(
|
|
685
753
|
project_id=project_id,
|
|
@@ -852,6 +920,93 @@ def _acquire_single_stage_target(
|
|
|
852
920
|
)
|
|
853
921
|
|
|
854
922
|
|
|
923
|
+
def single_containing_head(project_root: Path,
|
|
924
|
+
heads: dict[int, str]) -> int | None:
|
|
925
|
+
"""주어진 stage 들의 commit 을 혼자서 모두 담고 있는 stage. 없으면 None.
|
|
926
|
+
|
|
927
|
+
stage 브랜치는 스택이라 한 stage 가 다른 stage 들을 이미 담고 있는 경우가
|
|
928
|
+
흔하다. 그럴 땐 새 브랜치를 만들 이유가 없다 — 그 commit 이 곧 공통 바닥이다.
|
|
929
|
+
판정은 계획의 의존 그래프가 아니라 git 조상 관계로 한다: 계획상 tip 이어도 그
|
|
930
|
+
브랜치가 실제로 다른 stage 의 커밋을 담고 있는지가 문제이기 때문이다."""
|
|
931
|
+
from .worktree import is_ancestor
|
|
932
|
+
|
|
933
|
+
if not heads:
|
|
934
|
+
return None
|
|
935
|
+
for candidate, head in heads.items():
|
|
936
|
+
if all(is_ancestor(project_root, other, head)
|
|
937
|
+
for n, other in heads.items() if n != candidate):
|
|
938
|
+
return candidate
|
|
939
|
+
return None
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def containing_stage(project_root: Path,
|
|
943
|
+
done_by_stage: dict[int, dict[str, Any]]) -> int | None:
|
|
944
|
+
"""완료된 stage 전부를 혼자 담고 있는 stage. 하나라도 commit 이 비면 None.
|
|
945
|
+
|
|
946
|
+
whole-task 검증이 "이 브랜치가 곧 전체인가" 를 묻는 자리다. 병렬 stage 가 있어
|
|
947
|
+
tip 이 여럿이면 어느 브랜치도 전체가 아니므로 None 이다."""
|
|
948
|
+
heads = {n: row.get("head_commit", "")
|
|
949
|
+
for n, row in done_by_stage.items() if row.get("head_commit")}
|
|
950
|
+
if len(heads) != len(done_by_stage):
|
|
951
|
+
return None
|
|
952
|
+
return single_containing_head(project_root, heads)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _acquire_tip_stage_target(
|
|
956
|
+
request: FinalVerificationTargetRequest,
|
|
957
|
+
done_rows: list[dict[str, Any]],
|
|
958
|
+
registry_coordinates: tuple[str, str, str],
|
|
959
|
+
) -> FinalVerificationTargetAcquisition | None:
|
|
960
|
+
"""전체를 담은 단일 stage 브랜치가 있으면 그것을 whole-task 검증 대상으로
|
|
961
|
+
삼는다 — 머지 없이. 없거나 그 워크트리가 쓸 수 없으면 None 을 돌려 기존
|
|
962
|
+
task 브랜치 통합 경로로 넘긴다."""
|
|
963
|
+
from . import worktree_registry
|
|
964
|
+
from .consumers import latest_done_by_stage
|
|
965
|
+
from .worktree import _git, is_dirty_excluding_okstra
|
|
966
|
+
|
|
967
|
+
done = latest_done_by_stage(done_rows)
|
|
968
|
+
planned = [int(s["stage_number"]) for s in request.stage_map]
|
|
969
|
+
if any(n not in done for n in planned):
|
|
970
|
+
return None # 미완 stage 의 거부 메시지는 기존 경로가 낸다
|
|
971
|
+
tip = containing_stage(request.project_root,
|
|
972
|
+
{n: done[n] for n in planned})
|
|
973
|
+
if tip is None:
|
|
974
|
+
return None
|
|
975
|
+
row = worktree_registry.get_stage_row(*registry_coordinates, tip) or {}
|
|
976
|
+
worktree_path = row.get("worktree_path", "")
|
|
977
|
+
if not worktree_path or not Path(worktree_path).is_dir():
|
|
978
|
+
return None
|
|
979
|
+
head_result = _git(worktree_path, "rev-parse", "HEAD")
|
|
980
|
+
if head_result.returncode != 0:
|
|
981
|
+
return None
|
|
982
|
+
head = head_result.stdout.strip()
|
|
983
|
+
if head != done[tip].get("head_commit", ""):
|
|
984
|
+
# 브랜치 tip 이 done 기록과 갈라져 있다. 검증 대상이 무엇인지 모호하므로
|
|
985
|
+
# 판정을 기존 경로로 넘긴다.
|
|
986
|
+
return None
|
|
987
|
+
if is_dirty_excluding_okstra(worktree_path):
|
|
988
|
+
raise StageTargetError(
|
|
989
|
+
"final-verification: worktree has uncommitted source changes "
|
|
990
|
+
"(outside .okstra/) — commit or stash before verifying"
|
|
991
|
+
)
|
|
992
|
+
anchor_base = worktree_registry.get_implementation_base(
|
|
993
|
+
*registry_coordinates) or ""
|
|
994
|
+
stages = sorted(planned)
|
|
995
|
+
return FinalVerificationTargetAcquisition(
|
|
996
|
+
target=FinalVerificationTarget(
|
|
997
|
+
scope="whole-task",
|
|
998
|
+
base=anchor_base,
|
|
999
|
+
head=head,
|
|
1000
|
+
worktree_path=worktree_path,
|
|
1001
|
+
stages=stages,
|
|
1002
|
+
reports=[done[n].get("report_path", "") for n in stages],
|
|
1003
|
+
),
|
|
1004
|
+
relocations=(),
|
|
1005
|
+
worktree_branch=row.get("branch", ""),
|
|
1006
|
+
integration_result=None,
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
|
|
855
1010
|
def _acquire_whole_task_target(
|
|
856
1011
|
request: FinalVerificationTargetRequest,
|
|
857
1012
|
done_rows: list[dict[str, Any]],
|
|
@@ -859,6 +1014,10 @@ def _acquire_whole_task_target(
|
|
|
859
1014
|
) -> FinalVerificationTargetAcquisition:
|
|
860
1015
|
from . import worktree_registry
|
|
861
1016
|
|
|
1017
|
+
tip = _acquire_tip_stage_target(request, done_rows, registry_coordinates)
|
|
1018
|
+
if tip is not None:
|
|
1019
|
+
return tip
|
|
1020
|
+
|
|
862
1021
|
project_id, task_group, task_id = registry_coordinates
|
|
863
1022
|
entry = worktree_registry.lookup(project_id, task_group, task_id)
|
|
864
1023
|
worktree_path = (
|
|
@@ -946,6 +1105,8 @@ def integrate_and_teardown_whole_task(
|
|
|
946
1105
|
if entry is None:
|
|
947
1106
|
return {"skipped": "task worktree is no longer registered"}
|
|
948
1107
|
|
|
1108
|
+
done_by_stage = consumers.latest_done_by_stage(done_rows)
|
|
1109
|
+
tip = containing_stage(project_root, done_by_stage)
|
|
949
1110
|
with worktree_provision_mutex(okstra_home(), project_id, task_group, task_id):
|
|
950
1111
|
result = integrate_stages(
|
|
951
1112
|
project_id=project_id,
|
|
@@ -955,6 +1116,11 @@ def integrate_and_teardown_whole_task(
|
|
|
955
1116
|
stage_map=stage_map,
|
|
956
1117
|
done_rows=done_rows,
|
|
957
1118
|
teardown=True,
|
|
1119
|
+
# 한 stage 브랜치가 이미 전체를 담고 있으면 검증은 거기서 돌았다
|
|
1120
|
+
# (`_acquire_tip_stage_target`). 그 경우 task 브랜치로의 머지는 아무도
|
|
1121
|
+
# 읽지 않는 산출물이므로 정리만 한다.
|
|
1122
|
+
merge=tip is None,
|
|
1123
|
+
verified_head=(done_by_stage.get(tip) or {}).get("head_commit", ""),
|
|
958
1124
|
)
|
|
959
1125
|
return {
|
|
960
1126
|
"tornDown": result.torn_down,
|
|
@@ -36,7 +36,7 @@ from .dispatch_core import (
|
|
|
36
36
|
await_dispatches,
|
|
37
37
|
dispatch_plan,
|
|
38
38
|
)
|
|
39
|
-
from .ports.worker_dispatch import WorkerDispatchRequest
|
|
39
|
+
from .ports.worker_dispatch import WorkerDispatchPort, WorkerDispatchRequest
|
|
40
40
|
from .registry.host_registry import default_host_registry
|
|
41
41
|
from .registry.provider_registry import default_provider_registry
|
|
42
42
|
from .session import observe_lead_session
|
|
@@ -161,18 +161,7 @@ def _dispatch(args) -> int:
|
|
|
161
161
|
raise DispatchError("--jobs-file and --workers cannot be combined")
|
|
162
162
|
manifest = _load_manifest(args.project_root, args.run_manifest)
|
|
163
163
|
_observe_lead_session_from_manifest(Path(args.project_root).resolve(), manifest)
|
|
164
|
-
|
|
165
|
-
lead_runtime = _require_string(manifest, "leadRuntime")
|
|
166
|
-
fallback_port = default_host_registry().resolve(
|
|
167
|
-
lead_runtime
|
|
168
|
-
).worker_dispatch()
|
|
169
|
-
port = dispatch_port_for_terminal_backend(
|
|
170
|
-
terminal_backend,
|
|
171
|
-
fallback_port,
|
|
172
|
-
supported_worker_wrappers=_SUPPORTED_WRAPPERS,
|
|
173
|
-
unsupported_worker_label=f"{lead_runtime} lead",
|
|
174
|
-
required_lead_runtime=None if _is_cmux_run(manifest) else lead_runtime,
|
|
175
|
-
)
|
|
164
|
+
port = team_dispatch_port(manifest)
|
|
176
165
|
request = WorkerDispatchRequest(
|
|
177
166
|
project_root=Path(args.project_root),
|
|
178
167
|
run_manifest=Path(args.run_manifest),
|
|
@@ -195,6 +184,26 @@ def _dispatch(args) -> int:
|
|
|
195
184
|
return result
|
|
196
185
|
|
|
197
186
|
|
|
187
|
+
def team_dispatch_port(manifest: Mapping[str, Any]) -> WorkerDispatchPort:
|
|
188
|
+
"""이 run 의 워커를 띄울 포트. cmux run 이면 pane, 아니면 리드 호스트의 포트.
|
|
189
|
+
|
|
190
|
+
`team dispatch` 와 finalize 의 `translate` 단계가 같이 쓴다 — 번역자만
|
|
191
|
+
CLI 래퍼 디스패처로 띄우던 동안 cmux run 에서도 번역자는 pane 없이 돌았다
|
|
192
|
+
(실측 2026-09-22, dev-10860 implementation-planning-001: 18건 중 번역자 1건).
|
|
193
|
+
"""
|
|
194
|
+
lead_runtime = _require_string(manifest, "leadRuntime")
|
|
195
|
+
fallback_port = default_host_registry().resolve(
|
|
196
|
+
lead_runtime
|
|
197
|
+
).worker_dispatch()
|
|
198
|
+
return dispatch_port_for_terminal_backend(
|
|
199
|
+
_manifest_backend(manifest),
|
|
200
|
+
fallback_port,
|
|
201
|
+
supported_worker_wrappers=_SUPPORTED_WRAPPERS,
|
|
202
|
+
unsupported_worker_label=f"{lead_runtime} lead",
|
|
203
|
+
required_lead_runtime=None if _is_cmux_run(manifest) else lead_runtime,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
198
207
|
def _await(args) -> int:
|
|
199
208
|
manifest = _load_manifest(args.project_root, args.run_manifest)
|
|
200
209
|
_validate_team_manifest(manifest)
|