okstra 0.218.0 → 0.219.1
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 +5 -5
- package/dist/cli-registry.mjs +0 -10
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/config.mjs +2 -3
- package/dist/commands/lifecycle/config.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +6 -0
- package/dist/commands/pr/pr.mjs +58 -0
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/lib/runtime-payload.mjs +1 -2
- package/dist/lib/runtime-payload.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +9 -7
- package/docs/cli.md +3 -4
- package/docs/project-structure-overview.md +4 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -3
- package/runtime/prompts/lead/report-writer.md +10 -17
- package/runtime/prompts/lead/team-contract.md +1 -2
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +0 -1
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +0 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +2 -27
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +3 -349
- package/runtime/python/okstra_ctl/dispatch_state.py +0 -13
- package/runtime/python/okstra_ctl/domain/provider.py +1 -1
- package/runtime/python/okstra_ctl/domain/role.py +0 -6
- package/runtime/python/okstra_ctl/final_report_paths.py +0 -16
- package/runtime/python/okstra_ctl/i18n.py +2 -2
- package/runtime/python/okstra_ctl/path_hints.py +0 -4
- package/runtime/python/okstra_ctl/paths.py +0 -3
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -0
- package/runtime/python/okstra_ctl/phases/final_verification/spec.md +2 -0
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +17 -1
- package/runtime/python/okstra_ctl/phases/implementation/entry.py +12 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +4 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +24 -3
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +11 -11
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +4 -2
- package/runtime/python/okstra_ctl/render.py +0 -10
- package/runtime/python/okstra_ctl/render_final_report.py +4 -4
- package/runtime/python/okstra_ctl/report_finalize.py +37 -76
- package/runtime/python/okstra_ctl/report_html/render.py +4 -44
- package/runtime/python/okstra_ctl/report_language.py +3 -3
- package/runtime/python/okstra_ctl/report_prose_keys.py +153 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
- package/runtime/python/okstra_ctl/run.py +1 -21
- package/runtime/python/okstra_ctl/stage_integrate.py +37 -2
- package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
- package/runtime/python/okstra_ctl/stage_targets.py +29 -62
- package/runtime/python/okstra_ctl/team.py +1 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +0 -8
- package/runtime/python/okstra_ctl/wizard/registry.py +2 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +10 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -12
- package/runtime/python/okstra_ctl/worktree/__init__.py +1 -1
- package/runtime/python/okstra_ctl/worktree/decisions.py +3 -4
- package/runtime/python/okstra_ctl/worktree/provision.py +27 -0
- package/runtime/python/okstra_ctl/worktree/rebase.py +108 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +14 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +1 -1
- package/runtime/skills/okstra-code-review/SKILL.md +1 -1
- package/runtime/skills/okstra-discussion/SKILL.md +1 -1
- package/runtime/skills/okstra-pr-gen/SKILL.md +9 -0
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-setup/SKILL.md +2 -2
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/templates/pr/drafting-rules.md +4 -0
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/html/i18n/en.json +0 -1
- package/runtime/templates/reports/html/i18n/ko.json +0 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +1 -28
- package/runtime/agents/operations/report-translation.json +0 -6
- package/runtime/agents/roles/translator.json +0 -19
- package/runtime/bin/okstra-report-translate.py +0 -404
- package/runtime/prompts/duties/translator.json +0 -32
- package/runtime/python/okstra_ctl/report_translation.py +0 -509
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +0 -356
- package/runtime/templates/translator-prompt-preamble.md +0 -36
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Final-report leaf keys whose values a reader reads as prose and nothing else.
|
|
2
|
+
|
|
3
|
+
A value under one of these keys is displayed text; ids, enum values, paths and
|
|
4
|
+
anchor targets live under other keys.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
PROSE_KEYS = frozenset({
|
|
9
|
+
"acceptance",
|
|
10
|
+
"action",
|
|
11
|
+
"actions",
|
|
12
|
+
"addedWork",
|
|
13
|
+
"alternativesConsidered",
|
|
14
|
+
"answer",
|
|
15
|
+
"approach",
|
|
16
|
+
"approvalDisposition",
|
|
17
|
+
"approvalEvidence",
|
|
18
|
+
"architectureBoundaries",
|
|
19
|
+
"assumptions",
|
|
20
|
+
"behavior",
|
|
21
|
+
"blastRadius",
|
|
22
|
+
"blockReason",
|
|
23
|
+
"blockedReason",
|
|
24
|
+
"blockers",
|
|
25
|
+
"body",
|
|
26
|
+
"boundary",
|
|
27
|
+
"briefEvidence",
|
|
28
|
+
"canonical",
|
|
29
|
+
"carryIn",
|
|
30
|
+
"causeSummary",
|
|
31
|
+
"change",
|
|
32
|
+
"check",
|
|
33
|
+
"claim",
|
|
34
|
+
"commitment",
|
|
35
|
+
"complexity",
|
|
36
|
+
"condition",
|
|
37
|
+
"confirmingSignal",
|
|
38
|
+
"conformanceExemption",
|
|
39
|
+
"consequences",
|
|
40
|
+
"constraint",
|
|
41
|
+
"context",
|
|
42
|
+
"coreMechanism",
|
|
43
|
+
"coreReason",
|
|
44
|
+
"counterevidence",
|
|
45
|
+
"decision",
|
|
46
|
+
"decisions",
|
|
47
|
+
"declinedFixRecommendations",
|
|
48
|
+
"description",
|
|
49
|
+
"details",
|
|
50
|
+
"directionChange",
|
|
51
|
+
"disagreement",
|
|
52
|
+
"discrepancy",
|
|
53
|
+
"disproveWith",
|
|
54
|
+
"environmentSetup",
|
|
55
|
+
"escalationTrigger",
|
|
56
|
+
"evidence",
|
|
57
|
+
"evidenceRequired",
|
|
58
|
+
"exemptionReaffirm",
|
|
59
|
+
"exemptionReason",
|
|
60
|
+
"exitContract",
|
|
61
|
+
"exitContractSummary",
|
|
62
|
+
"expected",
|
|
63
|
+
"expectedBehaviorAfter",
|
|
64
|
+
"expectedChangeAreas",
|
|
65
|
+
"expectedForm",
|
|
66
|
+
"expectedOutcome",
|
|
67
|
+
"expectedResult",
|
|
68
|
+
"expectedVerification",
|
|
69
|
+
"explanation",
|
|
70
|
+
"fact",
|
|
71
|
+
"failureHandling",
|
|
72
|
+
"falsifyingEvidenceChecked",
|
|
73
|
+
"finalConclusion",
|
|
74
|
+
"goal",
|
|
75
|
+
"guardrails",
|
|
76
|
+
"headline",
|
|
77
|
+
"howToStart",
|
|
78
|
+
"hypothesis",
|
|
79
|
+
"ifStillOpen",
|
|
80
|
+
"injectedAs",
|
|
81
|
+
"interfaces",
|
|
82
|
+
"item",
|
|
83
|
+
"justification",
|
|
84
|
+
"knownFacts",
|
|
85
|
+
"label",
|
|
86
|
+
"mitigation",
|
|
87
|
+
"motivation",
|
|
88
|
+
"name",
|
|
89
|
+
"need",
|
|
90
|
+
"nextStep",
|
|
91
|
+
"noVariationRationale",
|
|
92
|
+
"notApplicableReason",
|
|
93
|
+
"note",
|
|
94
|
+
"observableFailure",
|
|
95
|
+
"observed",
|
|
96
|
+
"option",
|
|
97
|
+
"outcome",
|
|
98
|
+
"outputSummary",
|
|
99
|
+
"performed",
|
|
100
|
+
"planItem",
|
|
101
|
+
"position",
|
|
102
|
+
"problem",
|
|
103
|
+
"question",
|
|
104
|
+
"rationale",
|
|
105
|
+
"readyWhen",
|
|
106
|
+
"reason",
|
|
107
|
+
"rejectedSummary",
|
|
108
|
+
"rejectingSignal",
|
|
109
|
+
"relation",
|
|
110
|
+
"replacedInTest",
|
|
111
|
+
"replacedWith",
|
|
112
|
+
"requestKind",
|
|
113
|
+
"requiredDecision",
|
|
114
|
+
"requiredWork",
|
|
115
|
+
"requirement",
|
|
116
|
+
"resolution",
|
|
117
|
+
"responsibility",
|
|
118
|
+
"reversibility",
|
|
119
|
+
"risk",
|
|
120
|
+
"rolloutCost",
|
|
121
|
+
"routingRecommendation",
|
|
122
|
+
"satisfaction",
|
|
123
|
+
"scope",
|
|
124
|
+
"selfFixNote",
|
|
125
|
+
"sliceValue",
|
|
126
|
+
"stageTitle",
|
|
127
|
+
"stageValidation",
|
|
128
|
+
"statement",
|
|
129
|
+
"suggestedAction",
|
|
130
|
+
"summary",
|
|
131
|
+
"supersededStatement",
|
|
132
|
+
"symptom",
|
|
133
|
+
"systemInterpretation",
|
|
134
|
+
"testCaseBoundary",
|
|
135
|
+
"testCaseFailure",
|
|
136
|
+
"testCaseSuccess",
|
|
137
|
+
"testCoverageCost",
|
|
138
|
+
"text",
|
|
139
|
+
"title",
|
|
140
|
+
"topic",
|
|
141
|
+
"trigger",
|
|
142
|
+
"triggerSignal",
|
|
143
|
+
"unblockCondition",
|
|
144
|
+
"userChoice",
|
|
145
|
+
"userConstraints",
|
|
146
|
+
"userInput",
|
|
147
|
+
"userText",
|
|
148
|
+
"verification",
|
|
149
|
+
"verificationMethod",
|
|
150
|
+
"verificationSignal",
|
|
151
|
+
"whyItMatters",
|
|
152
|
+
"workingAssumption",
|
|
153
|
+
})
|
|
@@ -866,7 +866,7 @@ def _attempt_result_specs(
|
|
|
866
866
|
for row in invocations_value
|
|
867
867
|
if isinstance(row, Mapping) and _string(row.get("invocationRef"))
|
|
868
868
|
}
|
|
869
|
-
excluded_duties = {"lead", "report-writer"
|
|
869
|
+
excluded_duties = {"lead", "report-writer"}
|
|
870
870
|
specs: list[tuple[str, str, str, bool]] = []
|
|
871
871
|
seen: set[str] = set()
|
|
872
872
|
for attempt in attempts_value:
|
|
@@ -1896,19 +1896,12 @@ def _build_static_execution_manifest(
|
|
|
1896
1896
|
plan: AssignmentPlan,
|
|
1897
1897
|
context: AssignmentContext,
|
|
1898
1898
|
lead: ResolvedAssignment,
|
|
1899
|
-
translator: ResolvedAssignment,
|
|
1900
1899
|
) -> ExecutionManifest:
|
|
1901
1900
|
participants: list[ParticipantAssignment] = []
|
|
1902
1901
|
roles: list[RoleExecution] = []
|
|
1903
|
-
# The translator is resolved outside the role plan, like the lead, and both
|
|
1904
|
-
# are named in `invocationAssignments`. Without a role execution here,
|
|
1905
|
-
# `agent-prompt materialize --audience translator` has no canonical
|
|
1906
|
-
# identity to bind to and refuses — so a `reportLanguage: ko` run could
|
|
1907
|
-
# never write its translation sidecar and rendered the English source.
|
|
1908
1902
|
assignments = (
|
|
1909
1903
|
lead,
|
|
1910
1904
|
*(row for row in plan.assignments if row.role != "leader"),
|
|
1911
|
-
translator,
|
|
1912
1905
|
)
|
|
1913
1906
|
for index, assignment in enumerate(assignments, start=1):
|
|
1914
1907
|
participant_ref = f"participant-{index:03d}"
|
|
@@ -2387,7 +2380,6 @@ class _ModelBindings:
|
|
|
2387
2380
|
lead_assignment: RoleAssignment
|
|
2388
2381
|
worker_assignments: tuple[RoleAssignment, ...]
|
|
2389
2382
|
invocation_assignments: dict[str, dict[str, object]]
|
|
2390
|
-
translator: ResolvedAssignment
|
|
2391
2383
|
|
|
2392
2384
|
|
|
2393
2385
|
@dataclass(frozen=True)
|
|
@@ -2622,13 +2614,6 @@ def _project_model_bindings(
|
|
|
2622
2614
|
if executor_rows
|
|
2623
2615
|
else None
|
|
2624
2616
|
)
|
|
2625
|
-
translator = _model_for_role(
|
|
2626
|
-
context=context,
|
|
2627
|
-
provider=report_writer.provider_id,
|
|
2628
|
-
raw_value=report_writer.model_id,
|
|
2629
|
-
role="translator",
|
|
2630
|
-
duty_id="translator",
|
|
2631
|
-
)
|
|
2632
2617
|
projections = {
|
|
2633
2618
|
provider: _legacy_projection_from_plan(provider, plan, context)
|
|
2634
2619
|
for provider in ("claude", "codex", "antigravity")
|
|
@@ -2637,7 +2622,6 @@ def _project_model_bindings(
|
|
|
2637
2622
|
lead_assignment,
|
|
2638
2623
|
workers,
|
|
2639
2624
|
critic_assignment,
|
|
2640
|
-
_role_assignment(resolved=translator, role="translator"),
|
|
2641
2625
|
inp.task_type,
|
|
2642
2626
|
)
|
|
2643
2627
|
worker_by_provider = {row.provider: row for row in workers}
|
|
@@ -2668,7 +2652,6 @@ def _project_model_bindings(
|
|
|
2668
2652
|
lead_assignment=lead_assignment,
|
|
2669
2653
|
worker_assignments=workers,
|
|
2670
2654
|
invocation_assignments=invocation_assignments,
|
|
2671
|
-
translator=translator,
|
|
2672
2655
|
)
|
|
2673
2656
|
|
|
2674
2657
|
|
|
@@ -2759,7 +2742,6 @@ def _build_invocation_assignments(
|
|
|
2759
2742
|
lead: RoleAssignment,
|
|
2760
2743
|
workers: tuple[RoleAssignment, ...],
|
|
2761
2744
|
critic: RoleAssignment | None,
|
|
2762
|
-
translator: RoleAssignment,
|
|
2763
2745
|
task_type: str,
|
|
2764
2746
|
) -> dict[str, dict[str, object]]:
|
|
2765
2747
|
assignments = {"lead": lead.to_model_payload()}
|
|
@@ -2776,7 +2758,6 @@ def _build_invocation_assignments(
|
|
|
2776
2758
|
# role 라벨을 만들기 때문에 두 행의 이름이 겹쳐 validate-run 이
|
|
2777
2759
|
# `duplicate worker role detected` 로 런을 실패시킨다.
|
|
2778
2760
|
assignments[critic_assignment_ref(task_type)] = critic.to_model_payload()
|
|
2779
|
-
assignments["translator"] = translator.to_model_payload()
|
|
2780
2761
|
return assignments
|
|
2781
2762
|
|
|
2782
2763
|
|
|
@@ -3660,7 +3641,7 @@ def _allowed_agent_audiences(
|
|
|
3660
3641
|
inp: PrepareInputs,
|
|
3661
3642
|
models: _ModelBindings,
|
|
3662
3643
|
) -> list[str]:
|
|
3663
|
-
audiences = {"lead"
|
|
3644
|
+
audiences = {"lead"}
|
|
3664
3645
|
if models.worker_assignments:
|
|
3665
3646
|
audiences.add("reverification-worker")
|
|
3666
3647
|
if any(item.worker_id == "report-writer" for item in models.worker_assignments):
|
|
@@ -4162,7 +4143,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4162
4143
|
assignment_plan,
|
|
4163
4144
|
assignment_context,
|
|
4164
4145
|
models.lead,
|
|
4165
|
-
models.translator,
|
|
4166
4146
|
)
|
|
4167
4147
|
dynamic_roles = tuple(
|
|
4168
4148
|
requirement.role
|
|
@@ -283,8 +283,14 @@ def _teardown_all(project_id, task_group, task_id, task_wt: str,
|
|
|
283
283
|
|
|
284
284
|
def _remove_clean_worktree(task_wt: str, wt: str, stage_n: int) -> tuple[str, str] | None:
|
|
285
285
|
"""clean stage worktree 를 물리 제거. dirty/실패면 (outcome, detail), 성공이면 None."""
|
|
286
|
-
from .worktree import
|
|
287
|
-
|
|
286
|
+
from .worktree import (
|
|
287
|
+
is_dirty_excluding_okstra,
|
|
288
|
+
nested_worktree_excludes,
|
|
289
|
+
remove_worktree_force,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if nested_worktree_excludes(wt):
|
|
293
|
+
return ("skipped", "nested worktrees remain — preserved")
|
|
288
294
|
if is_dirty_excluding_okstra(wt):
|
|
289
295
|
return ("skipped", "dirty — 미커밋 변경 보존")
|
|
290
296
|
rm = remove_worktree_force(task_wt, wt)
|
|
@@ -293,6 +299,35 @@ def _remove_clean_worktree(task_wt: str, wt: str, stage_n: int) -> tuple[str, st
|
|
|
293
299
|
return None
|
|
294
300
|
|
|
295
301
|
|
|
302
|
+
def reclaim_completed_stage(
|
|
303
|
+
project_root: Path, project_id: str, task_group: str, task_id: str,
|
|
304
|
+
stage: int, expected_head: str, done: dict | None, status: str,
|
|
305
|
+
) -> tuple[str, str]:
|
|
306
|
+
from . import worktree_registry
|
|
307
|
+
from .worktree import main_worktree_path
|
|
308
|
+
|
|
309
|
+
row = worktree_registry.get_stage_row(project_id, task_group, task_id, stage)
|
|
310
|
+
if not row:
|
|
311
|
+
return "skipped", "stage worktree is not registered"
|
|
312
|
+
if status != "done" or row.get("status") == "active":
|
|
313
|
+
return "skipped", "stage is unfinished or active"
|
|
314
|
+
if not expected_head or (done or {}).get("head_commit") != expected_head:
|
|
315
|
+
return "skipped", "completion commit does not match this report"
|
|
316
|
+
tip = _git(project_root, "rev-parse", "--verify", f"refs/heads/{row['branch']}")
|
|
317
|
+
if tip.returncode or tip.stdout.strip() != expected_head:
|
|
318
|
+
return "skipped", "stage branch moved after completion"
|
|
319
|
+
wt = Path(row["worktree_path"])
|
|
320
|
+
if wt.resolve() == main_worktree_path(project_root).resolve():
|
|
321
|
+
return "skipped", "main worktree is preserved"
|
|
322
|
+
if wt.exists():
|
|
323
|
+
current = _git(wt, "symbolic-ref", "--short", "HEAD")
|
|
324
|
+
if current.returncode or current.stdout.strip() != row["branch"]:
|
|
325
|
+
return "skipped", "worktree no longer holds the registered stage branch"
|
|
326
|
+
return _teardown_stage(
|
|
327
|
+
project_id, task_group, task_id, str(project_root), stage, row, expected_head,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
296
331
|
def _unmerged_branch_tip_warning(task_wt: str, stage_n: int, branch: str,
|
|
297
332
|
verified_head: str = "") -> str:
|
|
298
333
|
"""Warn text when the stage branch tip is not an ancestor of the commit
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""선행 기준이 이동한 단계 브랜치를 의존 순서로 재배치한다."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import consumers, worktree_registry
|
|
8
|
+
from .stage_targets import (
|
|
9
|
+
StageWorktreeIdentity,
|
|
10
|
+
order_stage_closure,
|
|
11
|
+
resolve_stage_base_commit,
|
|
12
|
+
)
|
|
13
|
+
from .worktree import is_ancestor
|
|
14
|
+
from .worktree.rebase import _checked_git, rebase_stage_branch
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _rebase_completed_stage(
|
|
18
|
+
project_root: Path,
|
|
19
|
+
plan_run_root: Path,
|
|
20
|
+
identity: StageWorktreeIdentity,
|
|
21
|
+
row: dict,
|
|
22
|
+
done_row: dict,
|
|
23
|
+
base: str,
|
|
24
|
+
) -> dict:
|
|
25
|
+
number, branch = row["stage"], row["branch"]
|
|
26
|
+
tip = _checked_git(project_root, "rev-parse", f"refs/heads/{branch}")
|
|
27
|
+
if tip != done_row.get("head_commit"):
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
f"stage {number} branch moved beyond its done commit; reconcile it before rebasing"
|
|
30
|
+
)
|
|
31
|
+
head = rebase_stage_branch(project_root, branch, row["base_ref"], base)
|
|
32
|
+
worktree_registry.update_stage_base(
|
|
33
|
+
identity.project_id,
|
|
34
|
+
identity.task_group,
|
|
35
|
+
identity.task_id,
|
|
36
|
+
number,
|
|
37
|
+
expected_base=row["base_ref"],
|
|
38
|
+
base_ref=base,
|
|
39
|
+
)
|
|
40
|
+
consumers.append_consumer(
|
|
41
|
+
plan_run_root,
|
|
42
|
+
impl_task_key=done_row["impl_task_key"],
|
|
43
|
+
stage=number,
|
|
44
|
+
status="done",
|
|
45
|
+
force_reappend=True,
|
|
46
|
+
head_commit=head,
|
|
47
|
+
replaced_commit=tip,
|
|
48
|
+
reconciled=True,
|
|
49
|
+
reconcile_reason="dependency-rebase",
|
|
50
|
+
report_path=done_row.get("report_path", ""),
|
|
51
|
+
)
|
|
52
|
+
return {**done_row, "head_commit": head}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _refresh_predecessor_head(
|
|
56
|
+
root: Path,
|
|
57
|
+
plan: Path,
|
|
58
|
+
row: dict,
|
|
59
|
+
done_row: dict,
|
|
60
|
+
active: bool,
|
|
61
|
+
) -> dict:
|
|
62
|
+
tip = _checked_git(root, "rev-parse", f"refs/heads/{row['branch']}")
|
|
63
|
+
previous = done_row["head_commit"]
|
|
64
|
+
if tip == previous:
|
|
65
|
+
return done_row
|
|
66
|
+
if active or not is_ancestor(root, previous, tip):
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"predecessor stage {row['stage']} is active or rewritten; finish or reconcile it first"
|
|
69
|
+
)
|
|
70
|
+
consumers.append_consumer(
|
|
71
|
+
plan,
|
|
72
|
+
impl_task_key=done_row["impl_task_key"],
|
|
73
|
+
stage=row["stage"],
|
|
74
|
+
status="done",
|
|
75
|
+
force_reappend=True,
|
|
76
|
+
head_commit=tip,
|
|
77
|
+
replaced_commit=previous,
|
|
78
|
+
reconciled=True,
|
|
79
|
+
reconcile_reason="predecessor-advance",
|
|
80
|
+
report_path=done_row.get("report_path", ""),
|
|
81
|
+
)
|
|
82
|
+
return {**done_row, "head_commit": tip}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def synchronize_stage_stack(
|
|
86
|
+
*,
|
|
87
|
+
project_root: Path,
|
|
88
|
+
plan_run_root: Path,
|
|
89
|
+
stage_map: list[dict],
|
|
90
|
+
identity: StageWorktreeIdentity,
|
|
91
|
+
) -> list[int]:
|
|
92
|
+
"""호출자가 task-key provision 잠금을 잡은 상태에서 실행한다."""
|
|
93
|
+
rows = consumers.read_consumers(plan_run_root)
|
|
94
|
+
done = consumers.latest_done_by_stage(rows)
|
|
95
|
+
statuses = consumers.last_lifecycle_status_by_stage(rows)
|
|
96
|
+
anchor = (
|
|
97
|
+
worktree_registry.get_implementation_base(
|
|
98
|
+
identity.project_id,
|
|
99
|
+
identity.task_group,
|
|
100
|
+
identity.task_id,
|
|
101
|
+
)
|
|
102
|
+
or ""
|
|
103
|
+
)
|
|
104
|
+
graph = [(s["stage_number"], s.get("depends_on") or []) for s in stage_map]
|
|
105
|
+
order = order_stage_closure(graph, {n for n, _ in graph}, set())
|
|
106
|
+
if len(order) != len(graph):
|
|
107
|
+
raise RuntimeError("cannot synchronize a cyclic Stage Map")
|
|
108
|
+
by_number = {s["stage_number"]: s for s in stage_map}
|
|
109
|
+
predecessors = {dep for _, deps in graph for dep in deps}
|
|
110
|
+
changed = []
|
|
111
|
+
for number in order:
|
|
112
|
+
row = worktree_registry.get_stage_row(
|
|
113
|
+
identity.project_id,
|
|
114
|
+
identity.task_group,
|
|
115
|
+
identity.task_id,
|
|
116
|
+
number,
|
|
117
|
+
)
|
|
118
|
+
stage = by_number[number]
|
|
119
|
+
if not row or number not in done or stage.get("cancelled"):
|
|
120
|
+
continue
|
|
121
|
+
active = statuses.get(number) == "started" or row.get("status") == "active"
|
|
122
|
+
if _synchronize_recorded_stage(
|
|
123
|
+
project_root,
|
|
124
|
+
plan_run_root,
|
|
125
|
+
identity,
|
|
126
|
+
stage,
|
|
127
|
+
row,
|
|
128
|
+
done,
|
|
129
|
+
anchor,
|
|
130
|
+
active,
|
|
131
|
+
number in predecessors,
|
|
132
|
+
):
|
|
133
|
+
changed.append(number)
|
|
134
|
+
return changed
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _synchronize_recorded_stage(
|
|
138
|
+
root: Path,
|
|
139
|
+
plan: Path,
|
|
140
|
+
identity: StageWorktreeIdentity,
|
|
141
|
+
stage: dict,
|
|
142
|
+
row: dict,
|
|
143
|
+
done: dict[int, dict],
|
|
144
|
+
anchor: str,
|
|
145
|
+
active: bool,
|
|
146
|
+
predecessor: bool,
|
|
147
|
+
) -> bool:
|
|
148
|
+
number = stage["stage_number"]
|
|
149
|
+
previous = done[number]["head_commit"]
|
|
150
|
+
if predecessor:
|
|
151
|
+
done[number] = _refresh_predecessor_head(root, plan, row, done[number], active)
|
|
152
|
+
if stage.get("depends_on"):
|
|
153
|
+
done[number] = _synchronize_dependent(
|
|
154
|
+
root,
|
|
155
|
+
plan,
|
|
156
|
+
identity,
|
|
157
|
+
stage,
|
|
158
|
+
row,
|
|
159
|
+
done,
|
|
160
|
+
anchor,
|
|
161
|
+
active,
|
|
162
|
+
)
|
|
163
|
+
return previous != done[number]["head_commit"]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _synchronize_dependent(
|
|
167
|
+
root: Path,
|
|
168
|
+
plan: Path,
|
|
169
|
+
identity: StageWorktreeIdentity,
|
|
170
|
+
stage: dict,
|
|
171
|
+
row: dict,
|
|
172
|
+
done: dict[int, dict],
|
|
173
|
+
anchor: str,
|
|
174
|
+
active: bool,
|
|
175
|
+
) -> dict:
|
|
176
|
+
number = stage["stage_number"]
|
|
177
|
+
base = resolve_stage_base_commit(
|
|
178
|
+
stage,
|
|
179
|
+
list(done.values()),
|
|
180
|
+
anchor,
|
|
181
|
+
project_root=root,
|
|
182
|
+
plan_run_root=plan,
|
|
183
|
+
identity=identity,
|
|
184
|
+
)
|
|
185
|
+
if row["base_ref"] == base:
|
|
186
|
+
return done[number]
|
|
187
|
+
if active:
|
|
188
|
+
raise RuntimeError(f"cannot rebase active stage {number}; finish its run first")
|
|
189
|
+
return _rebase_completed_stage(root, plan, identity, row, done[number], base)
|
|
@@ -831,76 +831,43 @@ def containing_stage(project_root: Path,
|
|
|
831
831
|
return single_containing_head(project_root, heads)
|
|
832
832
|
|
|
833
833
|
|
|
834
|
-
def
|
|
834
|
+
def teardown_completed_stage_worktrees(
|
|
835
835
|
*, project_root: Path, task_group: str, task_id: str,
|
|
836
|
+
expected_heads: dict[int, str] | None = None,
|
|
836
837
|
) -> dict[str, Any]:
|
|
837
|
-
"""
|
|
838
|
-
|
|
839
|
-
진입은 통합만 하고 정리를 남겨 두므로(`phases/final_verification/target.py`), 정리는
|
|
840
|
-
판정 뒤인 Phase 7 에서 여기로 들어온다. 통합은 이미 끝나 있어 Phase A 는 전부
|
|
841
|
-
`already_merged` 로 지나가고 Phase B 만 실제 일을 한다. 두 번 불려도 결과는 같다 —
|
|
842
|
-
사라진 worktree 는 건너뛰고, 미커밋 변경이 남은 stage 트리는 보존한다.
|
|
843
|
-
|
|
844
|
-
stage_map 은 done 행에서 만든다. 정리 대상은 완료된 stage 뿐이고, 계획에만 있고
|
|
845
|
-
완료되지 않은 stage 는 Phase A 가 어차피 건너뛰기 때문이다.
|
|
846
|
-
"""
|
|
838
|
+
"""완료된 단계의 작업 트리만 회수하고 브랜치와 기준 기록은 보존한다."""
|
|
847
839
|
from okstra_project.dirs import okstra_home, project_json_path
|
|
848
840
|
|
|
849
|
-
from . import consumers
|
|
841
|
+
from . import consumers
|
|
850
842
|
from .json_boundary import load_owned_object
|
|
851
843
|
from .locks import worktree_provision_mutex
|
|
852
844
|
from .paths import task_runs_dir
|
|
853
|
-
from .stage_integrate import
|
|
845
|
+
from .stage_integrate import reclaim_completed_stage
|
|
854
846
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
except (OSError, ValueError):
|
|
860
|
-
project_id = ""
|
|
847
|
+
config = project_json_path(project_root)
|
|
848
|
+
if not config.is_file():
|
|
849
|
+
return {"skipped": "project.json is missing"}
|
|
850
|
+
project_id = load_owned_object(config, artifact="project config").get("projectId", "")
|
|
861
851
|
if not project_id:
|
|
862
852
|
return {"skipped": "project.json declares no projectId"}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
project_root, task_group, task_id
|
|
866
|
-
) / "implementation-planning"
|
|
867
|
-
done_rows = [
|
|
868
|
-
row for row in consumers.read_consumers(plan_run_root)
|
|
869
|
-
if row.get("status") == "done"
|
|
870
|
-
]
|
|
871
|
-
if not done_rows:
|
|
872
|
-
return {"skipped": "no done stage rows to reclaim"}
|
|
873
|
-
stage_map = [
|
|
874
|
-
{"stage_number": stage}
|
|
875
|
-
for stage in sorted(consumers.latest_done_by_stage(done_rows))
|
|
876
|
-
]
|
|
877
|
-
|
|
878
|
-
entry = worktree_registry.lookup(project_id, task_group, task_id)
|
|
879
|
-
if entry is None:
|
|
880
|
-
return {"skipped": "task worktree is no longer registered"}
|
|
881
|
-
|
|
882
|
-
done_by_stage = consumers.latest_done_by_stage(done_rows)
|
|
883
|
-
tip = containing_stage(project_root, done_by_stage)
|
|
853
|
+
plan = task_runs_dir(project_root, task_group, task_id) / "implementation-planning"
|
|
854
|
+
removed, skipped, warnings = [], [], []
|
|
884
855
|
with worktree_provision_mutex(okstra_home(), project_id, task_group, task_id):
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
for stage, reason in result.teardown_skipped
|
|
904
|
-
],
|
|
905
|
-
"warnings": result.warnings,
|
|
906
|
-
}
|
|
856
|
+
rows = consumers.read_consumers(plan)
|
|
857
|
+
done = consumers.latest_done_by_stage(rows)
|
|
858
|
+
statuses = consumers.last_lifecycle_status_by_stage(rows)
|
|
859
|
+
heads = expected_heads if expected_heads is not None else {
|
|
860
|
+
stage: row.get("head_commit", "") for stage, row in done.items()
|
|
861
|
+
}
|
|
862
|
+
for stage, head in sorted(heads.items()):
|
|
863
|
+
outcome, detail = reclaim_completed_stage(
|
|
864
|
+
project_root, project_id, task_group, task_id, stage, head,
|
|
865
|
+
done.get(stage), statuses.get(stage, ""),
|
|
866
|
+
)
|
|
867
|
+
if outcome == "torn_down":
|
|
868
|
+
removed.append(stage)
|
|
869
|
+
elif outcome == "skipped":
|
|
870
|
+
skipped.append({"stage": stage, "reason": detail})
|
|
871
|
+
else:
|
|
872
|
+
warnings.append(detail)
|
|
873
|
+
return {"tornDown": removed, "teardownSkipped": skipped, "warnings": warnings}
|
|
@@ -228,12 +228,7 @@ def _record_dispatch(args, manifest: Mapping[str, Any], prepared) -> list[str]:
|
|
|
228
228
|
|
|
229
229
|
|
|
230
230
|
def team_dispatch_port(manifest: Mapping[str, Any]) -> WorkerDispatchPort:
|
|
231
|
-
"""이 run 의 워커를 띄울 포트. cmux run 이면 pane, 아니면 리드 호스트의 포트.
|
|
232
|
-
|
|
233
|
-
`team dispatch` 와 finalize 의 `translate` 단계가 같이 쓴다 — 번역자만
|
|
234
|
-
CLI 래퍼 디스패처로 띄우던 동안 cmux run 에서도 번역자는 pane 없이 돌았다
|
|
235
|
-
(실측 2026-09-22, dev-10860 implementation-planning-001: 18건 중 번역자 1건).
|
|
236
|
-
"""
|
|
231
|
+
"""이 run 의 워커를 띄울 포트. cmux run 이면 pane, 아니면 리드 호스트의 포트."""
|
|
237
232
|
lead_runtime = _require_string(manifest, "leadRuntime")
|
|
238
233
|
fallback_port = default_host_registry().resolve(
|
|
239
234
|
lead_runtime
|
|
@@ -333,14 +333,6 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
333
333
|
lines.append(f" pr-template : {state.pr_template_path} ({state.pr_template_scope or 'once'})")
|
|
334
334
|
if state.fix_cycle:
|
|
335
335
|
lines.append(f" fix-cycle : {state.fix_cycle}")
|
|
336
|
-
report_writer_models = state.role_models.get("report-writer", [])
|
|
337
|
-
translator_model = (
|
|
338
|
-
report_writer_models[0] if report_writer_models
|
|
339
|
-
else "(selection required)"
|
|
340
|
-
)
|
|
341
|
-
lines.append(_msg(
|
|
342
|
-
state.workspace_root, "confirmation", "translation_scope", model=translator_model,
|
|
343
|
-
))
|
|
344
336
|
lines.append(_msg(
|
|
345
337
|
state.workspace_root, "confirmation", "provider_data_scope",
|
|
346
338
|
project_root=state.project_root,
|
|
@@ -93,10 +93,10 @@ from .state import (
|
|
|
93
93
|
from .prompts import _opt, _p
|
|
94
94
|
from .sources import (
|
|
95
95
|
_brief_resolved,
|
|
96
|
+
_clarification_pick_required,
|
|
96
97
|
_has_prior_run_inputs,
|
|
97
98
|
_latest_implementation_planning_report,
|
|
98
99
|
_reverify_scope_pick_required,
|
|
99
|
-
_suggest_latest_final_report,
|
|
100
100
|
)
|
|
101
101
|
from .roles import _clear_role_selection, _reset_role_selection_from, next_role_prompt
|
|
102
102
|
from .steps_identity import (
|
|
@@ -535,8 +535,7 @@ STEPS: list[Step] = [
|
|
|
535
535
|
Step(S_CLARIFICATION_PICK,
|
|
536
536
|
applies=lambda s: (S_CLARIFICATION_PICK not in s.answered
|
|
537
537
|
and _identity_ready(s)
|
|
538
|
-
and (s
|
|
539
|
-
or bool(_suggest_latest_final_report(s)))),
|
|
538
|
+
and _clarification_pick_required(s)),
|
|
540
539
|
build=_build_clarification_pick, submit=_submit_clarification_pick,
|
|
541
540
|
owns=("clarification_response_path", "clarification_pending_text")),
|
|
542
541
|
Step(S_CLARIFICATION,
|
|
@@ -600,6 +600,16 @@ def _latest_run_inputs(
|
|
|
600
600
|
phase_segment=phase_segment)
|
|
601
601
|
|
|
602
602
|
|
|
603
|
+
def _clarification_pick_required(state: WizardState) -> bool:
|
|
604
|
+
if state.task_type == "release-handoff":
|
|
605
|
+
report = _suggest_latest_final_report(state)
|
|
606
|
+
return bool(report and sidecar_answers(Path(state.project_root) / report))
|
|
607
|
+
return (
|
|
608
|
+
state.task_type != "implementation-planning"
|
|
609
|
+
or bool(_suggest_latest_final_report(state))
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
603
613
|
def _carried_planning_report(state: WizardState) -> Optional[Path]:
|
|
604
614
|
"""이번 재실행이 이어받는 직전 implementation-planning 리포트 (없으면 None)."""
|
|
605
615
|
if state.task_type != "implementation-planning":
|