okstra 0.207.1 → 0.209.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 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +1 -1
- package/dist/commands/lifecycle/doctor.mjs +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +30 -9
- package/docs/cli.md +26 -22
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +15 -8
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/operations/explain-flow.json +6 -0
- package/runtime/bin/lib/okstra/cli.sh +1 -5
- package/runtime/bin/lib/okstra/globals.sh +0 -2
- package/runtime/bin/lib/okstra/usage.sh +5 -3
- package/runtime/bin/okstra.sh +0 -2
- package/runtime/prompts/duties/business-flow-investigator.json +14 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +22 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +12 -17
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +0 -91
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
- package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
- package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
- package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
- package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
- package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
- package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
- package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
- package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
- package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
- package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
- package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
- package/runtime/python/okstra_ctl/cmux.py +21 -16
- package/runtime/python/okstra_ctl/convergence.py +173 -2
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
- package/runtime/python/okstra_ctl/coverage_census.py +603 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
- package/runtime/python/okstra_ctl/direct_work.py +1 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
- package/runtime/python/okstra_ctl/doctor.py +12 -6
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/group_context.py +5 -4
- package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
- package/runtime/python/okstra_ctl/manager_split.py +4 -1
- package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
- package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
- package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +14 -8
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
- package/runtime/python/okstra_ctl/plan_items.py +1 -1
- package/runtime/python/okstra_ctl/render.py +10 -43
- package/runtime/python/okstra_ctl/render_final_report.py +3 -0
- package/runtime/python/okstra_ctl/report_assembly.py +15 -1
- package/runtime/python/okstra_ctl/report_finalize.py +50 -0
- package/runtime/python/okstra_ctl/report_html/render.py +3 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
- package/runtime/python/okstra_ctl/run.py +78 -409
- package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
- package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
- package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
- package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
- package/runtime/python/okstra_ctl/wizard/render.py +2 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
- package/runtime/python/okstra_ctl/wizard/state.py +36 -145
- package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
- package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
- package/runtime/schemas/business-flow-v1.schema.json +847 -0
- package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
- package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
- package/runtime/skills/okstra-inspect/facets/history.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/templates/reports/business-flow.template.md +106 -0
- package/runtime/templates/reports/html/base.template.html +14 -1
- package/runtime/templates/reports/html/business-flow.template.html +31 -0
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/worker-prompt-preamble.md +11 -2
- package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
- package/runtime/validators/validate-run.py +70 -21
- package/runtime/validators/validate_analysis_report.py +21 -21
- package/runtime/python/okstra_ctl/workers.py +0 -133
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""위저드 상태 — `WizardState`·`Prompt`·`Option`·`Step` 데이터 타입,
|
|
1
|
+
"""위저드 상태 — `WizardState`·`Prompt`·`Option`·`Step` 데이터 타입, 직전 run 입력의 역할 선택 변환, 상태 술어."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from dataclasses import asdict, dataclass, field
|
|
@@ -30,8 +30,7 @@ from .ids import (
|
|
|
30
30
|
|
|
31
31
|
@dataclass
|
|
32
32
|
class WizardState:
|
|
33
|
-
#
|
|
34
|
-
execution_identity_version: int = 1
|
|
33
|
+
# role-first canonical model selections
|
|
35
34
|
role_counts: dict[str, int] = field(default_factory=dict)
|
|
36
35
|
role_models: dict[str, list[str]] = field(default_factory=dict)
|
|
37
36
|
role_selection_order: list[str] = field(default_factory=list)
|
|
@@ -61,8 +60,6 @@ class WizardState:
|
|
|
61
60
|
|
|
62
61
|
# task-type + dependents
|
|
63
62
|
task_type: str = ""
|
|
64
|
-
profile_workers: list[str] = field(default_factory=list)
|
|
65
|
-
profile_optional_workers: list[str] = field(default_factory=list)
|
|
66
63
|
|
|
67
64
|
# brief
|
|
68
65
|
keep_existing_brief: Optional[bool] = None
|
|
@@ -100,9 +97,6 @@ class WizardState:
|
|
|
100
97
|
design_prep_notes: str = ""
|
|
101
98
|
selected_stage: str = "auto"
|
|
102
99
|
selected_stages: str = "" # implementation 다중선택 위상정렬 CSV
|
|
103
|
-
executor: str = ""
|
|
104
|
-
critic: str = ""
|
|
105
|
-
critic_pending_text: bool = False
|
|
106
100
|
|
|
107
101
|
# release-handoff: PR 을 열 stage 들. stage 하나가 PR 하나다.
|
|
108
102
|
handoff_stages: str = "" # csv ("2,3")
|
|
@@ -111,18 +105,6 @@ class WizardState:
|
|
|
111
105
|
reuse_previous: Optional[bool] = None
|
|
112
106
|
|
|
113
107
|
# customize
|
|
114
|
-
use_defaults: Optional[bool] = None
|
|
115
|
-
workers_override: str = ""
|
|
116
|
-
workers_custom_pending: bool = False
|
|
117
|
-
lead_provider: str = ""
|
|
118
|
-
lead_model: str = ""
|
|
119
|
-
claude_model: str = ""
|
|
120
|
-
codex_model: str = ""
|
|
121
|
-
antigravity_model: str = ""
|
|
122
|
-
grok_model: str = ""
|
|
123
|
-
kimi_model: str = ""
|
|
124
|
-
report_writer_provider: str = ""
|
|
125
|
-
report_writer_model: str = ""
|
|
126
108
|
directive: str = ""
|
|
127
109
|
directive_pending_text: bool = False
|
|
128
110
|
last_directive_cached: str = ""
|
|
@@ -159,13 +141,10 @@ class WizardState:
|
|
|
159
141
|
|
|
160
142
|
def to_json(self) -> dict[str, Any]:
|
|
161
143
|
payload = asdict(self)
|
|
162
|
-
payload
|
|
163
|
-
payload["executionIdentityVersion"] = 2
|
|
144
|
+
payload["executionIdentityVersion"] = WIZARD_STATE_VERSION
|
|
164
145
|
payload["roleCounts"] = payload.pop("role_counts")
|
|
165
146
|
payload["roleModels"] = payload.pop("role_models")
|
|
166
147
|
payload["roleSelectionOrder"] = payload.pop("role_selection_order")
|
|
167
|
-
for field_name in _V1_PROVIDER_STATE_FIELDS:
|
|
168
|
-
payload.pop(field_name, None)
|
|
169
148
|
return payload
|
|
170
149
|
|
|
171
150
|
@classmethod
|
|
@@ -314,6 +293,9 @@ def _profile_path(workspace_root: Path, task_type: str) -> Path:
|
|
|
314
293
|
raise WizardError(str(exc)) from exc
|
|
315
294
|
|
|
316
295
|
|
|
296
|
+
WIZARD_STATE_VERSION = 2
|
|
297
|
+
|
|
298
|
+
|
|
317
299
|
_V2_STATE_FIELDS = (
|
|
318
300
|
"roleCounts",
|
|
319
301
|
"roleModels",
|
|
@@ -321,37 +303,6 @@ _V2_STATE_FIELDS = (
|
|
|
321
303
|
)
|
|
322
304
|
|
|
323
305
|
|
|
324
|
-
_V1_PROVIDER_STATE_FIELDS = (
|
|
325
|
-
"workers_override",
|
|
326
|
-
"lead_provider",
|
|
327
|
-
"lead_model",
|
|
328
|
-
"claude_model",
|
|
329
|
-
"codex_model",
|
|
330
|
-
"antigravity_model",
|
|
331
|
-
"grok_model",
|
|
332
|
-
"kimi_model",
|
|
333
|
-
"report_writer_provider",
|
|
334
|
-
"report_writer_model",
|
|
335
|
-
"executor",
|
|
336
|
-
"critic",
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
_V1_PROVIDER_STATE_JSON_FIELDS = frozenset({
|
|
341
|
-
*_V1_PROVIDER_STATE_FIELDS,
|
|
342
|
-
"workers",
|
|
343
|
-
"leadProvider",
|
|
344
|
-
"leadModel",
|
|
345
|
-
"claudeModel",
|
|
346
|
-
"codexModel",
|
|
347
|
-
"antigravityModel",
|
|
348
|
-
"grokModel",
|
|
349
|
-
"kimiModel",
|
|
350
|
-
"reportWriterProvider",
|
|
351
|
-
"reportWriterModel",
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
|
|
355
306
|
def _load_role_profile_for_state(state: WizardState) -> RoleProfile:
|
|
356
307
|
try:
|
|
357
308
|
return load_role_profile(
|
|
@@ -361,22 +312,12 @@ def _load_role_profile_for_state(state: WizardState) -> RoleProfile:
|
|
|
361
312
|
raise WizardError(str(exc)) from exc
|
|
362
313
|
|
|
363
314
|
|
|
364
|
-
def
|
|
365
|
-
|
|
366
|
-
state.execution_identity_version != 2
|
|
367
|
-
or not state.workspace_root
|
|
368
|
-
or not state.task_type
|
|
369
|
-
):
|
|
370
|
-
return False
|
|
371
|
-
try:
|
|
372
|
-
load_role_profile(_profile_path(Path(state.workspace_root), state.task_type))
|
|
373
|
-
except (OSError, RoleProfileError, WizardError):
|
|
374
|
-
return False
|
|
375
|
-
return True
|
|
376
|
-
|
|
315
|
+
def _role_selection_ready(state: WizardState) -> bool:
|
|
316
|
+
"""역할 화면을 만들 수 있을 만큼 task 정체성이 정해졌는가.
|
|
377
317
|
|
|
378
|
-
|
|
379
|
-
|
|
318
|
+
프로필 로드 실패는 여기서 삼키지 않는다 — 로드하는 쪽이 WizardError 로 낸다.
|
|
319
|
+
"""
|
|
320
|
+
return bool(state.workspace_root and state.task_type)
|
|
380
321
|
|
|
381
322
|
|
|
382
323
|
def _legacy_model_ref(
|
|
@@ -404,7 +345,7 @@ def _legacy_model_ref(
|
|
|
404
345
|
|
|
405
346
|
|
|
406
347
|
def _legacy_worker_providers(payload: dict[str, Any]) -> list[str]:
|
|
407
|
-
raw =
|
|
348
|
+
raw = payload.get("workers", "")
|
|
408
349
|
if isinstance(raw, list):
|
|
409
350
|
values = raw
|
|
410
351
|
elif isinstance(raw, str):
|
|
@@ -436,12 +377,11 @@ def _initial_cross_verification_requirement(
|
|
|
436
377
|
), None)
|
|
437
378
|
|
|
438
379
|
|
|
439
|
-
def
|
|
380
|
+
def _role_selections_from_run_inputs(
|
|
440
381
|
state: WizardState,
|
|
441
382
|
payload: dict[str, Any],
|
|
442
383
|
) -> None:
|
|
443
|
-
|
|
444
|
-
return
|
|
384
|
+
"""직전 run-inputs 의 provider·모델 기록을 역할별 모델 선택으로 옮긴다."""
|
|
445
385
|
profile = _load_role_profile_for_state(state)
|
|
446
386
|
pool = ModelPool.from_registry(default_provider_registry())
|
|
447
387
|
cross_requirement = _initial_cross_verification_requirement(profile)
|
|
@@ -461,11 +401,7 @@ def _convert_v1_provider_selections(
|
|
|
461
401
|
for model in pool.default_candidates(cross_requirement.role)
|
|
462
402
|
]
|
|
463
403
|
for ordinal, provider in enumerate(providers[:selected_count], start=1):
|
|
464
|
-
raw_model =
|
|
465
|
-
payload,
|
|
466
|
-
f"{provider}_model",
|
|
467
|
-
f"{provider}Model",
|
|
468
|
-
)
|
|
404
|
+
raw_model = payload.get(f"{provider}Model", "")
|
|
469
405
|
model_ref = _legacy_model_ref(
|
|
470
406
|
pool,
|
|
471
407
|
provider,
|
|
@@ -479,43 +415,17 @@ def _convert_v1_provider_selections(
|
|
|
479
415
|
# 여러 인스턴스 역할의 답은 체크박스 한 장이다.
|
|
480
416
|
state.role_selection_order.append(f"role-models:{cross_requirement.role}")
|
|
481
417
|
|
|
418
|
+
executor = payload.get("executor") or "claude"
|
|
419
|
+
critic = payload.get("critic") or ""
|
|
420
|
+
# 리더는 호스트가 정하므로 옮기지 않는다(`_discard_implicit_leader_selection`).
|
|
482
421
|
provider_rows = (
|
|
483
|
-
(
|
|
484
|
-
"leader",
|
|
485
|
-
_legacy_value(payload, "lead_provider", "leadProvider") or "claude",
|
|
486
|
-
_legacy_value(payload, "lead_model", "leadModel"),
|
|
487
|
-
),
|
|
488
422
|
(
|
|
489
423
|
"report-writer",
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
"report_writer_provider",
|
|
493
|
-
"reportWriterProvider",
|
|
494
|
-
) or "claude",
|
|
495
|
-
_legacy_value(
|
|
496
|
-
payload,
|
|
497
|
-
"report_writer_model",
|
|
498
|
-
"reportWriterModel",
|
|
499
|
-
),
|
|
500
|
-
),
|
|
501
|
-
(
|
|
502
|
-
"implementer",
|
|
503
|
-
_legacy_value(payload, "executor", "executor") or "claude",
|
|
504
|
-
_legacy_value(
|
|
505
|
-
payload,
|
|
506
|
-
f"{_legacy_value(payload, 'executor', 'executor') or 'claude'}_model",
|
|
507
|
-
f"{_legacy_value(payload, 'executor', 'executor') or 'claude'}Model",
|
|
508
|
-
),
|
|
509
|
-
),
|
|
510
|
-
(
|
|
511
|
-
"critic",
|
|
512
|
-
_legacy_value(payload, "critic", "critic"),
|
|
513
|
-
_legacy_value(
|
|
514
|
-
payload,
|
|
515
|
-
f"{_legacy_value(payload, 'critic', 'critic')}_model",
|
|
516
|
-
f"{_legacy_value(payload, 'critic', 'critic')}Model",
|
|
517
|
-
),
|
|
424
|
+
payload.get("reportWriterProvider") or "claude",
|
|
425
|
+
payload.get("reportWriterModel", ""),
|
|
518
426
|
),
|
|
427
|
+
("implementer", executor, payload.get(f"{executor}Model", "")),
|
|
428
|
+
("critic", critic, payload.get(f"{critic}Model", "")),
|
|
519
429
|
)
|
|
520
430
|
static_roles = {
|
|
521
431
|
requirement.role
|
|
@@ -532,34 +442,25 @@ def _convert_v1_provider_selections(
|
|
|
532
442
|
state.role_selection_order.append(f"role-model:{role}:1")
|
|
533
443
|
|
|
534
444
|
|
|
445
|
+
class StaleWizardStateError(WizardError):
|
|
446
|
+
"""저장된 상태가 현재 형식(`WIZARD_STATE_VERSION`)이 아니다 — 이어갈 수 없다."""
|
|
447
|
+
|
|
448
|
+
|
|
535
449
|
def _wizard_state_from_json(payload: dict[str, Any]) -> WizardState:
|
|
536
450
|
if not isinstance(payload, dict):
|
|
537
451
|
raise WizardError("wizard state must be a JSON object")
|
|
538
452
|
data = dict(payload)
|
|
539
453
|
version = data.pop("executionIdentityVersion", None)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
data["role_models"] = data.pop("roleModels")
|
|
549
|
-
data["role_selection_order"] = data.pop("roleSelectionOrder")
|
|
550
|
-
data["execution_identity_version"] = 2
|
|
551
|
-
_validate_v2_state_fields(data)
|
|
552
|
-
elif version in (None, 1):
|
|
553
|
-
if has_v2_fields:
|
|
554
|
-
raise WizardError("mixed wizard state versions are not allowed")
|
|
555
|
-
data.pop("execution_identity_version", None)
|
|
556
|
-
else:
|
|
557
|
-
raise WizardError(f"unsupported wizard state version: {version!r}")
|
|
454
|
+
if version != WIZARD_STATE_VERSION:
|
|
455
|
+
raise StaleWizardStateError(f"unsupported wizard state version: {version!r}")
|
|
456
|
+
if not all(field_name in data for field_name in _V2_STATE_FIELDS):
|
|
457
|
+
raise WizardError("wizard state is missing role selection fields")
|
|
458
|
+
data["role_counts"] = data.pop("roleCounts")
|
|
459
|
+
data["role_models"] = data.pop("roleModels")
|
|
460
|
+
data["role_selection_order"] = data.pop("roleSelectionOrder")
|
|
461
|
+
_validate_v2_state_fields(data)
|
|
558
462
|
allowed = WizardState.__dataclass_fields__.keys()
|
|
559
463
|
state = WizardState(**{key: value for key, value in data.items() if key in allowed})
|
|
560
|
-
if version != 2:
|
|
561
|
-
state.execution_identity_version = 2
|
|
562
|
-
_convert_v1_provider_selections(state, payload)
|
|
563
464
|
_discard_implicit_leader_selection(state)
|
|
564
465
|
return state
|
|
565
466
|
|
|
@@ -632,7 +533,7 @@ class Step:
|
|
|
632
533
|
|
|
633
534
|
|
|
634
535
|
def _identity_ready(s: WizardState) -> bool:
|
|
635
|
-
"""All identity questions (task pick →
|
|
536
|
+
"""All identity questions (task pick → stage/handoff picks) answered."""
|
|
636
537
|
if not s.task_type:
|
|
637
538
|
return False
|
|
638
539
|
# release-handoff 는 brief 가 없다 — prepare 가 검증 보고서 인용 input 을 생성한다.
|
|
@@ -645,9 +546,6 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
645
546
|
if s.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
646
547
|
if not s.approved_plan_path:
|
|
647
548
|
return False
|
|
648
|
-
if s.task_type == "implementation" and not _role_selection_enabled(s):
|
|
649
|
-
if not s.executor:
|
|
650
|
-
return False
|
|
651
549
|
if (s.task_type == "release-handoff"
|
|
652
550
|
and S_HANDOFF_STAGE_PICK not in s.answered):
|
|
653
551
|
return False
|
|
@@ -659,7 +557,6 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
659
557
|
"existing_brief_path": "", "task_type": "",
|
|
660
558
|
"task_group_suggestion": "", "task_id_suggestion": "",
|
|
661
559
|
"task_group_pending_text": False, "task_id_pending_text": False,
|
|
662
|
-
"profile_workers": [], "profile_optional_workers": [],
|
|
663
560
|
"keep_existing_brief": None,
|
|
664
561
|
"brief_path": "", "brief_path_pending_text": False,
|
|
665
562
|
"project_evidence_path": "", "project_evidence_pending_text": False,
|
|
@@ -675,15 +572,9 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
675
572
|
"selected_stage": "auto",
|
|
676
573
|
"selected_stages": "",
|
|
677
574
|
"handoff_stages": "",
|
|
678
|
-
"executor": "", "critic": "", "critic_pending_text": False,
|
|
679
|
-
"execution_identity_version": 1,
|
|
680
575
|
"role_counts": {}, "role_models": {}, "role_selection_order": [],
|
|
681
576
|
"reuse_previous": None,
|
|
682
|
-
"
|
|
683
|
-
"workers_custom_pending": False,
|
|
684
|
-
"lead_provider": "", "lead_model": "", "claude_model": "", "codex_model": "",
|
|
685
|
-
"antigravity_model": "", "grok_model": "", "kimi_model": "",
|
|
686
|
-
"report_writer_provider": "", "report_writer_model": "", "directive": "",
|
|
577
|
+
"directive": "",
|
|
687
578
|
"directive_pending_text": False,
|
|
688
579
|
"related_tasks_raw": "", "related_tasks_pending_text": False,
|
|
689
580
|
"clarification_response_path": "", "clarification_pending_text": False,
|
|
@@ -1,144 +1,43 @@
|
|
|
1
|
-
"""위저드 상태 파일 — `load_state_file`·`save_state_file
|
|
1
|
+
"""위저드 상태 파일 — `load_state_file`·`save_state_file`."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import hashlib
|
|
5
4
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
5
|
|
|
8
6
|
from okstra_ctl.convergence_store import write_json_atomic
|
|
9
|
-
from okstra_ctl.json_boundary import
|
|
7
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
10
8
|
|
|
11
|
-
from .state import
|
|
9
|
+
from .state import StaleWizardStateError, WizardState
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
#
|
|
12
|
+
# 형식이 바뀐 상태 파일에서도 살리는 값 — 위저드를 처음부터 다시 여는 데 필요한 부트스트랩뿐이다.
|
|
13
|
+
_BOOTSTRAP_FIELDS = (
|
|
14
|
+
"workspace_root",
|
|
15
|
+
"project_root",
|
|
16
|
+
"project_id",
|
|
17
|
+
"preferred_task_key",
|
|
18
|
+
"host_runtime",
|
|
19
|
+
"host_entry_mode",
|
|
20
|
+
"available_functions",
|
|
21
|
+
)
|
|
15
22
|
|
|
16
|
-
_LEGACY_SOURCE_FIELD = "legacySource"
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
suffix: int | None = None,
|
|
27
|
-
) -> Path:
|
|
28
|
-
numbered = f".{suffix}" if suffix is not None else ""
|
|
29
|
-
return path.with_name(f"{path.name}.v2.{source_digest}{numbered}.json")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _content_addressed_v2_companion_paths(
|
|
33
|
-
path: Path,
|
|
34
|
-
source_digest: str,
|
|
35
|
-
) -> tuple[Path, ...]:
|
|
36
|
-
canonical = _content_addressed_v2_companion_path(path, source_digest)
|
|
37
|
-
numbered_prefix = f"{path.name}.v2.{source_digest}."
|
|
38
|
-
numbered = tuple(sorted(
|
|
39
|
-
candidate
|
|
40
|
-
for candidate in path.parent.iterdir()
|
|
41
|
-
if candidate.is_file()
|
|
42
|
-
and candidate.name.startswith(numbered_prefix)
|
|
43
|
-
and candidate.name.endswith(".json")
|
|
44
|
-
))
|
|
45
|
-
return (canonical, *numbered)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _available_v2_companion_path(path: Path, source_digest: str) -> Path:
|
|
49
|
-
candidate = _content_addressed_v2_companion_path(path, source_digest)
|
|
50
|
-
suffix = 1
|
|
51
|
-
while candidate.exists():
|
|
52
|
-
candidate = _content_addressed_v2_companion_path(
|
|
53
|
-
path,
|
|
54
|
-
source_digest,
|
|
55
|
-
suffix,
|
|
56
|
-
)
|
|
57
|
-
suffix += 1
|
|
58
|
-
return candidate
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _legacy_source_metadata(path: Path, source: bytes) -> dict[str, str]:
|
|
62
|
-
return {
|
|
63
|
-
"path": str(path.resolve()),
|
|
64
|
-
"sha256": hashlib.sha256(source).hexdigest(),
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _matching_v2_companion(
|
|
69
|
-
path: Path,
|
|
70
|
-
source_metadata: dict[str, str],
|
|
71
|
-
) -> dict[str, Any] | None:
|
|
72
|
-
try:
|
|
73
|
-
payload = load_owned_object(path, artifact="wizard state")
|
|
74
|
-
except (OSError, UnicodeError, JsonBoundaryError):
|
|
75
|
-
return None
|
|
76
|
-
if not isinstance(payload, dict):
|
|
77
|
-
return None
|
|
78
|
-
if payload.get("executionIdentityVersion") != 2:
|
|
79
|
-
return None
|
|
80
|
-
if payload.get(_LEGACY_SOURCE_FIELD) != source_metadata:
|
|
81
|
-
return None
|
|
82
|
-
return payload
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _load_matching_v2_state(
|
|
86
|
-
paths: tuple[Path, ...],
|
|
87
|
-
source_metadata: dict[str, str],
|
|
88
|
-
) -> tuple[WizardState | None, Path | None]:
|
|
89
|
-
for companion_path in paths:
|
|
90
|
-
payload = _matching_v2_companion(companion_path, source_metadata)
|
|
91
|
-
if payload is None:
|
|
92
|
-
continue
|
|
93
|
-
try:
|
|
94
|
-
return WizardState.from_json(payload), companion_path
|
|
95
|
-
except WizardError:
|
|
96
|
-
continue
|
|
97
|
-
return None, None
|
|
24
|
+
def _fresh_state_from_stale(payload: dict) -> WizardState:
|
|
25
|
+
defaults = WizardState()
|
|
26
|
+
kwargs = {}
|
|
27
|
+
for name in _BOOTSTRAP_FIELDS:
|
|
28
|
+
value = payload.get(name)
|
|
29
|
+
if isinstance(value, type(getattr(defaults, name))):
|
|
30
|
+
kwargs[name] = value
|
|
31
|
+
return WizardState(**kwargs)
|
|
98
32
|
|
|
99
33
|
|
|
100
34
|
def load_state_file(path: Path) -> WizardState:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
resumed_state = None
|
|
108
|
-
companion_path = None
|
|
109
|
-
if source_version in (None, 1):
|
|
110
|
-
content_paths = _content_addressed_v2_companion_paths(
|
|
111
|
-
source_path,
|
|
112
|
-
source_digest,
|
|
113
|
-
)
|
|
114
|
-
resumed_state, companion_path = _load_matching_v2_state(
|
|
115
|
-
content_paths,
|
|
116
|
-
source_metadata,
|
|
117
|
-
)
|
|
118
|
-
if resumed_state is None:
|
|
119
|
-
resumed_state, _ = _load_matching_v2_state(
|
|
120
|
-
(_legacy_v2_companion_path(source_path),),
|
|
121
|
-
source_metadata,
|
|
122
|
-
)
|
|
123
|
-
state = resumed_state or WizardState.from_json(data)
|
|
124
|
-
state._source_execution_identity_version = source_version
|
|
125
|
-
if source_version in (None, 1):
|
|
126
|
-
state._legacy_source_metadata = source_metadata
|
|
127
|
-
state._v2_companion_path = companion_path or _available_v2_companion_path(
|
|
128
|
-
source_path,
|
|
129
|
-
source_digest,
|
|
130
|
-
)
|
|
131
|
-
return state
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _save_resumed_state_file(path: Path, state: WizardState) -> None:
|
|
135
|
-
"""Persist native v2 state while keeping legacy resume sources read-only."""
|
|
136
|
-
if getattr(state, "_source_execution_identity_version", 2) in (None, 1):
|
|
137
|
-
payload = state.to_json()
|
|
138
|
-
payload[_LEGACY_SOURCE_FIELD] = state._legacy_source_metadata
|
|
139
|
-
write_json_atomic(state._v2_companion_path, payload)
|
|
140
|
-
return
|
|
141
|
-
save_state_file(path, state)
|
|
35
|
+
data = load_owned_object(Path(path), artifact="wizard state")
|
|
36
|
+
try:
|
|
37
|
+
return WizardState.from_json(data)
|
|
38
|
+
except StaleWizardStateError:
|
|
39
|
+
# 이전 형식의 진행분은 옮기지 않는다 — 같은 프로젝트로 새 세션을 시작한다.
|
|
40
|
+
return _fresh_state_from_stale(data)
|
|
142
41
|
|
|
143
42
|
|
|
144
43
|
def save_state_file(path: Path, state: WizardState) -> None:
|
|
@@ -63,8 +63,6 @@ from .sources import (
|
|
|
63
63
|
_file_recency,
|
|
64
64
|
_git_main_worktree,
|
|
65
65
|
_latest_revision_requested_analysis_type,
|
|
66
|
-
_load_profile_optional_workers,
|
|
67
|
-
_load_profile_workers,
|
|
68
66
|
_parse_iso_timestamp,
|
|
69
67
|
_phase_after,
|
|
70
68
|
_project_relative_path,
|
|
@@ -671,12 +669,6 @@ def _apply_task_type(state: WizardState, value: str) -> str:
|
|
|
671
669
|
# 퍼지해야 downstream 재선택 시 carry 단계가 다시 나온다.
|
|
672
670
|
state.answered = [a for a in state.answered if a != S_BRIEF_CARRY]
|
|
673
671
|
carried = _carry_in_existing_brief(state)
|
|
674
|
-
state.profile_workers = _load_profile_workers(
|
|
675
|
-
Path(state.workspace_root), value
|
|
676
|
-
)
|
|
677
|
-
state.profile_optional_workers = _load_profile_optional_workers(
|
|
678
|
-
Path(state.workspace_root), value
|
|
679
|
-
)
|
|
680
672
|
# Reuse-worktree is decided once identity is final. Recompute here so
|
|
681
673
|
# subsequent base-ref step knows whether to apply.
|
|
682
674
|
state.reuse_worktree = _resolve_reuse_worktree(state)
|
|
@@ -839,11 +831,31 @@ def _build_brief_carry(state: WizardState) -> Prompt:
|
|
|
839
831
|
)
|
|
840
832
|
|
|
841
833
|
|
|
834
|
+
def _checked_out_canonical_ref(project_root: Path) -> str:
|
|
835
|
+
"""The main worktree's branch when it is one of the listed base refs, else ''.
|
|
836
|
+
|
|
837
|
+
Projects differ in their integration branch (main vs preprod), and nothing in
|
|
838
|
+
okstra records it; the branch the user keeps checked out is the observed hint.
|
|
839
|
+
"""
|
|
840
|
+
try:
|
|
841
|
+
main_wt = _git_main_worktree(project_root)
|
|
842
|
+
except WizardError:
|
|
843
|
+
return ""
|
|
844
|
+
result = subprocess.run(
|
|
845
|
+
["git", "-C", str(main_wt), "symbolic-ref", "--quiet", "--short", "HEAD"],
|
|
846
|
+
capture_output=True, text=True,
|
|
847
|
+
)
|
|
848
|
+
branch = result.stdout.strip() if result.returncode == 0 else ""
|
|
849
|
+
return branch if branch in CANONICAL_BASE_REFS else ""
|
|
850
|
+
|
|
851
|
+
|
|
842
852
|
def _build_base_ref_pick(state: WizardState) -> Prompt:
|
|
843
853
|
t = _p(state.workspace_root, "base_ref_pick")
|
|
844
854
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
845
|
-
|
|
846
|
-
|
|
855
|
+
recommended = _checked_out_canonical_ref(Path(state.project_root))
|
|
856
|
+
refs = sorted(CANONICAL_BASE_REFS, key=lambda r: r != recommended)
|
|
857
|
+
options = [_opt(r, f"{r}{recommended_suffix}", recommended=True) if r == recommended
|
|
858
|
+
else _opt(r) for r in refs]
|
|
847
859
|
for value, label in _static_options(t):
|
|
848
860
|
options.append(_opt(value=value, label=label))
|
|
849
861
|
return Prompt(
|
|
@@ -131,7 +131,7 @@ def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpe
|
|
|
131
131
|
if spec.recommend_note:
|
|
132
132
|
label += spec.recommend_note(state, suggestion)
|
|
133
133
|
options.append(_opt(spec.recommend_token, label, recommended=True))
|
|
134
|
-
|
|
134
|
+
setattr(state, spec.cache_attr, suggestion)
|
|
135
135
|
options.append(_opt(PICK_SKIP, t["options"][PICK_SKIP]))
|
|
136
136
|
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
137
137
|
return Prompt(
|
|
@@ -151,8 +151,8 @@ def _submit_optional_cached_pick(
|
|
|
151
151
|
setattr(state, attr, "")
|
|
152
152
|
setattr(state, spec.pending_attr, False)
|
|
153
153
|
return t["echo_suffixes"]["skip"]
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
cached = getattr(state, spec.cache_attr)
|
|
155
|
+
if value == spec.recommend_token and cached:
|
|
156
156
|
setattr(state, spec.target_attr, cached)
|
|
157
157
|
setattr(state, spec.pending_attr, False)
|
|
158
158
|
echo = t["echo_suffixes"][spec.echo_suffix_key].format(value=cached)
|
|
@@ -162,9 +162,10 @@ def _submit_optional_cached_pick(
|
|
|
162
162
|
if value == PICK_TYPE_CUSTOM:
|
|
163
163
|
setattr(state, spec.pending_attr, True)
|
|
164
164
|
return None
|
|
165
|
+
offered = ([spec.recommend_token] if cached else []) + [PICK_SKIP, PICK_TYPE_CUSTOM]
|
|
165
166
|
raise WizardError(
|
|
166
167
|
f"unexpected {spec.prompt_key} value: {value!r} "
|
|
167
|
-
f"(expected {
|
|
168
|
+
f"(expected {', '.join(map(repr, offered[:-1]))}, or {offered[-1]!r})"
|
|
168
169
|
)
|
|
169
170
|
|
|
170
171
|
|