okstra 0.224.0 → 0.225.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/dist/commands/execute/render-bundle.mjs +7 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/pr/pr.mjs +42 -13
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/docs/cli.md +11 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +4 -1
- package/runtime/bin/okstra-provider-exec.py +32 -16
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/duties/code-reviewer.json +10 -5
- package/runtime/prompts/wizard/prompts.ko.json +10 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +3 -1
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +5 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +25 -0
- package/runtime/python/okstra_ctl/cmux.py +4 -2
- package/runtime/python/okstra_ctl/code_review_coverage.py +237 -0
- package/runtime/python/okstra_ctl/code_review_target.py +31 -2
- package/runtime/python/okstra_ctl/coverage_census.py +2 -2
- package/runtime/python/okstra_ctl/dispatch_core.py +15 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +6 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +66 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +5 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/round_completion.py +177 -0
- package/runtime/python/okstra_ctl/render.py +2 -0
- package/runtime/python/okstra_ctl/run.py +23 -2
- package/runtime/python/okstra_ctl/team.py +12 -18
- package/runtime/python/okstra_ctl/wizard/__init__.py +1 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +2 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +3 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +11 -0
- package/runtime/python/okstra_ctl/wizard/render.py +1 -0
- package/runtime/python/okstra_ctl/wizard/state.py +3 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +41 -1
- package/runtime/python/okstra_ctl/worker_dispatch.py +9 -0
- package/runtime/python/okstra_ctl/worker_request.py +12 -1
- package/runtime/schemas/execution-manifest-v2.schema.json +1 -0
- package/runtime/skills/_fragments/bash-invocation-rule.md +1 -1
- package/runtime/skills/_fragments/host-question-interface.md +1 -0
- package/runtime/skills/_fragments/host-runtime-selection.md +1 -0
- package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -1
- package/runtime/skills/_fragments/wizard-preflight.md +1 -1
- package/runtime/skills/_fragments/wizard-render-bundle.md +4 -4
- package/runtime/skills/okstra-brief-gen/SKILL.md +45 -47
- package/runtime/skills/okstra-brief-gen/references/reporter-confirmations.md +3 -3
- package/runtime/skills/okstra-brief-gen/references/tracker-recursion.md +1 -1
- package/runtime/skills/okstra-chat/SKILL.md +1 -1
- package/runtime/skills/okstra-code-review/SKILL.md +28 -15
- package/runtime/skills/okstra-code-review/references/census-rules.md +52 -2
- package/runtime/skills/okstra-code-review/references/review-calibration.md +45 -15
- package/runtime/skills/okstra-container-build/SKILL.md +6 -2
- package/runtime/skills/okstra-discussion/SKILL.md +6 -6
- package/runtime/skills/okstra-inspect/SKILL.md +5 -3
- package/runtime/skills/okstra-manager/SKILL.md +8 -4
- package/runtime/skills/okstra-memory/SKILL.md +1 -1
- package/runtime/skills/okstra-pr-gen/SKILL.md +25 -7
- package/runtime/skills/okstra-rollup/SKILL.md +6 -2
- package/runtime/skills/okstra-run/SKILL.md +8 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +13 -6
- package/runtime/skills/okstra-setup/SKILL.md +8 -4
- package/runtime/skills/okstra-setup/references/project-config.md +3 -3
- package/runtime/skills/okstra-usage/SKILL.md +5 -1
- package/runtime/skills/okstra-user-response/SKILL.md +7 -3
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""채택된 계획 검증 배치의 결정적 후처리."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from okstra_ctl.convergence import validated_run_authority
|
|
12
|
+
from okstra_ctl.convergence_store import load_owned_json_object, write_json_atomic
|
|
13
|
+
from okstra_ctl.dispatch_state import dispatch_worker_key
|
|
14
|
+
from okstra_ctl.error_log_write import record_runtime_failure
|
|
15
|
+
from okstra_ctl.plan_items import PlanItemContractError, analyser_key, critic_is_rostered
|
|
16
|
+
from okstra_ctl.run_context import dir_flock
|
|
17
|
+
from okstra_ctl.worker_prompt_policy import (
|
|
18
|
+
is_plan_verify_dispatch_kind, verification_dispatch_round,
|
|
19
|
+
)
|
|
20
|
+
from .authoring import finish_verification_round, validate_current_review
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from okstra_ctl.dispatch_core import DispatchPlan
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _verifier_key(row: Mapping[str, Any]) -> str:
|
|
27
|
+
if row.get("assignmentRef") == "critic/scope":
|
|
28
|
+
return "critic"
|
|
29
|
+
return analyser_key(dispatch_worker_key(row))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _round_dispatches(plan: DispatchPlan, team: Mapping[str, Any]) -> tuple[int, list[dict]] | None:
|
|
33
|
+
accepted = {row.get("dispatchId") for row in team.get("agentResultLinks", [])
|
|
34
|
+
if isinstance(row, Mapping) and not row.get("supersededBy")}
|
|
35
|
+
native = [{**row, "kind": row.get("dispatchKind", ""),
|
|
36
|
+
"status": "completed" if row.get("status") == "dispatched" and row.get("dispatchId") in accepted else row.get("status"),
|
|
37
|
+
"workerResultPath": row.get("workerResultPath") or next(
|
|
38
|
+
(link.get("resultPath") for link in team.get("agentResultLinks", [])
|
|
39
|
+
if link.get("dispatchId") == row.get("dispatchId") and not link.get("supersededBy")), "")}
|
|
40
|
+
for row in team.get("agentDispatches", []) if isinstance(row, dict)]
|
|
41
|
+
tails = [rows[-1] for rows in (team.get("workerDispatches", []), native) if rows]
|
|
42
|
+
if not plan.jobs and tails and all(
|
|
43
|
+
not is_plan_verify_dispatch_kind(str(row.get("kind", ""))) for row in tails
|
|
44
|
+
):
|
|
45
|
+
return None
|
|
46
|
+
rows = [row for row in [*team.get("workerDispatches", []), *native] if isinstance(row, dict)
|
|
47
|
+
and is_plan_verify_dispatch_kind(str(row.get("kind", "")))]
|
|
48
|
+
targets = {verification_dispatch_round(job.dispatch_kind) for job in plan.jobs
|
|
49
|
+
if is_plan_verify_dispatch_kind(job.dispatch_kind)}
|
|
50
|
+
if plan.jobs and not targets:
|
|
51
|
+
return None
|
|
52
|
+
if not rows:
|
|
53
|
+
return None
|
|
54
|
+
if len(targets) > 1:
|
|
55
|
+
raise PlanItemContractError("one plan-verification batch cannot span multiple rounds")
|
|
56
|
+
number = next(iter(targets)) if targets else max(
|
|
57
|
+
verification_dispatch_round(row["kind"]) for row in rows
|
|
58
|
+
)
|
|
59
|
+
superseded = {row.get("dispatchId") for row in team.get("agentResultLinks", [])
|
|
60
|
+
if isinstance(row, Mapping) and row.get("supersededBy")}
|
|
61
|
+
latest: dict[str, dict] = {}
|
|
62
|
+
for row in rows:
|
|
63
|
+
if verification_dispatch_round(row["kind"]) != number or row.get("dispatchId") in superseded:
|
|
64
|
+
continue
|
|
65
|
+
key = _verifier_key(row)
|
|
66
|
+
previous = latest.get(key)
|
|
67
|
+
identity = row.get("invocationRef") or row.get("invocationId")
|
|
68
|
+
if previous and (not identity or identity != (previous.get("invocationRef") or previous.get("invocationId"))):
|
|
69
|
+
raise PlanItemContractError(f"ambiguous plan-verification invocations for {key}")
|
|
70
|
+
if previous is None or row.get("attempt", 1) > previous.get("attempt", 1):
|
|
71
|
+
latest[key] = row
|
|
72
|
+
return int(number), list(latest.values())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _accepted_results(plan: DispatchPlan, team: Mapping[str, Any], rows: list[dict]) -> list[str]:
|
|
76
|
+
authority = validated_run_authority(plan.manifest_path)
|
|
77
|
+
execution = authority.execution_manifest
|
|
78
|
+
results = []
|
|
79
|
+
for row in rows:
|
|
80
|
+
declared_path = plan.project_root / str(row.get("workerResultPath", ""))
|
|
81
|
+
path = declared_path.resolve(strict=True)
|
|
82
|
+
if declared_path.is_symlink():
|
|
83
|
+
raise PlanItemContractError("plan-verification result must not be a symlink")
|
|
84
|
+
if not path.is_relative_to(authority.run_dir / "worker-results") or not path.is_file():
|
|
85
|
+
raise PlanItemContractError("plan-verification result must stay inside this run's worker-results")
|
|
86
|
+
links = [link for link in team.get("agentResultLinks", []) if isinstance(link, Mapping)
|
|
87
|
+
and link.get("dispatchId") == row.get("dispatchId") and not link.get("supersededBy")]
|
|
88
|
+
if len(links) != 1 or (plan.project_root / str(links[0].get("resultPath", ""))).resolve() != path:
|
|
89
|
+
raise PlanItemContractError("plan-verification result has no unique accepted result link")
|
|
90
|
+
if execution is not None:
|
|
91
|
+
attempts = [attempt for attempt in execution.attempts
|
|
92
|
+
if attempt.invocation_ref == row.get("invocationRef")
|
|
93
|
+
and attempt.attempt == row.get("attempt")]
|
|
94
|
+
if len(attempts) != 1 or attempts[0].status != "ok":
|
|
95
|
+
raise PlanItemContractError("plan-verification attempt is not accepted")
|
|
96
|
+
if (plan.project_root / str(attempts[0].result_path or "")).resolve() != path:
|
|
97
|
+
raise PlanItemContractError("accepted attempt result path does not match the dispatch")
|
|
98
|
+
token = f'-plan-verify-r{verification_dispatch_round(row["kind"])}-'
|
|
99
|
+
if token not in path.name:
|
|
100
|
+
raise PlanItemContractError("plan-verification result filename has the wrong round")
|
|
101
|
+
worker = path.name.split(token, 1)[0]
|
|
102
|
+
if analyser_key(worker) != _verifier_key(row):
|
|
103
|
+
raise PlanItemContractError("plan-verification result has the wrong worker")
|
|
104
|
+
results.append(f"{worker}={path}")
|
|
105
|
+
return sorted(results)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _completion_receipt(plan: DispatchPlan, number: int, results: list[str]) -> dict[str, Any]:
|
|
109
|
+
review = validate_current_review(plan.manifest_path)
|
|
110
|
+
digest = hashlib.sha256()
|
|
111
|
+
for path in [review.items, review.narrative, *(Path(value.split("=", 1)[1]) for value in results)]:
|
|
112
|
+
digest.update(str(path).encode())
|
|
113
|
+
digest.update(path.read_bytes())
|
|
114
|
+
digest.update(json.dumps(results).encode())
|
|
115
|
+
fingerprint = digest.hexdigest()
|
|
116
|
+
receipt = review.state.with_name(f"plan-round-completion-r{number}-{review.state.name.removeprefix('plan-body-verification-')}")
|
|
117
|
+
state = load_owned_json_object(review.state)
|
|
118
|
+
if receipt.is_file():
|
|
119
|
+
recorded = load_owned_json_object(receipt)
|
|
120
|
+
if recorded.get("inputDigest") == fingerprint and recorded.get("stateDigest") == hashlib.sha256(review.state.read_bytes()).hexdigest():
|
|
121
|
+
return recorded
|
|
122
|
+
if any(row.get("round") == number for row in state.get("roundHistory", [])):
|
|
123
|
+
return {"status": "needs-lead", "round": number,
|
|
124
|
+
"reason": "round already closed; automatic completion will not replace its recorded inputs"}
|
|
125
|
+
prepared = load_owned_json_object(review.items)
|
|
126
|
+
if prepared.get("dispatchKind") == "reverify":
|
|
127
|
+
return {"status": "needs-lead", "round": number,
|
|
128
|
+
"reason": "self-fix re-verification needs the lead's cause groups and completion metadata"}
|
|
129
|
+
completed = finish_verification_round(review, results, number)
|
|
130
|
+
output = {**completed, "status": "completed", "round": number,
|
|
131
|
+
"inputDigest": fingerprint, "stateDigest": hashlib.sha256(review.state.read_bytes()).hexdigest(),
|
|
132
|
+
"receiptPath": str(receipt)}
|
|
133
|
+
write_json_atomic(receipt, output)
|
|
134
|
+
return output
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def complete_ready_plan_round(plan: DispatchPlan) -> dict[str, Any] | None:
|
|
138
|
+
from okstra_ctl.dispatch_core import TERMINAL_DISPATCH_STATUSES
|
|
139
|
+
|
|
140
|
+
if plan.manifest.get("taskType") != "implementation-planning":
|
|
141
|
+
return None
|
|
142
|
+
number = 0
|
|
143
|
+
try:
|
|
144
|
+
with dir_flock(plan.team_state_path.parent, ".plan-round-completion.lock"):
|
|
145
|
+
team = load_owned_json_object(plan.team_state_path)
|
|
146
|
+
batch = _round_dispatches(plan, team)
|
|
147
|
+
if batch is None:
|
|
148
|
+
return None
|
|
149
|
+
number, rows = batch
|
|
150
|
+
review = validate_current_review(plan.manifest_path)
|
|
151
|
+
prepared = load_owned_json_object(review.items)
|
|
152
|
+
workers = {analyser_key(worker) for worker in prepared.get("reviewInputs", {}).get("workers", [])}
|
|
153
|
+
actual = {_verifier_key(row) for row in rows}
|
|
154
|
+
if prepared.get("dispatchKind") == "critic-tie":
|
|
155
|
+
if not critic_is_rostered(plan.manifest):
|
|
156
|
+
return {"status": "needs-lead", "round": number,
|
|
157
|
+
"reason": "the run has no rostered critic to settle this tie"}
|
|
158
|
+
if actual & workers and not actual - (workers | {"critic"}):
|
|
159
|
+
return {"status": "needs-lead", "round": number,
|
|
160
|
+
"reason": "critic shares an analyser round; preserve its queue through manual append"}
|
|
161
|
+
workers = {"critic"}
|
|
162
|
+
if actual - workers:
|
|
163
|
+
raise PlanItemContractError("plan-verification dispatch includes an unassigned verifier")
|
|
164
|
+
if not workers or actual != workers:
|
|
165
|
+
return {"status": "pending", "round": number, "reason": "waiting for the complete prepared verifier roster"}
|
|
166
|
+
if any(row.get("status") not in TERMINAL_DISPATCH_STATUSES for row in rows):
|
|
167
|
+
return {"status": "pending", "round": number, "reason": "waiting for every verifier to settle"}
|
|
168
|
+
if any(row.get("status") != "completed" for row in rows):
|
|
169
|
+
raise PlanItemContractError("one or more plan verifiers failed; preserve their results for lead recovery")
|
|
170
|
+
return _completion_receipt(plan, number, _accepted_results(plan, team, rows))
|
|
171
|
+
except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired) as exc:
|
|
172
|
+
output = {"status": "failed", "round": number, "reason": str(exc)}
|
|
173
|
+
logged = record_runtime_failure(plan.manifest_path, command="plan-round automatic completion",
|
|
174
|
+
exit_code=2, detail=str(exc))
|
|
175
|
+
if not logged["ok"]:
|
|
176
|
+
output["errorLogFailure"] = logged["reason"]
|
|
177
|
+
return output
|
|
@@ -1761,6 +1761,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1761
1761
|
Path(ctx.get("PROJECT_ROOT", "")), task_manifest, {}
|
|
1762
1762
|
),
|
|
1763
1763
|
"leadModel": ctx.get("LEAD_MODEL", ""),
|
|
1764
|
+
# `okstra-provider-exec.py` 가 워커를 띄울 때마다 읽는다.
|
|
1765
|
+
"reasoningEffort": ctx.get("REASONING_EFFORT", ""),
|
|
1764
1766
|
# Every other path in this manifest is project-relative; this is the
|
|
1765
1767
|
# anchor they resolve against. Convergence reads it to place run
|
|
1766
1768
|
# artifacts, so a manifest without it cannot seed a round.
|
|
@@ -130,6 +130,7 @@ from .domain.host import (
|
|
|
130
130
|
ProviderUnavailable,
|
|
131
131
|
)
|
|
132
132
|
from .domain.provider import ServedModelAttestation
|
|
133
|
+
from .domain.worker_exec import REASONING_EFFORTS
|
|
133
134
|
from .execution_identity import (
|
|
134
135
|
ExecutionManifest,
|
|
135
136
|
ParticipantAssignment,
|
|
@@ -634,6 +635,9 @@ class PrepareInputs:
|
|
|
634
635
|
# discussion 전용: 링크·짧은 설명·이 task 의 key. brief 자리는 prepare 가
|
|
635
636
|
# 이 값으로 만든 입력 문서가 채운다.
|
|
636
637
|
discussion_start: str = ""
|
|
638
|
+
# 이 run 이 띄우는 모든 워커 CLI 의 추론 강도(`REASONING_EFFORTS`). 빈 값이면
|
|
639
|
+
# 각 CLI 가 자기 설정을 따른다.
|
|
640
|
+
reasoning_effort: str = ""
|
|
637
641
|
|
|
638
642
|
|
|
639
643
|
@dataclass
|
|
@@ -4257,6 +4261,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4257
4261
|
"OKSTRA_PLAN_VERIFICATION": (
|
|
4258
4262
|
"false" if not inp.plan_verification_enabled else ""
|
|
4259
4263
|
),
|
|
4264
|
+
"REASONING_EFFORT": inp.reasoning_effort,
|
|
4260
4265
|
"ANALYSIS_SOURCE_COMMIT": analysis_source_commit,
|
|
4261
4266
|
"ANALYSIS_TARGET_JSON": json.dumps(resolved_target, ensure_ascii=False),
|
|
4262
4267
|
"EVIDENCE_INPUTS_JSON": json.dumps(
|
|
@@ -4687,6 +4692,10 @@ def build_prepare_argument_parser():
|
|
|
4687
4692
|
),
|
|
4688
4693
|
)
|
|
4689
4694
|
p.add_argument("--render-only", action="store_true", dest="render_only")
|
|
4695
|
+
p.add_argument(
|
|
4696
|
+
"--summary-only", action="store_true",
|
|
4697
|
+
help="Omit the lead prompt body from render-only output; still prepare all artifacts.",
|
|
4698
|
+
)
|
|
4690
4699
|
p.add_argument(
|
|
4691
4700
|
"--terminal-backend",
|
|
4692
4701
|
dest="terminal_backend",
|
|
@@ -4698,6 +4707,16 @@ def build_prepare_argument_parser():
|
|
|
4698
4707
|
"않고 멈춘다."
|
|
4699
4708
|
),
|
|
4700
4709
|
)
|
|
4710
|
+
p.add_argument(
|
|
4711
|
+
"--reasoning-effort",
|
|
4712
|
+
default="",
|
|
4713
|
+
choices=["", *REASONING_EFFORTS],
|
|
4714
|
+
dest="reasoning_effort",
|
|
4715
|
+
help=(
|
|
4716
|
+
"Reasoning effort every worker CLI in this run is started with. "
|
|
4717
|
+
"Empty leaves each CLI on its own configuration."
|
|
4718
|
+
),
|
|
4719
|
+
)
|
|
4701
4720
|
p.add_argument(
|
|
4702
4721
|
"--no-plan-verification",
|
|
4703
4722
|
action="store_false",
|
|
@@ -4897,6 +4916,7 @@ def main(argv: list[str]) -> int:
|
|
|
4897
4916
|
plan_verification_enabled=args.plan_verification_enabled,
|
|
4898
4917
|
fix_cycle=args.fix_cycle,
|
|
4899
4918
|
discussion_start=args.discussion_start,
|
|
4919
|
+
reasoning_effort=args.reasoning_effort,
|
|
4900
4920
|
)
|
|
4901
4921
|
try:
|
|
4902
4922
|
out = prepare_task_bundle(inputs)
|
|
@@ -4919,8 +4939,9 @@ def main(argv: list[str]) -> int:
|
|
|
4919
4939
|
stages_csv = ",".join(str(s) for s in cr["active_stages"])
|
|
4920
4940
|
print(f"okstra concurrent-run stages: {stages_csv}")
|
|
4921
4941
|
if inputs.render_only:
|
|
4922
|
-
|
|
4923
|
-
|
|
4942
|
+
if not args.summary_only:
|
|
4943
|
+
print()
|
|
4944
|
+
print(out.prompt_text, end="")
|
|
4924
4945
|
else:
|
|
4925
4946
|
print(f"okstra current run dir: {ctx['RUN_DIR']}")
|
|
4926
4947
|
print(f"final report path: {ctx['FINAL_REPORT_PATH']}")
|
|
@@ -35,6 +35,7 @@ from .dispatch_core import (
|
|
|
35
35
|
DispatchPlan,
|
|
36
36
|
await_dispatches,
|
|
37
37
|
dispatch_plan,
|
|
38
|
+
existing_dispatch_plan as _plan_for_existing,
|
|
38
39
|
)
|
|
39
40
|
from .dispatch_checkpoints import (
|
|
40
41
|
record_collect_checkpoints,
|
|
@@ -257,6 +258,11 @@ def _await(args) -> int:
|
|
|
257
258
|
timeout_seconds=args.timeout_seconds,
|
|
258
259
|
heartbeat_seconds=0 if args.json else args.heartbeat_seconds,
|
|
259
260
|
)
|
|
261
|
+
from .phases.implementation_planning.round_completion import complete_ready_plan_round
|
|
262
|
+
|
|
263
|
+
completion = complete_ready_plan_round(plan) if code == 0 else None
|
|
264
|
+
if completion and completion["status"] == "failed":
|
|
265
|
+
code = 2
|
|
260
266
|
if should_record:
|
|
261
267
|
exit_state = _load_json(plan.team_state_path, "team-state")
|
|
262
268
|
lines.extend(record_collect_checkpoints(
|
|
@@ -272,11 +278,16 @@ def _await(args) -> int:
|
|
|
272
278
|
if args.json:
|
|
273
279
|
payload = _await_payload(plan, code == 0)
|
|
274
280
|
payload["progressLines"] = lines
|
|
281
|
+
if completion:
|
|
282
|
+
payload["planVerification"] = completion
|
|
275
283
|
_print_json(payload)
|
|
276
284
|
else:
|
|
277
285
|
for line in lines:
|
|
278
286
|
print(line)
|
|
279
|
-
|
|
287
|
+
if completion:
|
|
288
|
+
print(json.dumps({"planVerification": completion}, ensure_ascii=False))
|
|
289
|
+
print("PLAN_VERIFICATION_FAILED" if completion and completion["status"] == "failed"
|
|
290
|
+
else "ALL_WORKERS_DONE" if code == 0 else "POLL_TIMEOUT")
|
|
280
291
|
return code
|
|
281
292
|
|
|
282
293
|
|
|
@@ -402,23 +413,6 @@ def _observe_lead_session_from_manifest(
|
|
|
402
413
|
observe_lead_session(project_root, team_state_path)
|
|
403
414
|
|
|
404
415
|
|
|
405
|
-
def _plan_for_existing(
|
|
406
|
-
project_root: Path, workspace_root: Path, run_manifest_path: Path, manifest: Mapping[str, Any]
|
|
407
|
-
) -> DispatchPlan:
|
|
408
|
-
root = project_root.resolve()
|
|
409
|
-
manifest_path = _resolve_project_path(root, str(run_manifest_path))
|
|
410
|
-
return DispatchPlan(
|
|
411
|
-
project_root=root,
|
|
412
|
-
workspace_root=workspace_root.resolve(),
|
|
413
|
-
manifest_path=manifest_path,
|
|
414
|
-
team_state_path=_resolve_project_path(root, _require_string(manifest, "teamStatePath")),
|
|
415
|
-
lead_events_path=_resolve_project_path(root, _require_string(manifest, "leadEventsPath")),
|
|
416
|
-
manifest=manifest,
|
|
417
|
-
jobs=(),
|
|
418
|
-
default_backend=_manifest_backend(manifest),
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
|
|
422
416
|
def _await_payload(plan: DispatchPlan, completed: bool) -> dict[str, Any]:
|
|
423
417
|
team_state = _load_json(plan.team_state_path, "team-state")
|
|
424
418
|
dispatches = [d for d in team_state.get("workerDispatches", []) if isinstance(d, dict)]
|
|
@@ -333,6 +333,8 @@ 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
|
+
if state.reasoning_effort:
|
|
337
|
+
lines.append(f" reasoning : {state.reasoning_effort}")
|
|
336
338
|
lines.append(_msg(
|
|
337
339
|
state.workspace_root, "confirmation", "provider_data_scope",
|
|
338
340
|
project_root=state.project_root,
|
|
@@ -58,6 +58,7 @@ from .ids import (
|
|
|
58
58
|
S_PROJECT_EVIDENCE_PICK,
|
|
59
59
|
S_PR_TEMPLATE,
|
|
60
60
|
S_PR_TEMPLATE_PICK,
|
|
61
|
+
S_REASONING_EFFORT,
|
|
61
62
|
S_PR_TEMPLATE_SCOPE,
|
|
62
63
|
S_RELATED_TASKS,
|
|
63
64
|
S_RELATED_TASKS_PICK,
|
|
@@ -169,6 +170,9 @@ from .steps_plan import (
|
|
|
169
170
|
_submit_stage_pick,
|
|
170
171
|
)
|
|
171
172
|
from .steps_options import (
|
|
173
|
+
_build_reasoning_effort,
|
|
174
|
+
_reasoning_effort_required,
|
|
175
|
+
_submit_reasoning_effort,
|
|
172
176
|
_report_language_required,
|
|
173
177
|
_build_report_language,
|
|
174
178
|
_submit_report_language,
|
|
@@ -516,6 +520,13 @@ STEPS: list[Step] = [
|
|
|
516
520
|
and S_DIRECTIVE not in s.answered),
|
|
517
521
|
build=_build_directive, submit=_submit_directive,
|
|
518
522
|
owns=("directive", "directive_pending_text")),
|
|
523
|
+
# 역할 화면과 directive 뒤 — 이 run 이 띄울 워커가 정해진 다음에 묻는다.
|
|
524
|
+
Step(S_REASONING_EFFORT,
|
|
525
|
+
applies=lambda s: (S_REASONING_EFFORT not in s.answered
|
|
526
|
+
and not s.directive_pending_text
|
|
527
|
+
and _reasoning_effort_required(s)),
|
|
528
|
+
build=_build_reasoning_effort, submit=_submit_reasoning_effort,
|
|
529
|
+
owns=("reasoning_effort",)),
|
|
519
530
|
Step(S_RELATED_TASKS_PICK,
|
|
520
531
|
applies=lambda s: (_identity_ready(s)
|
|
521
532
|
and S_RELATED_TASKS_PICK not in s.answered),
|
|
@@ -118,6 +118,7 @@ def render_args(state: WizardState) -> dict[str, Any]:
|
|
|
118
118
|
),
|
|
119
119
|
"pr-template-path": pr_template,
|
|
120
120
|
"fix-cycle": state.fix_cycle,
|
|
121
|
+
"reasoning-effort": state.reasoning_effort,
|
|
121
122
|
"host-session-context-json": serialize_host_session_context(
|
|
122
123
|
_host_session_context(state)
|
|
123
124
|
),
|
|
@@ -132,6 +132,8 @@ class WizardState:
|
|
|
132
132
|
# confirm / edit
|
|
133
133
|
# "" | "yes" | "no" — done(release-handoff) task 재진입의 fix-cycle 기록 여부
|
|
134
134
|
fix_cycle: str = ""
|
|
135
|
+
# `REASONING_EFFORTS` 중 하나. 워커가 정해지기 전에는 비어 있다.
|
|
136
|
+
reasoning_effort: str = ""
|
|
135
137
|
confirmed: Optional[bool] = None
|
|
136
138
|
confirmation_stages: str = ""
|
|
137
139
|
confirmation_prompt: str = ""
|
|
@@ -627,6 +629,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
627
629
|
"pr_template_path": "", "pr_template_pending_text": False,
|
|
628
630
|
"pr_template_scope": "",
|
|
629
631
|
"fix_cycle": "",
|
|
632
|
+
"reasoning_effort": "",
|
|
630
633
|
"confirmed": None, "edit_target": "", "confirmation_prompt": "", "confirmation_stages": "",
|
|
631
634
|
"confirmation_scope": {}, "user_authorization": {},
|
|
632
635
|
}
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Callable, Optional
|
|
8
8
|
|
|
9
9
|
from okstra_ctl.clarification_items import user_response_sidecars
|
|
10
|
+
from okstra_ctl.domain.worker_exec import REASONING_EFFORTS
|
|
10
11
|
from okstra_ctl.manager_paths import MANAGER_CONTEXT_DIRECTIVE_PREFIX
|
|
11
12
|
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
12
13
|
from okstra_ctl.report_language import is_language_tag
|
|
@@ -25,6 +26,7 @@ from .ids import (
|
|
|
25
26
|
S_PR_TEMPLATE,
|
|
26
27
|
S_PR_TEMPLATE_PICK,
|
|
27
28
|
S_PR_TEMPLATE_SCOPE,
|
|
29
|
+
S_REASONING_EFFORT,
|
|
28
30
|
S_RELATED_TASKS,
|
|
29
31
|
S_RELATED_TASKS_PICK,
|
|
30
32
|
S_REPORT_LANGUAGE,
|
|
@@ -33,7 +35,14 @@ from .ids import (
|
|
|
33
35
|
_REUSE_LAST_TOKEN,
|
|
34
36
|
_SIBLINGS_TOKEN,
|
|
35
37
|
)
|
|
36
|
-
from .state import
|
|
38
|
+
from .state import (
|
|
39
|
+
Option,
|
|
40
|
+
Prompt,
|
|
41
|
+
WizardError,
|
|
42
|
+
WizardState,
|
|
43
|
+
_identity_ready,
|
|
44
|
+
_load_role_profile_for_state,
|
|
45
|
+
)
|
|
37
46
|
from .prompts import _opt, _p, _pick_snippet
|
|
38
47
|
from .sources import _latest_run_inputs, _require_file, _suggest_latest_final_report
|
|
39
48
|
|
|
@@ -428,3 +437,34 @@ def _submit_pr_template_scope(state: WizardState, value: str) -> Optional[str]:
|
|
|
428
437
|
)
|
|
429
438
|
state.pr_template_scope = value
|
|
430
439
|
return f"pr-template-scope: {value}"
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
_RECOMMENDED_REASONING_EFFORT = "high"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _reasoning_effort_required(state: WizardState) -> bool:
|
|
446
|
+
"""워커를 띄우지 않는 프로필(release-handoff)에는 강도를 넘길 CLI 가 없다."""
|
|
447
|
+
return _identity_ready(state) and bool(_load_role_profile_for_state(state).roles)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _build_reasoning_effort(state: WizardState) -> Prompt:
|
|
451
|
+
t = _p(state.workspace_root, "reasoning_effort")
|
|
452
|
+
others = [level for level in REASONING_EFFORTS if level != _RECOMMENDED_REASONING_EFFORT]
|
|
453
|
+
return Prompt(
|
|
454
|
+
step=S_REASONING_EFFORT, kind="pick", label=t["label"],
|
|
455
|
+
options=[
|
|
456
|
+
_opt(_RECOMMENDED_REASONING_EFFORT,
|
|
457
|
+
t["options"][_RECOMMENDED_REASONING_EFFORT], recommended=True),
|
|
458
|
+
*(_opt(level, t["options"][level]) for level in others),
|
|
459
|
+
],
|
|
460
|
+
echo_template=t["echo_template"])
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _submit_reasoning_effort(state: WizardState, value: str) -> Optional[str]:
|
|
464
|
+
level = value.strip().lower()
|
|
465
|
+
if level not in REASONING_EFFORTS:
|
|
466
|
+
raise WizardError(
|
|
467
|
+
f"expected one of {', '.join(REASONING_EFFORTS)}, got: {value!r}"
|
|
468
|
+
)
|
|
469
|
+
state.reasoning_effort = level
|
|
470
|
+
return f"reasoning-effort: {level}"
|
|
@@ -45,10 +45,19 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
45
45
|
_print_json(_backend_payload(backend_plan, dry_run=True))
|
|
46
46
|
return 0
|
|
47
47
|
code, lines = _dispatch_recording_checkpoints(backend_plan)
|
|
48
|
+
from .phases.implementation_planning.round_completion import complete_ready_plan_round
|
|
49
|
+
from .worker_prompt_policy import is_plan_verify_dispatch_kind
|
|
50
|
+
|
|
51
|
+
completion = complete_ready_plan_round(backend_plan) if code == 0 and any(
|
|
52
|
+
is_plan_verify_dispatch_kind(job.dispatch_kind) for job in backend_plan.jobs
|
|
53
|
+
) else None
|
|
54
|
+
if completion and completion["status"] == "failed":
|
|
55
|
+
code = 2
|
|
48
56
|
_print_json({
|
|
49
57
|
**_backend_payload(backend_plan, dry_run=False),
|
|
50
58
|
"exitCode": code,
|
|
51
59
|
"progressLines": lines,
|
|
60
|
+
**({"planVerification": completion} if completion else {}),
|
|
52
61
|
})
|
|
53
62
|
return code
|
|
54
63
|
except DispatchError as exc:
|
|
@@ -12,7 +12,11 @@ import os
|
|
|
12
12
|
import subprocess
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
-
from .domain.worker_exec import
|
|
15
|
+
from .domain.worker_exec import (
|
|
16
|
+
REASONING_EFFORTS,
|
|
17
|
+
ExecutionPolicy,
|
|
18
|
+
WorkerExecRequest,
|
|
19
|
+
)
|
|
16
20
|
from .domain.worker_role import role_spec
|
|
17
21
|
from .write_policy import WriteEnforcement, WritePolicy
|
|
18
22
|
|
|
@@ -39,6 +43,7 @@ def build_request(
|
|
|
39
43
|
session_id: str = "",
|
|
40
44
|
write_policy: WritePolicy | None = None,
|
|
41
45
|
write_enforcement: WriteEnforcement | None = None,
|
|
46
|
+
reasoning_effort: str = "",
|
|
42
47
|
) -> WorkerExecRequest:
|
|
43
48
|
"""One dispatch, with every value the strategies will not re-derive.
|
|
44
49
|
|
|
@@ -46,6 +51,11 @@ def build_request(
|
|
|
46
51
|
strategies hand what they are given straight to a CLI, so an unresolved
|
|
47
52
|
`..` or symlink would survive into the sandbox boundary itself.
|
|
48
53
|
"""
|
|
54
|
+
if reasoning_effort and reasoning_effort not in REASONING_EFFORTS:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"reasoning effort must be one of {', '.join(REASONING_EFFORTS)}: "
|
|
57
|
+
f"{reasoning_effort!r}"
|
|
58
|
+
)
|
|
49
59
|
root = project_root.resolve()
|
|
50
60
|
worktree = worktree_path.resolve() if worktree_path is not None else None
|
|
51
61
|
return WorkerExecRequest(
|
|
@@ -64,6 +74,7 @@ def build_request(
|
|
|
64
74
|
),
|
|
65
75
|
idle_timeout_seconds=idle_timeout_seconds,
|
|
66
76
|
session_id=session_id,
|
|
77
|
+
reasoning_effort=reasoning_effort,
|
|
67
78
|
)
|
|
68
79
|
|
|
69
80
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"schemaVersion": {"const": "2.0"},
|
|
19
19
|
"executionIdentityVersion": {"const": 2},
|
|
20
20
|
"entryMode": {"enum": ["current-session", "new-session"]},
|
|
21
|
+
"reasoningEffort": {"enum": ["", "low", "medium", "high", "xhigh"]},
|
|
21
22
|
"participantAssignments": {
|
|
22
23
|
"type": "array",
|
|
23
24
|
"items": {"$ref": "#/$defs/ParticipantAssignment"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Run one
|
|
1
|
+
Run one shell-command tool call, starting with the literal token `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` — literal command tokens keep execution permissions reviewable):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
For authored questions, `prompt_user` means the current host's available question interface, not a literal tool name. Use the selected host relay when one is provided. Preserve every question, option, description, and answer; use native controls only when the live session permits them and the complete interaction fits its limits. Otherwise show complete numbered choices and accept the user's reply; collect free text through normal conversation. If a native control is refused, follow the relay's recovery mapping, or use text when no relay is provided. A delivery acknowledgement or preselected option is not an answer; wait for the user's actual response.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Resolve `<host-runtime>` from the launcher's `OKSTRA_RUNTIME_HOST` when present; otherwise use the registered host ID declared by the current harness. Do not infer the host from worker models, installed executables, or `PATH`. If neither source identifies the host, report that it is unknown instead of substituting another host. Keep the same resolved host when retrying against another project directory.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
**
|
|
1
|
+
**Shell invocation rule (permission-friendly)**: every shell command in this skill begins with the literal token `okstra` (or another already-allowed binary) and passes literal argument values. Do not introduce shell variables (`$STATE_FILE`, `$ANSWER`, `$projectRoot`, ...), `$(...)` command substitution, or leading `VAR=...` assignments — any of those make the command harder to audit; apply the selected host adapter's execution-permission guidance. When a prior tool call emitted a path or value, read it from the tool output and paste the literal string into the next command.
|
|
@@ -6,7 +6,7 @@ Record the semantic functions that the live harness can actually perform. Always
|
|
|
6
6
|
|
|
7
7
|
The host-native provider runs as the current lead session when its role is supported. Other selected providers run through their registered CLI wrappers. Claude remains only the model-policy default; it is not the process owner.
|
|
8
8
|
|
|
9
|
-
Run one
|
|
9
|
+
Run one shell-command tool call (Shell invocation rule from the top of this file applies):
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
okstra preflight --runtime <host-runtime>
|
|
@@ -29,9 +29,9 @@ okstra config set pr-template-path "<value>" --scope global
|
|
|
29
29
|
|
|
30
30
|
If an action has an unknown `command`, `key`, or `scope`, stop and report the wizard output instead of inventing a command.
|
|
31
31
|
|
|
32
|
-
Before rendering the next phase's bundle — and between worker rounds within a phase (reverify/critic/gapverify batches), after you have collected that round's results and token usage and before you dispatch the next round — close the panes of the dispatches that finished in the prior round so they do not accumulate: `okstra team reclaim --project-root <projectRoot> --run-manifest <RUN_MANIFEST_PATH>` closes them, records `phase-batch-cleanup panes=<n>` with the number it closed, and prints that `PROGRESS:` line last — emit it as printed and do not call `okstra lead-progress append` for it. The command reads each dispatch's recorded status, so an in-progress worker keeps its pane whichever moment you call it. It closes only the panes okstra opened and recorded — a pane the harness opened for its own teammate carries no recorded id and is not okstra's to close.
|
|
32
|
+
Before rendering the next phase's bundle — and between worker rounds within a phase (reverify/critic/gapverify batches), after you have collected that round's results and token usage and before you dispatch the next round — close the panes of the dispatches that finished in the prior round so they do not accumulate: `okstra team reclaim --project-root <projectRoot> --run-manifest <RUN_MANIFEST_PATH>` closes them, records `phase-batch-cleanup panes=<n>` with the number it closed, and prints that `PROGRESS:` line last — emit it as printed and do not call `okstra lead-progress append` for it. The command reads each dispatch's recorded status, so an in-progress worker keeps its pane whichever moment you call it. It closes only the panes okstra opened and recorded — a pane the harness opened for its own teammate carries no recorded id and is not okstra's to close. After collecting results and token usage, apply the selected adapter's `shutdown_workers` operation only to confirmed-complete workers; preserve in-flight workers and the lead. A `cli-wrapper` run holds no pane at all and `team reclaim` refuses it, so record the checkpoint there with `okstra lead-progress append … --phase phase-batch-cleanup --field panes=0`.
|
|
33
33
|
|
|
34
|
-
Before you ask the user for any approval, clarification, or decision after workers have been dispatched, run `okstra team reclaim … --gate` first: it closes the finished panes and prints `PROGRESS: phase-gate-cleanup panes=<n>` for you to emit, without recording a batch cleanup. Then
|
|
34
|
+
Before you ask the user for any approval, clarification, or decision after workers have been dispatched, run `okstra team reclaim … --gate` first: it closes the finished panes and prints `PROGRESS: phase-gate-cleanup panes=<n>` for you to emit, without recording a batch cleanup. Then apply the selected adapter's `shutdown_workers` operation to confirmed-complete workers after collecting their results and token usage; preserve in-flight workers and the lead. This keeps a user gate from being shown while finished worker panes remain; in-progress dispatches keep their panes. Then follow `prompts/lead/okstra-lead-contract.md` "User confirmation before an approval blocker": read cited plan items, worker findings, and files before asking, and ask in the user's language with each option's outcome.
|
|
35
35
|
|
|
36
36
|
Build the `okstra render-bundle` invocation from `outcome.renderArgv`, passing every token verbatim and in order (including empty strings — they are intentional `use phase default` markers).
|
|
37
37
|
|
|
@@ -44,13 +44,13 @@ Step 3's empty-answer and escaping rules apply verbatim: every flag in `renderAr
|
|
|
44
44
|
`renderArgv` already contains exactly one `--lead-runtime <host-runtime>` pair. Do not add or replace it. Do not enumerate a fixed provider list in this skill because the wizard and role model pool own the ordered tokens.
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
okstra render-bundle \
|
|
47
|
+
okstra render-bundle --summary-only \
|
|
48
48
|
<outcome.renderArgv token 1> \
|
|
49
49
|
<outcome.renderArgv token 2> \
|
|
50
50
|
<each remaining outcome.renderArgv token in order>
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
`render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, `okstra run manifest:`, and
|
|
53
|
+
`render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, `okstra run manifest:`, and other preparation status. `--summary-only` omits the lead prompt body from stdout; Step 6 reads the verified saved prompt from the manifest path. Parse the labelled lines for `TASK_ROOT`, `INSTRUCTION_SET_PATH`, and `RUN_MANIFEST_PATH`. Also watch for an optional `okstra concurrent-run stages:` label line — present only when a concurrent run is detected (see "Concurrent-run detection branch" below).
|
|
54
54
|
|
|
55
55
|
Before acting as the lead, read `resources.leadPromptMetadataPath` from the run
|
|
56
56
|
manifest and execute the host-native specification-link gate:
|