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.
Files changed (133) hide show
  1. package/README.md +3 -2
  2. package/dist/cli-registry.mjs +6 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/render-bundle.mjs +1 -1
  5. package/dist/commands/lifecycle/doctor.mjs +1 -1
  6. package/dist/lib/skill-catalog.mjs +1 -0
  7. package/dist/lib/skill-catalog.mjs.map +1 -1
  8. package/docs/architecture/storage-model.md +14 -0
  9. package/docs/architecture.md +30 -9
  10. package/docs/cli.md +26 -22
  11. package/docs/contributor-change-matrix.md +1 -1
  12. package/docs/project-structure-overview.md +15 -8
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/agents/operations/explain-flow.json +6 -0
  16. package/runtime/bin/lib/okstra/cli.sh +1 -5
  17. package/runtime/bin/lib/okstra/globals.sh +0 -2
  18. package/runtime/bin/lib/okstra/usage.sh +5 -3
  19. package/runtime/bin/okstra.sh +0 -2
  20. package/runtime/prompts/duties/business-flow-investigator.json +14 -0
  21. package/runtime/prompts/lead/context-loader.md +1 -1
  22. package/runtime/prompts/lead/convergence.md +22 -7
  23. package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
  24. package/runtime/prompts/lead/report-writer.md +1 -1
  25. package/runtime/prompts/lead/team-contract.md +12 -17
  26. package/runtime/prompts/profiles/_common-contract.md +3 -3
  27. package/runtime/prompts/wizard/prompts.ko.json +0 -91
  28. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
  29. package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
  30. package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
  31. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
  32. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
  33. package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
  34. package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
  35. package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
  36. package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
  37. package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
  38. package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
  39. package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
  40. package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
  41. package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
  42. package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
  43. package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
  44. package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
  45. package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
  46. package/runtime/python/okstra_ctl/cmux.py +21 -16
  47. package/runtime/python/okstra_ctl/convergence.py +173 -2
  48. package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
  49. package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
  50. package/runtime/python/okstra_ctl/coverage_census.py +603 -0
  51. package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
  52. package/runtime/python/okstra_ctl/direct_work.py +1 -1
  53. package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
  54. package/runtime/python/okstra_ctl/doctor.py +12 -6
  55. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  56. package/runtime/python/okstra_ctl/group_context.py +5 -4
  57. package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
  58. package/runtime/python/okstra_ctl/manager_split.py +4 -1
  59. package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
  60. package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
  61. package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
  62. package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
  63. package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
  64. package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
  65. package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
  66. package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
  67. package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
  68. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
  69. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
  70. package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
  71. package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
  72. package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
  73. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
  74. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
  75. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
  76. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
  77. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
  78. package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
  79. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
  80. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
  81. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
  82. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
  83. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
  84. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
  85. package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
  86. package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
  87. package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
  88. package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
  89. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
  90. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
  91. package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
  92. package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +14 -8
  93. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
  94. package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
  95. package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
  96. package/runtime/python/okstra_ctl/plan_items.py +1 -1
  97. package/runtime/python/okstra_ctl/render.py +10 -43
  98. package/runtime/python/okstra_ctl/render_final_report.py +3 -0
  99. package/runtime/python/okstra_ctl/report_assembly.py +15 -1
  100. package/runtime/python/okstra_ctl/report_finalize.py +50 -0
  101. package/runtime/python/okstra_ctl/report_html/render.py +3 -0
  102. package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
  103. package/runtime/python/okstra_ctl/run.py +78 -409
  104. package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
  105. package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
  106. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
  107. package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
  108. package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
  109. package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
  110. package/runtime/python/okstra_ctl/wizard/render.py +2 -2
  111. package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
  112. package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
  113. package/runtime/python/okstra_ctl/wizard/state.py +36 -145
  114. package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
  115. package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
  116. package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
  117. package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
  118. package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
  119. package/runtime/schemas/business-flow-v1.schema.json +847 -0
  120. package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
  121. package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
  122. package/runtime/skills/okstra-inspect/facets/history.md +5 -5
  123. package/runtime/skills/okstra-run/SKILL.md +2 -2
  124. package/runtime/templates/reports/business-flow.template.md +106 -0
  125. package/runtime/templates/reports/html/base.template.html +14 -1
  126. package/runtime/templates/reports/html/business-flow.template.html +31 -0
  127. package/runtime/templates/reports/html/i18n/en.json +1 -0
  128. package/runtime/templates/reports/html/i18n/ko.json +1 -0
  129. package/runtime/templates/worker-prompt-preamble.md +11 -2
  130. package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
  131. package/runtime/validators/validate-run.py +70 -21
  132. package/runtime/validators/validate_analysis_report.py +21 -21
  133. package/runtime/python/okstra_ctl/workers.py +0 -133
@@ -55,6 +55,12 @@ from .phases.implementation_planning.entry import (
55
55
  )
56
56
  from . import fix_cycles
57
57
  from .analysis_packet import build_analysis_packet
58
+ from .coverage_census import (
59
+ CensusError,
60
+ census_filename,
61
+ census_for_run,
62
+ render_census_section,
63
+ )
58
64
  from . import group_context
59
65
  from .stage_ledger import (
60
66
  build_stage_ledger,
@@ -218,13 +224,6 @@ from .session import (
218
224
  from .phases.release_handoff.entry import (
219
225
  materialize_handoff_input, resolve_pr_template,
220
226
  )
221
- from .worker_artifacts import worker_provider_id
222
- from .workers import (
223
- normalize_workers,
224
- resolve_optional_workers,
225
- resolve_profile_workers,
226
- validate_workers_against_profile,
227
- )
228
227
  from .workflow import compute_workflow_state, load_phase_boundary
229
228
  from .locks import worktree_provision_mutex
230
229
  from . import stage_targets as _stage_targets
@@ -528,7 +527,7 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
528
527
  # 가 whole-task 통합 실패를 동일 타입으로 받는다. run.py 는 재노출만 한다.
529
528
  PrepareError = _stage_targets.PrepareError
530
529
  from .phases.implementation_option_selection.entry import (
531
- validate_analyser_roster, validate_brief_requirements,
530
+ validate_brief_requirements,
532
531
  )
533
532
 
534
533
 
@@ -557,7 +556,6 @@ class PrepareInputs:
557
556
  evidence_inputs_raw: str = ""
558
557
  user_authorization_json: str = ""
559
558
  directive: str = ""
560
- workers_override: str = ""
561
559
  role_counts_raw: tuple[str, ...] = ()
562
560
  role_models_raw: tuple[str, ...] = ()
563
561
  host_session_context: HostSessionContext = field(
@@ -1103,31 +1101,6 @@ def _record_artifact_runtime_render_only_event(inp: PrepareInputs, ctx: dict) ->
1103
1101
  )
1104
1102
 
1105
1103
 
1106
- def _workers_flag_value(roster_csv: str) -> str:
1107
- """로스터 CSV 를 `--workers` 가 받는 어휘(provider id)로 되돌린다.
1108
-
1109
- 두 어휘가 다르다. 로스터 행은 슬롯 id(`codex-verifier`)이고 `--workers` 는
1110
- 프로파일의 `Required workers:` 가 선언한 **provider 허용목록**이다
1111
- ([`workers.py`](workers.py)). 로스터를 그대로 실으면 재실행 argv 가
1112
- `unknown workers: codex-verifier` 로 죽는다 — 이 인자를 만드는 곳은 여기
1113
- 하나뿐이므로 변환도 여기서 한다.
1114
-
1115
- 같은 provider 의 슬롯 둘은 한 항목으로 접힌다. 슬롯 수는 이 인자가 아니라
1116
- 함께 실리는 `--role-count` · `--role-model` 이 나른다.
1117
- """
1118
- seen: set[str] = set()
1119
- out: list[str] = []
1120
- for token in (part.strip() for part in roster_csv.split(",")):
1121
- if not token:
1122
- continue
1123
- provider = worker_provider_id(token)
1124
- if provider in seen:
1125
- continue
1126
- seen.add(provider)
1127
- out.append(provider)
1128
- return ",".join(out)
1129
-
1130
-
1131
1104
  def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
1132
1105
  """rerun 충실 재현을 위한 canonical argv 재구성."""
1133
1106
  has_role_selection = bool(inp.role_counts_raw or inp.role_models_raw)
@@ -1136,9 +1109,6 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
1136
1109
  {} if inp.host_session_context.entry_mode == "current-session"
1137
1110
  else legacy_fallbacks
1138
1111
  )
1139
- workers = inp.workers_override or _workers_flag_value(
1140
- legacy_fallbacks.get("RECOMMENDED_ANALYSERS", "")
1141
- )
1142
1112
  pairs = [
1143
1113
  ("--task-type", inp.task_type),
1144
1114
  ("--project-id", inp.project_id),
@@ -1152,7 +1122,6 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
1152
1122
  ("--implementation-option", inp.implementation_option),
1153
1123
  ("--clarification-response", inp.clarification_response_path),
1154
1124
  ("--selected-direction", inp.selected_direction_path),
1155
- ("--workers", workers),
1156
1125
  (
1157
1126
  "--lead-provider",
1158
1127
  inp.lead_provider or lead_fallbacks.get("LEAD_PROVIDER", ""),
@@ -1161,19 +1130,9 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
1161
1130
  "--lead-model",
1162
1131
  inp.lead_model or lead_fallbacks.get("LEAD_MODEL", ""),
1163
1132
  ),
1164
- (
1165
- "--claude-model",
1166
- inp.claude_model or legacy_fallbacks.get("CLAUDE_WORKER_MODEL", ""),
1167
- ),
1168
- (
1169
- "--codex-model",
1170
- inp.codex_model or legacy_fallbacks.get("CODEX_WORKER_MODEL", ""),
1171
- ),
1172
- (
1173
- "--antigravity-model",
1174
- inp.antigravity_model
1175
- or legacy_fallbacks.get("ANTIGRAVITY_WORKER_MODEL", ""),
1176
- ),
1133
+ ("--claude-model", inp.claude_model),
1134
+ ("--codex-model", inp.codex_model),
1135
+ ("--antigravity-model", inp.antigravity_model),
1177
1136
  ("--worker-model", inp.worker_models_raw),
1178
1137
  (
1179
1138
  "--report-writer-provider",
@@ -1817,36 +1776,6 @@ def _register_and_check_project(project_root: Path, inp: PrepareInputs) -> None:
1817
1776
  _apply_tdd_bypass_if_requested(inp, project_root)
1818
1777
 
1819
1778
 
1820
- def _resolve_roster(inp: PrepareInputs, profile_file: Path) -> tuple[list[str], str]:
1821
- """profile 의 `Required workers:` 블록을 권위로 roster 를 해소한다.
1822
-
1823
- release-handoff 은 의도적으로 single-lead (worker dispatch / teammate /
1824
- convergence 없음). profile 에 `- Required workers:` 블록이 없으므로 어떤
1825
- override 가 와도 빈 roster 를 강제해 profile 계약과 일관성을 유지한다."""
1826
- if inp.task_type == "release-handoff":
1827
- return [], ""
1828
- profile_workers = resolve_profile_workers(profile_file)
1829
- optional_workers = resolve_optional_workers(profile_file)
1830
- profile_workers_csv = ",".join(profile_workers)
1831
- workers = normalize_workers(inp.workers_override or profile_workers_csv)
1832
- if inp.workers_override.strip():
1833
- validate_workers_against_profile(workers, profile_workers, optional_workers)
1834
- try:
1835
- role_profile = load_role_profile(profile_file)
1836
- except RoleProfileError as exc:
1837
- raise PrepareError(str(exc)) from exc
1838
- requires_report_writer = any(
1839
- row.role == "report-writer" and row.min_count == 1
1840
- for row in role_profile.roles
1841
- )
1842
- if requires_report_writer and "report-writer" not in workers:
1843
- workers.append("report-writer")
1844
- if not workers:
1845
- raise PrepareError(f"no workers resolved for profile: {inp.task_type}")
1846
- _validate_option_selection_roster(inp.task_type, workers)
1847
- return workers, ",".join(workers)
1848
-
1849
-
1850
1779
  def _profile_requires_executor(profile_file: Path) -> bool:
1851
1780
  try:
1852
1781
  profile = load_role_profile(profile_file)
@@ -1865,21 +1794,10 @@ def _normalize_prepare_model_selection(
1865
1794
  try:
1866
1795
  _validate_critic_choice(inp.critic)
1867
1796
  profile = load_role_profile(profile_file)
1868
- profile_workers = resolve_profile_workers(profile_file)
1869
- has_worker_models = bool(
1870
- inp.worker_models_raw.strip()
1871
- or inp.claude_model.strip()
1872
- or inp.codex_model.strip()
1873
- or inp.antigravity_model.strip()
1874
- )
1875
- workers = inp.workers_override
1876
- if not workers and has_worker_models:
1877
- workers = ",".join(profile_workers)
1878
1797
  selection = normalize_model_selection_inputs(
1879
1798
  profile=profile,
1880
1799
  role_counts_raw=inp.role_counts_raw,
1881
1800
  role_models_raw=inp.role_models_raw,
1882
- workers=workers,
1883
1801
  worker_model=inp.worker_models_raw,
1884
1802
  critic=inp.critic,
1885
1803
  executor=inp.executor,
@@ -1925,7 +1843,6 @@ def _resolve_prepare_assignments(
1925
1843
  inp: PrepareInputs,
1926
1844
  profile: RoleProfile,
1927
1845
  selection: CanonicalModelSelection,
1928
- workers: list[str],
1929
1846
  lead_runtime: str,
1930
1847
  terminal_backend: TerminalBackend,
1931
1848
  requires_executor: bool,
@@ -1935,7 +1852,6 @@ def _resolve_prepare_assignments(
1935
1852
  inp,
1936
1853
  profile,
1937
1854
  selection,
1938
- workers,
1939
1855
  scopes,
1940
1856
  requires_executor,
1941
1857
  )
@@ -2078,7 +1994,6 @@ def _selection_provider_ids(
2078
1994
  inp: PrepareInputs,
2079
1995
  profile: RoleProfile,
2080
1996
  selection: CanonicalModelSelection,
2081
- workers: list[str],
2082
1997
  scopes: ModelDefaultScopes,
2083
1998
  requires_executor: bool,
2084
1999
  ) -> tuple[str, ...]:
@@ -2095,15 +2010,21 @@ def _selection_provider_ids(
2095
2010
  inp,
2096
2011
  profile,
2097
2012
  selection,
2098
- workers,
2099
2013
  scopes,
2100
2014
  default_roles,
2101
2015
  requires_executor,
2102
2016
  )
2103
2017
  else:
2104
2018
  selected = list(
2105
- _selected_execution_provider_ids(inp, workers, requires_executor)
2019
+ _selected_execution_provider_ids(inp, requires_executor)
2106
2020
  )
2021
+ # 스냅샷에 없는 공급자의 모델은 번들 기본값 후보에서 빠진다. 사용자가 고르지
2022
+ # 않은 분석 칸은 등록 순서의 분석 공급자를 칸 수만큼 후보에 둔다.
2023
+ selected.extend(
2024
+ provider_ids("analyser")[:_bundled_analysis_default_slots(
2025
+ profile, selection, scopes,
2026
+ )]
2027
+ )
2107
2028
  selected.extend(
2108
2029
  provider
2109
2030
  for providers in selection.provider_constraints.values()
@@ -2173,7 +2094,6 @@ def _canonical_selection_provider_ids(
2173
2094
  inp: PrepareInputs,
2174
2095
  profile: RoleProfile,
2175
2096
  selection: CanonicalModelSelection,
2176
- workers: list[str],
2177
2097
  scopes: ModelDefaultScopes,
2178
2098
  default_roles: Sequence[str],
2179
2099
  requires_executor: bool,
@@ -2195,27 +2115,24 @@ def _canonical_selection_provider_ids(
2195
2115
  if implementer_uses_bundled_default:
2196
2116
  # `--executor` names the provider that implements, so it belongs in the
2197
2117
  # roster the same way the bundled default does. Reading only the default
2198
- # left `--executor <provider>` with no assignment of its own: the run
2199
- # rendered without that provider, and asking for it with `--workers`
2200
- # was refused as not being in the roster.
2118
+ # left `--executor <provider>` with no assignment of its own.
2201
2119
  selected.append(
2202
2120
  (inp.executor or "").strip().lower()
2203
2121
  or _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
2204
2122
  )
2205
- if _needs_profile_worker_candidates(profile, selection, scopes):
2206
- selected.extend(
2207
- worker for worker in workers if worker != "report-writer"
2208
- )
2209
2123
  return selected
2210
2124
 
2211
2125
 
2212
- def _needs_profile_worker_candidates(
2126
+ def _bundled_analysis_default_slots(
2213
2127
  profile: RoleProfile,
2214
2128
  selection: CanonicalModelSelection,
2215
2129
  scopes: ModelDefaultScopes,
2216
- ) -> bool:
2130
+ ) -> int:
2131
+ slots = 0
2217
2132
  for requirement in profile.roles:
2218
- if requirement.dynamic or requirement.role == "report-writer":
2133
+ if requirement.dynamic or requirement.role in {
2134
+ "report-writer", "implementer", "critic",
2135
+ }:
2219
2136
  continue
2220
2137
  desired = selection.role_counts.get(
2221
2138
  requirement.role,
@@ -2227,8 +2144,8 @@ def _needs_profile_worker_candidates(
2227
2144
  or requirement.role in scopes.global_
2228
2145
  )
2229
2146
  if explicit < desired and not has_scoped_default:
2230
- return True
2231
- return False
2147
+ slots = max(slots, desired)
2148
+ return slots
2232
2149
 
2233
2150
 
2234
2151
  def _providers_from_model_refs(
@@ -2400,13 +2317,6 @@ def _read_model_defaults(path: Path) -> dict[str, tuple[str, ...]]:
2400
2317
  return defaults
2401
2318
 
2402
2319
 
2403
- def _validate_option_selection_roster(task_type: str, workers: Sequence[str]) -> None:
2404
- if task_type == "implementation-option-selection":
2405
- validate_analyser_roster(workers)
2406
-
2407
-
2408
-
2409
-
2410
2320
  @dataclass
2411
2321
  class RoleAssignment:
2412
2322
  role: str
@@ -2530,29 +2440,6 @@ def recommended_role_models(
2530
2440
  return recommendations
2531
2441
 
2532
2442
 
2533
- def _parse_worker_model_overrides(
2534
- raw_value: str,
2535
- context: AssignmentContext,
2536
- ) -> dict[str, str]:
2537
- overrides: dict[str, str] = {}
2538
- for item in (raw_value or "").split(","):
2539
- item = item.strip()
2540
- if not item:
2541
- continue
2542
- provider, separator, model = item.partition("=")
2543
- provider = provider.strip().lower()
2544
- model = model.strip()
2545
- if not separator or not provider or not model:
2546
- raise PrepareError(
2547
- "--worker-model must use provider=model entries separated by commas"
2548
- )
2549
- if provider in overrides:
2550
- raise PrepareError(f"duplicate --worker-model provider: {provider}")
2551
- context.pool.resolve_provider(provider)
2552
- overrides[provider] = model
2553
- return overrides
2554
-
2555
-
2556
2443
  def _model_for_role(
2557
2444
  *,
2558
2445
  context: AssignmentContext,
@@ -2575,209 +2462,6 @@ def _model_for_role(
2575
2462
  raise PrepareError(str(exc)) from exc
2576
2463
 
2577
2464
 
2578
- def _resolve_worker_models(
2579
- inp: PrepareInputs,
2580
- workers: list[str],
2581
- context: AssignmentContext,
2582
- ) -> dict:
2583
- """Resolve provider models while preserving legacy fixed-provider projections."""
2584
- overrides = _parse_worker_model_overrides(inp.worker_models_raw, context)
2585
- legacy = {
2586
- "claude": inp.claude_model,
2587
- "codex": inp.codex_model,
2588
- "antigravity": inp.antigravity_model,
2589
- }
2590
- legacy_default_env = {
2591
- "claude": "OKSTRA_DEFAULT_CLAUDE_MODEL",
2592
- "codex": "OKSTRA_DEFAULT_CODEX_MODEL",
2593
- "antigravity": "OKSTRA_DEFAULT_ANTIGRAVITY_MODEL",
2594
- }
2595
-
2596
- def worker_model_input(provider: str) -> str:
2597
- selected = overrides.get(provider, legacy.get(provider, ""))
2598
- env_name = legacy_default_env.get(provider)
2599
- return selected or (_default(env_name, "") if env_name else "")
2600
-
2601
- selected_worker_providers = tuple(dict.fromkeys(
2602
- worker for worker in workers if worker != "report-writer"
2603
- ))
2604
- provider_models = {}
2605
- for provider in selected_worker_providers:
2606
- provider_models[provider] = _model_for_role(
2607
- context=context,
2608
- provider=provider,
2609
- raw_value=worker_model_input(provider),
2610
- role="analyser",
2611
- )
2612
- try:
2613
- lead_assignment = resolve_context_lead_provider(
2614
- context=context,
2615
- requested_provider=inp.lead_provider,
2616
- )
2617
- except (
2618
- HostCapabilityMismatch,
2619
- HostNotRegistered,
2620
- ProviderUnavailable,
2621
- UnknownProviderError,
2622
- ) as exc:
2623
- raise PrepareError(str(exc)) from exc
2624
- lead_provider = lead_assignment.provider
2625
- report_writer_provider = inp.report_writer_provider.strip().lower() or "claude"
2626
- if not context.pool.resolve_provider(report_writer_provider).supports_role(
2627
- "report-writer"
2628
- ):
2629
- raise PrepareError(
2630
- f"provider {report_writer_provider!r} does not support the report-writer role"
2631
- )
2632
-
2633
- def legacy_projection(provider: str) -> _LegacyModelProjection:
2634
- assigned = provider_models.get(provider)
2635
- if assigned is not None:
2636
- return _LegacyModelProjection(
2637
- model_id=assigned.model_id,
2638
- display=assigned.display,
2639
- execution=assigned.execution,
2640
- )
2641
- try:
2642
- selected = resolve_model_selection(
2643
- context.pool,
2644
- provider,
2645
- "analyser",
2646
- worker_model_input(provider),
2647
- )
2648
- except (ValueError, RuntimeError) as exc:
2649
- raise PrepareError(str(exc)) from exc
2650
- return _LegacyModelProjection(
2651
- model_id=selected.model_ref.model_id,
2652
- display=selected.display_name,
2653
- execution=selected.execution_value,
2654
- )
2655
-
2656
- return {
2657
- "lead_provider": lead_provider,
2658
- "report_writer_provider": report_writer_provider,
2659
- "lead": _model_for_role(
2660
- context=context,
2661
- provider=lead_provider,
2662
- raw_value=inp.lead_model,
2663
- role="leader",
2664
- default_override=_legacy_claude_model_override(
2665
- lead_provider,
2666
- "OKSTRA_DEFAULT_LEAD_MODEL",
2667
- ),
2668
- duty_id="lead",
2669
- ),
2670
- "cw": legacy_projection("claude"),
2671
- "co": legacy_projection("codex"),
2672
- "ge": legacy_projection("antigravity"),
2673
- "rw": _model_for_role(
2674
- context=context,
2675
- provider=report_writer_provider,
2676
- raw_value=inp.report_writer_model,
2677
- role="report-writer",
2678
- default_override=_legacy_claude_model_override(
2679
- report_writer_provider,
2680
- "OKSTRA_DEFAULT_REPORT_WRITER_MODEL",
2681
- ),
2682
- ),
2683
- "providers": provider_models,
2684
- "worker_model_inputs": {
2685
- provider: worker_model_input(provider)
2686
- for provider in dict.fromkeys((
2687
- *selected_worker_providers,
2688
- *overrides,
2689
- *legacy,
2690
- ))
2691
- },
2692
- }
2693
-
2694
-
2695
- def _resolve_model_bindings(
2696
- inp: PrepareInputs,
2697
- workers: list[str],
2698
- context: AssignmentContext,
2699
- requires_executor: bool,
2700
- ) -> _ModelBindings:
2701
- """worker 모델 + critic 선택 + executor 바인딩을 한 묶음으로 해소·검증한다."""
2702
- m = _resolve_worker_models(inp, workers, context)
2703
- critic_choice = _validate_critic_choice(inp.critic)
2704
- critic_model_execution = ""
2705
- critic_meta = None
2706
- if critic_choice in provider_ids("critic"):
2707
- critic_meta = _model_for_role(
2708
- context=context,
2709
- provider=critic_choice,
2710
- raw_value=m["worker_model_inputs"].get(critic_choice, ""),
2711
- role="critic",
2712
- duty_id="scope-critic",
2713
- )
2714
- critic_model_execution = critic_meta.execution
2715
- executor_assignment = None
2716
- display_name = ""
2717
- if requires_executor:
2718
- executor_assignment, display_name = _resolve_executor_assignment(
2719
- inp,
2720
- workers,
2721
- context,
2722
- m["worker_model_inputs"],
2723
- )
2724
- lead_assignment = _role_assignment(
2725
- resolved=m["lead"],
2726
- role="lead",
2727
- )
2728
- worker_assignments = _build_worker_assignments(workers, m)
2729
- critic_assignment = (
2730
- _role_assignment(
2731
- resolved=critic_meta,
2732
- role="critic",
2733
- )
2734
- if critic_meta is not None
2735
- else None
2736
- )
2737
- translator_meta = _model_for_role(
2738
- context=context,
2739
- provider=m["report_writer_provider"],
2740
- raw_value=m["rw"].model_id,
2741
- role="translator",
2742
- duty_id="translator",
2743
- )
2744
- translator_assignment = _role_assignment(
2745
- resolved=translator_meta,
2746
- role="translator",
2747
- )
2748
- invocation_assignments = _build_invocation_assignments(
2749
- lead_assignment,
2750
- worker_assignments,
2751
- critic_assignment,
2752
- translator_assignment,
2753
- inp.task_type,
2754
- )
2755
- worker_by_provider = {
2756
- row.provider: row for row in worker_assignments
2757
- }
2758
- return _ModelBindings(
2759
- lead=m["lead"], cw=m["cw"], co=m["co"], ge=m["ge"], rw=m["rw"],
2760
- critic_choice=critic_choice,
2761
- critic_model_execution=critic_model_execution,
2762
- executor_display_name=display_name,
2763
- codex_worker_execution=(
2764
- worker_by_provider["codex"].model_execution_value
2765
- if "codex" in worker_by_provider
2766
- else m["co"].execution
2767
- ),
2768
- antigravity_worker_execution=(
2769
- worker_by_provider["antigravity"].model_execution_value
2770
- if "antigravity" in worker_by_provider
2771
- else m["ge"].execution
2772
- ),
2773
- executor_assignment=executor_assignment,
2774
- lead_assignment=lead_assignment,
2775
- worker_assignments=worker_assignments,
2776
- invocation_assignments=invocation_assignments,
2777
- translator=translator_meta,
2778
- )
2779
-
2780
-
2781
2465
  def _validate_critic_choice(raw_value: str) -> str:
2782
2466
  """critic 은 어느 phase 에서도 opt-in 이다 — 프로필이 `min: 0` 으로 선언한다.
2783
2467
 
@@ -3006,12 +2690,10 @@ def _legacy_projection_from_plan(
3006
2690
 
3007
2691
  def _selected_execution_provider_ids(
3008
2692
  inp: PrepareInputs,
3009
- workers: list[str],
3010
2693
  requires_executor: bool,
3011
2694
  ) -> tuple[str, ...]:
3012
2695
  selected = [
3013
2696
  inp.lead_provider,
3014
- *(worker for worker in workers if worker != "report-writer"),
3015
2697
  inp.report_writer_provider.strip().lower() or "claude",
3016
2698
  ]
3017
2699
  critic = inp.critic.strip().lower()
@@ -3025,64 +2707,6 @@ def _selected_execution_provider_ids(
3025
2707
  return tuple(dict.fromkeys(selected))
3026
2708
 
3027
2709
 
3028
- def _resolve_executor_assignment(
3029
- inp: PrepareInputs,
3030
- workers: list[str],
3031
- context: AssignmentContext,
3032
- worker_model_inputs: dict[str, str],
3033
- ) -> tuple[RoleAssignment, str]:
3034
- executor_default = _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
3035
- executor_provider = (inp.executor or executor_default).strip().lower()
3036
- allowed_executors = provider_ids("executor")
3037
- if executor_provider not in allowed_executors:
3038
- raise PrepareError(
3039
- f"--executor must be one of: {', '.join(allowed_executors)} "
3040
- f"(got: {executor_provider!r})"
3041
- )
3042
- if executor_provider not in workers:
3043
- raise PrepareError(
3044
- f"--executor {executor_provider} requires {executor_provider!r} in "
3045
- f"--workers, but resolved roster is {workers!r}. "
3046
- f"Add it explicitly, e.g. --workers {','.join(sorted(set(workers + [executor_provider])))}."
3047
- )
3048
- model = _model_for_role(
3049
- context=context,
3050
- provider=executor_provider,
3051
- raw_value=worker_model_inputs.get(executor_provider, ""),
3052
- role="implementer",
3053
- duty_id="implementation-executor",
3054
- )
3055
- assignment = _role_assignment(
3056
- resolved=model,
3057
- role="executor",
3058
- worker_id=executor_provider,
3059
- )
3060
- display_name = (
3061
- f"{context.pool.resolve_provider(executor_provider).display_label} executor"
3062
- )
3063
- return assignment, display_name
3064
-
3065
-
3066
- def _build_worker_assignments(
3067
- workers: list[str], models: dict,
3068
- ) -> tuple[RoleAssignment, ...]:
3069
- assignments: list[RoleAssignment] = []
3070
- for worker_id in workers:
3071
- is_report_writer = worker_id == "report-writer"
3072
- provider = (
3073
- models["report_writer_provider"] if is_report_writer else worker_id
3074
- )
3075
- selected = (
3076
- models["rw"] if is_report_writer else models["providers"][provider]
3077
- )
3078
- assignments.append(_role_assignment(
3079
- resolved=selected,
3080
- role="report-writer" if is_report_writer else "analyser",
3081
- worker_id=worker_id,
3082
- ))
3083
- return tuple(assignments)
3084
-
3085
-
3086
2710
  def _role_assignment(
3087
2711
  *,
3088
2712
  resolved: ResolvedAssignment,
@@ -3138,7 +2762,7 @@ def _build_invocation_assignments(
3138
2762
  )
3139
2763
  if critic is not None:
3140
2764
  # 한 run 이 도달할 수 있는 critic 은 한 종류다. 둘 다 등록하면 실행될
3141
- # 수 없는 로스터 행이 남고, `_optional_worker_roles` 가 provider 로만
2765
+ # 수 없는 로스터 행이 남고, `_critic_role_rows` 가 provider 로만
3142
2766
  # role 라벨을 만들기 때문에 두 행의 이름이 겹쳐 validate-run 이
3143
2767
  # `duplicate worker role detected` 로 런을 실패시킨다.
3144
2768
  assignments[critic_assignment_ref(task_type)] = critic.to_model_payload()
@@ -3404,11 +3028,19 @@ def _write_instruction_set_sources(
3404
3028
  if inp.task_type == "implementation-planning" and inp.selected_direction_path
3405
3029
  else ""
3406
3030
  )
3031
+ from .business_flow.hooks import prepare_task_knowledge
3032
+
3033
+ business_knowledge = prepare_task_knowledge(
3034
+ Path(ctx["PROJECT_ROOT"]), instruction_set, ctx,
3035
+ (instruction_set / "task-brief.md").read_text(encoding="utf-8") + "\n" + inp.directive,
3036
+ )
3037
+ coverage_census_text = _write_coverage_census(inp, ctx, instruction_set)
3407
3038
  packet = build_analysis_packet(
3408
3039
  task_key=ctx["TASK_KEY"],
3409
3040
  task_type=ctx["TASK_TYPE"],
3410
3041
  task_brief_path=instruction_set / "task-brief.md",
3411
3042
  group_context_path=group_context_path,
3043
+ business_knowledge_text=business_knowledge,
3412
3044
  analysis_profile_path=instruction_set / "analysis-profile.md",
3413
3045
  reference_expectations_path=instruction_set / "reference-expectations.md",
3414
3046
  clarification_response_path=(
@@ -3426,6 +3058,7 @@ def _write_instruction_set_sources(
3426
3058
  Path(ctx["PROJECT_ROOT"]), ctx["TASK_KEY"],
3427
3059
  json.loads(ctx.get("RELATED_TASKS_JSON", "[]")),
3428
3060
  ),
3061
+ coverage_census_text=coverage_census_text,
3429
3062
  )
3430
3063
  if inp.task_type == "technical-verification":
3431
3064
  packet += materialize_technical_input_packet(
@@ -3441,6 +3074,46 @@ def _write_instruction_set_sources(
3441
3074
  return instruction_set
3442
3075
 
3443
3076
 
3077
+ def _write_coverage_census(
3078
+ inp: PrepareInputs, ctx: dict, instruction_set: Path,
3079
+ ) -> str:
3080
+ """Persist this run's coverage census and return its packet block.
3081
+
3082
+ The run-scoped state file is the canonical copy because the packet lives in
3083
+ the task-scoped instruction set and the next phase overwrites it. A profile
3084
+ block that cannot be read is reported and the run goes on without a census.
3085
+ """
3086
+ changed_files: tuple[str, ...] = ()
3087
+ if inp.task_type == "final-verification":
3088
+ changed_files = tuple(_git_out(
3089
+ ctx["VERIFICATION_WORKTREE_PATH"],
3090
+ "diff",
3091
+ "--name-only",
3092
+ f"{ctx['VERIFICATION_BASE_REF']}..{ctx['VERIFICATION_HEAD_REF']}",
3093
+ ).splitlines())
3094
+ try:
3095
+ census = census_for_run(
3096
+ task_type=inp.task_type,
3097
+ profile_text=(instruction_set / "analysis-profile.md").read_text(
3098
+ encoding="utf-8"
3099
+ ),
3100
+ brief_path=instruction_set / "task-brief.md",
3101
+ changed_files=changed_files,
3102
+ )
3103
+ except CensusError as exc:
3104
+ print(f"prepare: coverage census skipped: {exc}", file=sys.stderr)
3105
+ return ""
3106
+ if census is None:
3107
+ return ""
3108
+ state_dir = Path(ctx["RUN_STATE_DIR"])
3109
+ state_dir.mkdir(parents=True, exist_ok=True)
3110
+ census_path = state_dir / census_filename(inp.task_type, ctx["RUN_STATE_SEQ"])
3111
+ write_owned_object_atomic(census_path, census, artifact="coverage census")
3112
+ return render_census_section(
3113
+ census, relative_to_project_root(census_path, Path(ctx["PROJECT_ROOT"]))
3114
+ )
3115
+
3116
+
3444
3117
  def _write_analysis_evidence_artifact(ctx: dict, instruction_set: Path) -> None:
3445
3118
  """Write selected evidence metadata without copying the original reports."""
3446
3119
  if ctx.get("TASK_TYPE") not in ANALYSIS_TASK_TYPES:
@@ -4436,7 +4109,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4436
4109
  handoff_tokens = materialize_handoff_input(
4437
4110
  workspace_root, project_root, inp, install_hint=_INSTALL_HINT)
4438
4111
 
4439
- workers, selected_reviewers = _resolve_roster(inp, profile_file)
4440
4112
  pr_template_path_str, pr_template_source = (
4441
4113
  resolve_pr_template(inp) if inp.task_type == "release-handoff" else ("", "")
4442
4114
  )
@@ -4445,7 +4117,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4445
4117
  inp=inp,
4446
4118
  profile=role_profile,
4447
4119
  selection=selection,
4448
- workers=workers,
4449
4120
  lead_runtime=lead_runtime,
4450
4121
  terminal_backend=terminal_backend,
4451
4122
  requires_executor=requires_executor,
@@ -4805,7 +4476,7 @@ bundle, and prints the rendered lead prompt without dispatching workers.
4805
4476
  Usage:
4806
4477
  okstra codex-run --project-root <dir> --project-id <id> \
4807
4478
  --task-group <tg> --task-id <tid> --task-type <type> \
4808
- --task-brief <path-or-rel> [--workers <list>] [--directive <text>] \
4479
+ --task-brief <path-or-rel> [--directive <text>] \
4809
4480
  [--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \
4810
4481
  [--antigravity-model <m>] [--report-writer-model <m>] \
4811
4482
  [--related-tasks <list>] [--base-ref <ref>] \
@@ -4855,7 +4526,6 @@ def build_prepare_argument_parser():
4855
4526
  "open + run 부착, 'no'/'' = 기록 안 함."
4856
4527
  ),
4857
4528
  )
4858
- p.add_argument("--workers", default="", dest="workers_override")
4859
4529
  p.add_argument("--role-count", action="append", default=[], dest="role_counts_raw")
4860
4530
  p.add_argument("--role-model", action="append", default=[], dest="role_models_raw")
4861
4531
  p.add_argument(
@@ -5131,7 +4801,6 @@ def main(argv: list[str]) -> int:
5131
4801
  evidence_inputs_raw=args.evidence_inputs_raw,
5132
4802
  directive=args.directive,
5133
4803
  user_authorization_json=args.user_authorization_json,
5134
- workers_override=args.workers_override,
5135
4804
  role_counts_raw=tuple(args.role_counts_raw),
5136
4805
  role_models_raw=tuple(args.role_models_raw),
5137
4806
  host_session_context=host_session_context,