okstra 0.209.0 → 0.209.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/docs/architecture.md +1 -1
- package/docs/cli.md +9 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/duties/business-flow-investigator.json +1 -1
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
- package/runtime/prompts/wizard/prompts.ko.json +15 -14
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +15 -4
- package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +7 -3
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +8 -4
- package/runtime/python/okstra_ctl/agent/activity.py +2 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +7 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +2 -2
- package/runtime/python/okstra_ctl/business_flow/source.py +35 -2
- package/runtime/python/okstra_ctl/cmux.py +93 -25
- package/runtime/python/okstra_ctl/convergence.py +29 -10
- package/runtime/python/okstra_ctl/dispatch_checkpoints.py +93 -3
- package/runtime/python/okstra_ctl/dispatch_core.py +6 -1
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +16 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +70 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +21 -16
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +11 -9
- package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +79 -17
- package/runtime/python/okstra_ctl/plan_items.py +4 -2
- package/runtime/python/okstra_ctl/render.py +4 -0
- package/runtime/python/okstra_ctl/report_finalize.py +19 -7
- package/runtime/python/okstra_ctl/run.py +15 -1
- package/runtime/python/okstra_ctl/team.py +6 -4
- package/runtime/python/okstra_ctl/user_response.py +22 -0
- package/runtime/python/okstra_ctl/wizard/cli.py +0 -2
- package/runtime/python/okstra_ctl/wizard/engine.py +33 -46
- package/runtime/python/okstra_ctl/wizard/registry.py +2 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +2 -8
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +7 -5
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +1 -1
- package/runtime/python/okstra_ctl/worker_prompt_body.py +6 -1
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +13 -0
- package/runtime/python/okstra_ctl/write_policy.py +16 -0
- package/runtime/skills/okstra-run/SKILL.md +3 -3
- package/runtime/validators/validate-run.py +9 -2
|
@@ -278,7 +278,7 @@ def _single_model_options(
|
|
|
278
278
|
"""고정 단일 역할의 후보를 제공자별로 묶고 기존 추천을 유지한다."""
|
|
279
279
|
pool = context.pool
|
|
280
280
|
requirement = next(row for row in profile.roles if row.role == role)
|
|
281
|
-
|
|
281
|
+
_defaults, everything = _available_role_models(
|
|
282
282
|
state, profile, requirement, context, scopes,
|
|
283
283
|
)
|
|
284
284
|
prompt = _p(state.workspace_root, "role_model", role=role)
|
|
@@ -297,13 +297,7 @@ def _single_model_options(
|
|
|
297
297
|
options.append(_opt(
|
|
298
298
|
model_ref,
|
|
299
299
|
prompt["options"]["model"].format(
|
|
300
|
-
model_ref=model_ref,
|
|
301
|
-
display=model.display_name,
|
|
302
|
-
default_suffix=(
|
|
303
|
-
prompt["options"].get("default_suffix", "")
|
|
304
|
-
if model_ref in defaults
|
|
305
|
-
else ""
|
|
306
|
-
),
|
|
300
|
+
model_ref=model_ref, display=model.display_name,
|
|
307
301
|
),
|
|
308
302
|
recommended=not options,
|
|
309
303
|
))
|
|
@@ -27,6 +27,7 @@ from okstra_project.state import (
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
from .ids import (
|
|
30
|
+
BRIEF_CARRY_SWITCH_ENTRY,
|
|
30
31
|
BASE_REF_FREE_INPUT_TOKEN,
|
|
31
32
|
CANONICAL_BASE_REFS,
|
|
32
33
|
PICK_TYPE_CUSTOM,
|
|
@@ -559,7 +560,7 @@ def _implementation_needs_approval(
|
|
|
559
560
|
|
|
560
561
|
def _build_task_type(state: WizardState) -> Prompt:
|
|
561
562
|
t = _p(state.workspace_root, "task_type")
|
|
562
|
-
recommended_suffix =
|
|
563
|
+
recommended_suffix = ""
|
|
563
564
|
rerun_suffix = t["options"].get("_RERUN_SUFFIX", "")
|
|
564
565
|
next_suffix = t["options"].get("_NEXT_SUFFIX", "")
|
|
565
566
|
approve_suffix = t["options"].get("_APPROVE_SUFFIX", recommended_suffix)
|
|
@@ -826,7 +827,10 @@ def _build_brief_carry(state: WizardState) -> Prompt:
|
|
|
826
827
|
return Prompt(
|
|
827
828
|
step=S_BRIEF_CARRY, kind="pick",
|
|
828
829
|
label=t["label"],
|
|
829
|
-
options=[
|
|
830
|
+
options=[
|
|
831
|
+
_opt(k, v, recommended=k == BRIEF_CARRY_SWITCH_ENTRY)
|
|
832
|
+
for k, v in t["options"].items()
|
|
833
|
+
],
|
|
830
834
|
echo_template=t["echo_template"],
|
|
831
835
|
)
|
|
832
836
|
|
|
@@ -851,11 +855,9 @@ def _checked_out_canonical_ref(project_root: Path) -> str:
|
|
|
851
855
|
|
|
852
856
|
def _build_base_ref_pick(state: WizardState) -> Prompt:
|
|
853
857
|
t = _p(state.workspace_root, "base_ref_pick")
|
|
854
|
-
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
855
858
|
recommended = _checked_out_canonical_ref(Path(state.project_root))
|
|
856
859
|
refs = sorted(CANONICAL_BASE_REFS, key=lambda r: r != recommended)
|
|
857
|
-
options = [_opt(r,
|
|
858
|
-
else _opt(r) for r in refs]
|
|
860
|
+
options = [_opt(r, recommended=r == recommended) for r in refs]
|
|
859
861
|
for value, label in _static_options(t):
|
|
860
862
|
options.append(_opt(value=value, label=label))
|
|
861
863
|
return Prompt(
|
|
@@ -689,7 +689,7 @@ def _build_fix_cycle_confirm(state: WizardState) -> Prompt:
|
|
|
689
689
|
return Prompt(
|
|
690
690
|
step=S_FIX_CYCLE_CONFIRM, kind="pick", label=t["label"],
|
|
691
691
|
options=[
|
|
692
|
-
_opt("yes", opts["yes"]),
|
|
692
|
+
_opt("yes", opts["yes"], recommended=True),
|
|
693
693
|
_opt("no", opts["no"]),
|
|
694
694
|
_opt("abort", opts["abort"]),
|
|
695
695
|
],
|
|
@@ -23,6 +23,7 @@ def analysis_prompt_body(
|
|
|
23
23
|
model: str,
|
|
24
24
|
plan: PromptPlan,
|
|
25
25
|
approved_plan_path: str = "",
|
|
26
|
+
selected_direction_path: str = "",
|
|
26
27
|
) -> list[str]:
|
|
27
28
|
"""Render the provider-neutral body for an initial analysis audience."""
|
|
28
29
|
label = analysis_worker_label(worker_id)
|
|
@@ -41,7 +42,9 @@ def analysis_prompt_body(
|
|
|
41
42
|
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
42
43
|
"",
|
|
43
44
|
"## Inputs",
|
|
44
|
-
*analysis_input_lines(
|
|
45
|
+
*analysis_input_lines(
|
|
46
|
+
manifest, active_context, plan, approved_plan_path, selected_direction_path
|
|
47
|
+
),
|
|
45
48
|
"",
|
|
46
49
|
mcp_pointer_line(),
|
|
47
50
|
"",
|
|
@@ -56,6 +59,7 @@ def analysis_input_lines(
|
|
|
56
59
|
active_context: Mapping[str, Any],
|
|
57
60
|
plan: PromptPlan,
|
|
58
61
|
approved_plan_path: str = "",
|
|
62
|
+
selected_direction_path: str = "",
|
|
59
63
|
) -> list[str]:
|
|
60
64
|
if plan.packet_only:
|
|
61
65
|
packet_path = instruction_path(manifest, active_context, "analysisPacketPath")
|
|
@@ -70,6 +74,7 @@ def analysis_input_lines(
|
|
|
70
74
|
return existing_input_lines((
|
|
71
75
|
("Primary analysis packet", packet_path),
|
|
72
76
|
("Approved plan", approved_plan_path),
|
|
77
|
+
("Selected direction snapshot", selected_direction_path),
|
|
73
78
|
))
|
|
74
79
|
inputs = [
|
|
75
80
|
("Primary analysis packet", instruction_path(manifest, active_context, "analysisPacketPath")),
|
|
@@ -170,6 +170,9 @@ class PromptPlan:
|
|
|
170
170
|
# 기준으로 판정하라고 요구하는 phase 만 참이다 — 없는 phase 에 실으면
|
|
171
171
|
# 열거만 되고 열 파일이 없는 줄이 된다.
|
|
172
172
|
include_approved_plan: bool = False
|
|
173
|
+
# 계획 워커 직무는 `selected-direction.json` 을 먼저 읽으라고 하는데, 읽기
|
|
174
|
+
# 범위는 `## Inputs` 에 열거된 경로뿐이다. 그래서 계획 phase 는 스냅샷을 연다.
|
|
175
|
+
include_selected_direction: bool = False
|
|
173
176
|
|
|
174
177
|
|
|
175
178
|
def resolve_prompt_plan(
|
|
@@ -261,6 +264,14 @@ def resolve_prompt_plan(
|
|
|
261
264
|
packet_only=True,
|
|
262
265
|
required_headers=(GRILLING_LOG_HEADER,),
|
|
263
266
|
)
|
|
267
|
+
if task_type == "implementation-planning":
|
|
268
|
+
return _plan(
|
|
269
|
+
"analysis",
|
|
270
|
+
duty_audience=analysis_duty,
|
|
271
|
+
equality_group=analysis_equality_group,
|
|
272
|
+
packet_only=True,
|
|
273
|
+
include_selected_direction=True,
|
|
274
|
+
)
|
|
264
275
|
return _plan(
|
|
265
276
|
"analysis",
|
|
266
277
|
duty_audience=analysis_duty,
|
|
@@ -328,6 +339,7 @@ def _plan(
|
|
|
328
339
|
equality_group: str | None = None,
|
|
329
340
|
packet_only: bool = False,
|
|
330
341
|
include_approved_plan: bool = False,
|
|
342
|
+
include_selected_direction: bool = False,
|
|
331
343
|
allow_coding_preflight: bool = False,
|
|
332
344
|
required_headers: tuple[str, ...] = (),
|
|
333
345
|
max_body_lines: int | None = None,
|
|
@@ -339,6 +351,7 @@ def _plan(
|
|
|
339
351
|
equality_group=equality_group,
|
|
340
352
|
packet_only=packet_only,
|
|
341
353
|
include_approved_plan=include_approved_plan,
|
|
354
|
+
include_selected_direction=include_selected_direction,
|
|
342
355
|
allow_coding_preflight=allow_coding_preflight,
|
|
343
356
|
required_headers=_worker_facing_headers(audience, required_headers),
|
|
344
357
|
max_body_lines=max_body_lines,
|
|
@@ -31,6 +31,7 @@ from .final_report_paths import final_report_data_path
|
|
|
31
31
|
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
32
32
|
from .path_hints import hydrate_active_run_context
|
|
33
33
|
from .paths import task_conformance_manifest_file, task_qa_dir
|
|
34
|
+
from okstra_project.dirs import OKSTRA_RELATIVE
|
|
34
35
|
|
|
35
36
|
def build_write_policy(
|
|
36
37
|
invocation: Mapping[str, Any],
|
|
@@ -130,6 +131,20 @@ def _role_qa_artifact_paths(role: str, task_root: Path | None) -> tuple[Path, ..
|
|
|
130
131
|
return ()
|
|
131
132
|
|
|
132
133
|
|
|
134
|
+
def _role_project_memory_paths(role: str, project_root: Path) -> tuple[Path, ...]:
|
|
135
|
+
"""The project-memory files an approved plan step orders the executor to write.
|
|
136
|
+
|
|
137
|
+
The implementation-planning profile makes every decision draft a step that
|
|
138
|
+
creates `.okstra/decisions/<NNNN>-<slug>.md`, and a glossary proposal a step
|
|
139
|
+
that updates `.okstra/glossary.md`. Both sit outside the task root, so no
|
|
140
|
+
other grant reaches them and `_planned_source_paths` refused the dispatch.
|
|
141
|
+
"""
|
|
142
|
+
if role != "implementer":
|
|
143
|
+
return ()
|
|
144
|
+
okstra = project_root / OKSTRA_RELATIVE
|
|
145
|
+
return (okstra / "decisions", okstra / "glossary.md")
|
|
146
|
+
|
|
147
|
+
|
|
133
148
|
def _inherited_qa_evidence(
|
|
134
149
|
role: str, task_type: str, task_root: Path | None,
|
|
135
150
|
) -> tuple[Path, ...]:
|
|
@@ -212,6 +227,7 @@ def build_invocation_write_contract(
|
|
|
212
227
|
*artifact_paths,
|
|
213
228
|
*_technical_experiment_paths(artifact_paths, task_root),
|
|
214
229
|
*_role_qa_artifact_paths(role, task_root),
|
|
230
|
+
*_role_project_memory_paths(role, root),
|
|
215
231
|
)
|
|
216
232
|
)
|
|
217
233
|
)
|
|
@@ -59,9 +59,9 @@ Submit the answer shape required by `interaction.answerProtocol`; do not add nor
|
|
|
59
59
|
|
|
60
60
|
The final `confirm` step is a normal `pick` step with three options — `Proceed` / `Edit` / `Abort`(abort) — and is rendered the same way (no special handling). `Edit` rewinds to any earlier step (including `base-ref`); `Abort` terminally cancels the wizard. The branch/worktree decision the run will actually use (for `implementation`, the **stage worktree** — not the task-key directory) is folded into the Step 4 confirmation summary block as a `worktree` line, so there is no separate branch-confirm prompt.
|
|
61
61
|
|
|
62
|
-
Never invent additional questions. **Never drop, hide, merge, reorder, or truncate** a `pick` / `pick_group` option — relay every `options[]` entry, including
|
|
62
|
+
Never invent additional questions. **Never drop, hide, merge, reorder, or truncate** a `pick` / `pick_group` option — relay every `options[]` entry, including the flagged recommendation. Do not collapse a multi-option pick into a "recommended + Enter directly / Other" shortlist. The wizard's arrays are the complete authoritative choice sets, regardless of the current host UI's usual option limit. The run-prompt recommendation rule (1–2 recommendations + Enter directly) shapes the **option set** only for prompts this skill authors itself, never for wizard-provided options — you may not add, drop, or reorder a wizard option to produce a shortlist. It does not excuse you from recommending: before relaying a wizard step whose answer turns on something readable (the carried report, the sidecar the user already wrote, the prior Stage Map), read it, put what you found in the question body, and name which of the wizard's own options you recommend and why. Relaying a step with no context and no recommendation hands the whole question back to the user — see the lifecycle core contract "Asking the user (BLOCKING)".
|
|
63
63
|
|
|
64
|
-
**One recommendation, shown in one place, and it is option 1.** The option carrying `recommended: true` is what this run computed, and the wizard already placed it first — except on model-selection steps (`role-models:*`, `role-model:*`), which keep provider display order, so the flagged model may sit anywhere in the list. Append ` (추천)` to that option's label when you render it, and to no other. A checkbox step (`multi: true`) may flag several leading options: they are the recommended set, rendered the same way. Your prose recommendation names one of the flagged options. If you believe a different option is right, do not quietly recommend it in prose while the flagged one still reads as recommended on screen: say you disagree, name both, and let the user pick. When no option carries the flag, the run computed nothing for this step — recommend one from what you read and mark that one, and do not present the first option as a default just because it is first. **Enforced:** `scripts/okstra_ctl/wizard/state.py` `Prompt.__post_init__` refuses a step whose free-input option is not last, whose single-select recommendation is not exactly one option, whose recommendation is not placed first on a single-select step or is not the leading run on a checkbox step (both position checks are skipped on `role-models:*` / `role-model:*` steps), or that marks the free-input / abort escape as a recommendation.
|
|
64
|
+
**One recommendation, shown in one place, and it is option 1.** Wizard labels carry no recommendation marker of their own. The option carrying `recommended: true` is what this run computed, and the wizard already placed it first — except on model-selection steps (`role-models:*`, `role-model:*`), which keep provider display order, so the flagged model may sit anywhere in the list. Append ` (추천)` to that option's label when you render it, and to no other. A checkbox step (`multi: true`) may flag several leading options: they are the recommended set, rendered the same way. Your prose recommendation names one of the flagged options. If you believe a different option is right, do not quietly recommend it in prose while the flagged one still reads as recommended on screen: say you disagree, name both, and let the user pick. When no option carries the flag, the run computed nothing for this step — recommend one from what you read and mark that one, and do not present the first option as a default just because it is first. **Enforced:** `scripts/okstra_ctl/wizard/state.py` `Prompt.__post_init__` refuses a step whose free-input option is not last, whose single-select recommendation is not exactly one option, whose recommendation is not placed first on a single-select step or is not the leading run on a checkbox step (both position checks are skipped on `role-models:*` / `role-model:*` steps), or that marks the free-input / abort escape as a recommendation.
|
|
65
65
|
|
|
66
66
|
## Step 1: Preflight
|
|
67
67
|
|
|
@@ -470,7 +470,7 @@ When the user has to choose an implementation direction — a finished `implemen
|
|
|
470
470
|
~/.okstra/bin/okstra option-comparison --report <final-report-implementation-option-selection-NNN.data.json>
|
|
471
471
|
```
|
|
472
472
|
|
|
473
|
-
At `selected_direction_pick` the
|
|
473
|
+
At `selected_direction_pick` the option value is the report path, or `<report path>#<option id>` for a report still awaiting a direction — pass the part before `#` to `--report`. Picking a `#` value records that direction in the report's `user-responses/` sidecar. The command prints JSON; every number in the reply comes from it. Use the recorded `weightedScore` as printed; do not recompute it. Write the reply in the user's language, in this order:
|
|
474
474
|
|
|
475
475
|
1. **Lead-in** — one or two sentences on what the directions share and the axes they split on, drawn from `narrative.comparisonOverview` and each option's `coreMechanism`.
|
|
476
476
|
2. **Score table** — a Markdown table: one row per `criteria[]` entry in order, a weight column, one column per option (`<id> <short name>`), and a last row with each option's `weightedScore`. Write each criterion as its identifier with a translation in the user's language, e.g. `requirement-fit(요구사항 부합)`.
|
|
@@ -5660,14 +5660,21 @@ def run_plan_body_inputs(narrative_path: Path, state_path: Path) -> int:
|
|
|
5660
5660
|
print("validate-run: narrative has no implementationPlanning", file=sys.stderr)
|
|
5661
5661
|
return 2
|
|
5662
5662
|
data = {**narrative, "schemaVersion": "3.0"}
|
|
5663
|
-
data["implementationPlanning"] = {**planning, "planBodyVerification": verification}
|
|
5663
|
+
data["implementationPlanning"] = {**planning, "planBodyVerification": dict(verification)}
|
|
5664
|
+
gate = plan_body_gate_summary(data)
|
|
5665
|
+
if gate is not None:
|
|
5666
|
+
# 상태의 gate 값은 complete-round 가 같은 재계산으로 쓴다. 그 전에 도는
|
|
5667
|
+
# plan-verify 에서 선언값을 대조하면 seed 값과의 시차만 잡힌다.
|
|
5668
|
+
data["implementationPlanning"]["planBodyVerification"].update(
|
|
5669
|
+
gateResult=gate["recomputed"], gateBlockedBy=gate["blockedBy"]
|
|
5670
|
+
)
|
|
5664
5671
|
failures: list[str] = []
|
|
5665
5672
|
warnings = validate_plan_body_section(data, _report_path_for_state(state_path), failures)
|
|
5666
5673
|
failures, advisories = _partition_round_failures(failures)
|
|
5667
5674
|
payload = {
|
|
5668
5675
|
"ok": not failures,
|
|
5669
5676
|
"section": SECTION_PLAN_BODY,
|
|
5670
|
-
"gate":
|
|
5677
|
+
"gate": gate,
|
|
5671
5678
|
"failures": failures,
|
|
5672
5679
|
"advisories": advisories,
|
|
5673
5680
|
"warnings": warnings,
|