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
@@ -58,6 +58,7 @@ DUTY_ROLE_IDS = {
58
58
  "reverification-worker": "verifier",
59
59
  "code-reviewer": "verifier",
60
60
  "schedule-verifier": "verifier",
61
+ "business-flow-investigator": "analyser",
61
62
  "report-writer": "report-writer",
62
63
  "translator": "translator",
63
64
  }
@@ -39,7 +39,7 @@ from typing import Any, Mapping
39
39
  from okstra_project import list_project_tasks
40
40
  from okstra_project.phase_pointer import STATUS_TERMINAL
41
41
 
42
- from .brief_frontmatter import read_brief_frontmatter
42
+ from .brief_frontmatter import brief_task_id, read_brief_frontmatter
43
43
  from .ids import slugify_task_segment
44
44
  from .paths import find_asset_root
45
45
  from .run_context import dir_flock
@@ -387,7 +387,7 @@ _QUEUE_LINE_RE = re.compile(r"^\d+\. \[(?P<status>[^\]]*)\] (?P<task>\S+)")
387
387
 
388
388
  @dataclass(frozen=True)
389
389
  class QueueRow:
390
- task_id: str # slugified brief-id — the task directory name
390
+ task_id: str # slugified task-id (else brief-id) — the task directory name
391
391
  brief_id: str
392
392
  ticket_id: str
393
393
  brief: str # project-relative brief path
@@ -420,6 +420,7 @@ def group_briefs(project_root: Path, task_group: str) -> list[dict[str, Any]]:
420
420
  brief_id = frontmatter.get("brief-id") or path.stem
421
421
  match = _ORDINAL_RE.match(brief_id)
422
422
  briefs.append({
423
+ "task_id": slugify_task_segment(brief_task_id(frontmatter) or brief_id),
423
424
  "brief_id": brief_id,
424
425
  "ticket_id": frontmatter.get("ticket-id") or ticket_id_from_brief_id(brief_id),
425
426
  "ordinal": int(match.group("ordinal")) if match else None,
@@ -509,7 +510,7 @@ def group_queue(
509
510
  recorded = catalog_progress(project_root, task_group)
510
511
 
511
512
  def _status(brief: dict[str, Any]) -> tuple[str, str]:
512
- task_id = slugify_task_segment(brief["brief_id"])
513
+ task_id = brief["task_id"]
513
514
  if task_id in recorded:
514
515
  return recorded[task_id]
515
516
  entry = by_task.get(task_id)
@@ -527,7 +528,7 @@ def group_queue(
527
528
  if status_by_ticket[ticket][0] != QUEUE_DONE
528
529
  ))
529
530
  rows.append(QueueRow(
530
- slugify_task_segment(brief["brief_id"]), brief["brief_id"], brief["ticket_id"],
531
+ brief["task_id"], brief["brief_id"], brief["ticket_id"],
531
532
  brief["brief"], status, progress, open_waits,
532
533
  duplicates.get(brief["brief_id"], ()),
533
534
  ))
@@ -58,7 +58,6 @@ def normalize_model_selection_inputs(
58
58
  profile: RoleProfile,
59
59
  role_counts_raw: Sequence[str],
60
60
  role_models_raw: Sequence[str],
61
- workers: str,
62
61
  worker_model: str,
63
62
  critic: str,
64
63
  executor: str,
@@ -73,7 +72,6 @@ def normalize_model_selection_inputs(
73
72
  models = _parse_role_models(role_models_raw)
74
73
  legacy = _legacy_selection(
75
74
  profile=profile,
76
- workers=workers,
77
75
  worker_model=worker_model,
78
76
  critic=critic,
79
77
  executor=executor,
@@ -141,7 +139,6 @@ def _split_role_value(raw: str, option: str) -> tuple[str, str]:
141
139
  def _legacy_selection(
142
140
  *,
143
141
  profile: RoleProfile,
144
- workers: str,
145
142
  worker_model: str,
146
143
  critic: str,
147
144
  executor: str,
@@ -155,27 +152,12 @@ def _legacy_selection(
155
152
  constraints: dict[str, tuple[str, ...]] = {}
156
153
  values: dict[str, dict[str, str]] = {}
157
154
  sources: list[str] = []
158
- needs_initial = bool(
159
- workers.strip()
160
- or worker_model.strip()
161
- or any(model.strip() for model in provider_models.values())
162
- )
163
- initial = (
164
- _initial_cross_verification_requirement(profile)
165
- if needs_initial
166
- else None
167
- )
168
- if workers.strip() and initial is not None:
169
- providers = _legacy_worker_providers(workers)
170
- _validate_legacy_workers(initial, providers)
171
- counts[initial.role] = len(providers)
172
- constraints[initial.role] = providers
173
- report_writer = _requirement(profile, "report-writer")
174
- if report_writer is not None:
175
- counts["report-writer"] = report_writer.recommended_count
176
- sources.append("--workers")
177
155
  worker_values = _parse_worker_models(worker_model, provider_models)
178
- if worker_values and initial is not None:
156
+ if worker_values:
157
+ # A provider model flag fills one slot of the initial cross-verification
158
+ # role per named provider; the remaining slots take the model defaults.
159
+ initial = _initial_cross_verification_requirement(profile)
160
+ constraints[initial.role] = tuple(worker_values)
179
161
  values[initial.role] = worker_values
180
162
  sources.append("worker-model")
181
163
  _add_provider_selection(
@@ -201,30 +183,8 @@ def _initial_cross_verification_requirement(profile: RoleProfile) -> RoleRequire
201
183
  }:
202
184
  return requirement
203
185
  raise ModelSelectionInputError(
204
- "profile has no initial cross-verification role for legacy --workers"
205
- )
206
-
207
-
208
- def _legacy_worker_providers(workers: str) -> tuple[str, ...]:
209
- tokens = tuple(
210
- token.strip().lower() for token in workers.split(",") if token.strip()
186
+ "profile has no initial cross-verification role for a provider model flag"
211
187
  )
212
- return tuple(token for token in tokens if token != "report-writer")
213
-
214
-
215
- def _validate_legacy_workers(
216
- requirement: RoleRequirement,
217
- providers: tuple[str, ...],
218
- ) -> None:
219
- # minDistinctProviders 제거. 모델 참조 중복 거부는 Task 2.
220
- if (
221
- len(providers) < requirement.min_count
222
- or len(providers) > requirement.max_count
223
- ):
224
- raise ModelSelectionInputError(
225
- f"legacy --workers selects {len(providers)} {requirement.role} instances; "
226
- f"profile requires {requirement.min_count}..{requirement.max_count}"
227
- )
228
188
 
229
189
 
230
190
  def _parse_worker_models(
@@ -346,7 +306,7 @@ def _legacy_matches_canonical(
346
306
  canonical_models: Mapping[str, tuple[str, ...]],
347
307
  legacy: CanonicalModelSelection,
348
308
  ) -> bool:
349
- """레거시(--workers 등)가 변환 후 정본과 같은 값으로 수렴하는지 본다."""
309
+ """레거시(공급자 모델 플래그 등)가 변환 후 정본과 같은 값으로 수렴하는지 본다."""
350
310
  leg_count = legacy.role_counts.get(role)
351
311
  can_count = canonical_counts.get(role)
352
312
  if can_count is not None and leg_count is not None and can_count != leg_count:
@@ -456,10 +416,6 @@ def _validate_role_count(
456
416
  )
457
417
 
458
418
 
459
- def _requirement(profile: RoleProfile, role: str) -> RoleRequirement | None:
460
- return next((row for row in profile.roles if row.role == role), None)
461
-
462
-
463
419
  def serialize_host_session_context(context: HostSessionContext) -> str:
464
420
  """Serialize host-owned session facts as stable canonical JSON."""
465
421
  current = context.current_model
@@ -277,6 +277,7 @@ def _render_brief(
277
277
  assignment: dict,
278
278
  task_group: str,
279
279
  manager_task: str,
280
+ child_task_id: str,
280
281
  stem: str,
281
282
  created: str,
282
283
  key: str,
@@ -293,6 +294,7 @@ def _render_brief(
293
294
  "---",
294
295
  "type: brief",
295
296
  f"brief-id: {stem}",
297
+ f"task-id: {child_task_id}",
296
298
  "parent-id: self",
297
299
  f"ticket-id: {ticket}",
298
300
  f"source-type: {SOURCE_TYPE}",
@@ -429,7 +431,8 @@ def split_task(
429
431
  siblings = [(other["taskKey"], other["assignment"], other["briefPath"]) for other in items
430
432
  if other["issue"] is item["issue"] and other is not item]
431
433
  item["text"] = _render_brief(source=source, issue=item["issue"], assignment=item["assignment"],
432
- task_group=task_group, manager_task=manager_task, stem=item["stem"],
434
+ task_group=task_group, manager_task=manager_task,
435
+ child_task_id=item["childTaskId"], stem=item["stem"],
433
436
  created=when[:10], key=item["taskKey"], project_root=item["projectRoot"],
434
437
  siblings=siblings, graph=graph)
435
438
  errors = _validate_rendered(items, validator, group_segment)
@@ -2,18 +2,14 @@
2
2
 
3
3
  이 층은 파일을 열지 않는다 — 이미 읽힌 매핑을 받아 문자열만 만든다. 그래서
4
4
  참조 해석과 독립이고, 서식을 고칠 때 경로 규칙을 다시 읽을 필요가 없다.
5
- `_provider_models` 만 예외적으로 거절하지만, 그것도 디스크가 아니라 받은
6
- 매핑 안의 모순을 보는 것이다.
7
5
  """
8
6
  from __future__ import annotations
9
7
 
10
- from pathlib import Path
11
8
  from typing import Any, Mapping
12
9
 
13
10
  from okstra_project.phase_pointer import promote
14
11
 
15
12
  from ..fixed_text import line as _line, scalar as _value
16
- from ..json_boundary import JsonBoundaryError
17
13
 
18
14
 
19
15
  def _mapping(payload: Mapping[str, Any], key: str) -> Mapping[str, Any]:
@@ -87,7 +83,7 @@ def _critic_assignment_lines(run: Mapping[str, Any]) -> list[str]:
87
83
  """Opt-in critics live only in `invocationAssignments`, never in `workerAssignments`.
88
84
 
89
85
  The id is the assignment ref's last segment, the value team dispatch and
90
- `optionalWorkerRoles[].workerId` use (`render._optional_worker_roles`).
86
+ `criticRoles[].workerId` use (`render._critic_role_rows`).
91
87
  """
92
88
  assignments = run.get("invocationAssignments")
93
89
  if not isinstance(assignments, Mapping):
@@ -173,22 +169,3 @@ def _history_overview_text(rows: list[dict[str, object]], limit: int) -> str:
173
169
 
174
170
  def _csv(value: object) -> str:
175
171
  return ",".join(str(item) for item in value) if isinstance(value, list) else ""
176
-
177
-
178
- def _provider_models(run: Mapping[str, Any]) -> dict[str, str]:
179
- values: dict[str, set[str]] = {}
180
- assignments = run.get("workerAssignments")
181
- if isinstance(assignments, list):
182
- for assignment in assignments:
183
- if not isinstance(assignment, Mapping):
184
- continue
185
- provider = assignment.get("provider")
186
- model = assignment.get("model")
187
- if isinstance(provider, str) and isinstance(model, str) and model:
188
- values.setdefault(provider, set()).add(model)
189
- for provider, models in values.items():
190
- if len(models) > 1:
191
- raise JsonBoundaryError(
192
- Path("workerAssignments"), "run manifest", f"conflicting models for provider {provider}"
193
- )
194
- return {provider: next(iter(models)) for provider, models in values.items()}
@@ -18,10 +18,11 @@ from ..convergence import (
18
18
  from ..error_zip import last_output_path
19
19
  from ..fixed_text import line as _line, scalar as _value
20
20
  from ..json_boundary import JsonBoundaryError, load_owned_object
21
- from ..paths import okstra_home
21
+ from ..paths import find_asset_root, okstra_home
22
+ from ..phases.catalog import PhaseAssetError, profile_markdown
22
23
  from ..recap import assemble_group_recap, assemble_recap
24
+ from ..role_requirements import RoleProfileError, load_role_profile
23
25
  from ..timeline_runs import current_run_facts
24
- from ..worker_artifacts import worker_provider_id
25
26
  from .lines import (
26
27
  _assignment_line,
27
28
  _csv,
@@ -30,7 +31,6 @@ from .lines import (
30
31
  _next_phase_lines,
31
32
  _project_policy_lines,
32
33
  next_phase_pointer,
33
- _provider_models,
34
34
  _status_overview_text,
35
35
  _worker_assignment_lines,
36
36
  _worker_prompt_lines,
@@ -465,46 +465,59 @@ def render_report_input(project_root: Path, task_ref: str) -> str:
465
465
  )
466
466
 
467
467
 
468
- def _worker_providers_csv(recommended: Any) -> str:
469
- """로스터 슬롯 id 를 `--workers` 가 받는 provider id 로 접는다.
468
+ def _role_selection_rows(run: Mapping[str, Any]) -> list[tuple[str, str]]:
469
+ """실행된 역할 칸을 `--role-model` 값(`<role>=<provider>/<model>`)으로 옮긴다.
470
470
 
471
- `recommendedWorkers` 는 슬롯 id(`codex-verifier`)를 싣고 `--workers` 는
472
- 프로파일이 선언한 provider 허용목록만 받는다([`workers.py`](../workers.py)).
473
- 슬롯 id 를 그대로 넘기면 재실행이 `unknown workers` 로 죽으므로, 재실행 입력을
474
- 만드는 이 자리에서 어휘를 맞춘다. 슬롯 수는 `--role-count` 가 나른다.
471
+ critic 은 `workerAssignments` 가 아니라 `invocationAssignments` 의 `critic/*`
472
+ 에만 있다(`render._critic_role_rows`).
475
473
  """
476
- if not isinstance(recommended, list):
477
- return ""
478
- seen: list[str] = []
479
- for item in recommended:
480
- if not isinstance(item, str) or not item.strip():
474
+ rows: list[tuple[str, str]] = []
475
+ assignments = run.get("workerAssignments")
476
+ for assignment in assignments if isinstance(assignments, list) else ():
477
+ if not isinstance(assignment, Mapping):
481
478
  continue
482
- provider = worker_provider_id(item.strip())
483
- if provider not in seen:
484
- seen.append(provider)
485
- return ",".join(seen)
479
+ role, provider, model = (
480
+ assignment.get(key) for key in ("role", "provider", "model")
481
+ )
482
+ if all(isinstance(value, str) and value for value in (role, provider, model)):
483
+ rows.append((role, f"{provider}/{model}"))
484
+ invocations = run.get("invocationAssignments")
485
+ if isinstance(invocations, Mapping):
486
+ for ref, assignment in sorted(invocations.items()):
487
+ if not (isinstance(ref, str) and ref.startswith("critic/")):
488
+ continue
489
+ if not isinstance(assignment, Mapping):
490
+ continue
491
+ provider, model = assignment.get("provider"), assignment.get("model")
492
+ if isinstance(provider, str) and provider and isinstance(model, str) and model:
493
+ rows.append(("critic", f"{provider}/{model}"))
494
+ return rows
495
+
496
+
497
+ def _adjustable_roles(run_manifest: Path, task_type: object) -> set[str]:
498
+ """`--role-count` 를 받는 역할 — 하한과 상한이 다른 정적 역할."""
499
+ root = find_asset_root(("prompts", "profiles"), is_present=Path.is_dir)
500
+ if root is None or not isinstance(task_type, str):
501
+ raise JsonBoundaryError(run_manifest, "run manifest", "phase profile is unavailable")
502
+ try:
503
+ profile = load_role_profile(profile_markdown(root, task_type))
504
+ except (PhaseAssetError, RoleProfileError, ValueError) as exc:
505
+ raise JsonBoundaryError(run_manifest, "run manifest", str(exc)) from exc
506
+ return {
507
+ requirement.role
508
+ for requirement in profile.roles
509
+ if not requirement.dynamic and requirement.min_count < requirement.max_count
510
+ }
486
511
 
487
512
 
488
513
  def render_rerun_input(run_manifest: Path) -> str:
489
514
  run_manifest = _owned_run_manifest_path(run_manifest)
490
515
  run = load_owned_object(run_manifest, artifact="run manifest")
491
- models = _provider_models(run)
492
- team = _mapping(run, "teamContract")
493
- executor = _mapping(team, "executor")
494
- executor_provider = executor.get("provider")
495
- executor_model = executor.get("model")
496
- if (
497
- isinstance(executor_provider, str)
498
- and isinstance(executor_model, str)
499
- and executor_model
500
- and models.get(executor_provider)
501
- and models[executor_provider] != executor_model
502
- ):
503
- raise JsonBoundaryError(
504
- run_manifest,
505
- "run manifest",
506
- f"executor model conflicts with provider {executor_provider}",
507
- )
516
+ rows = _role_selection_rows(run)
517
+ adjustable = _adjustable_roles(run_manifest, run.get("taskType"))
518
+ counts: dict[str, int] = {}
519
+ for role, _model_ref in rows:
520
+ counts[role] = counts.get(role, 0) + 1
508
521
  return (
509
522
  "# Okstra Rerun Input\n\n"
510
523
  + _line("Project ID", run.get("projectId"))
@@ -512,13 +525,13 @@ def render_rerun_input(run_manifest: Path) -> str:
512
525
  + _line("Task ID", run.get("taskId"))
513
526
  + _line("Task type", run.get("taskType"))
514
527
  + _line("Task brief", run.get("taskBriefPath"))
515
- + _line("Workers", _worker_providers_csv(run.get("recommendedWorkers")))
516
528
  + _line("Related tasks", _csv(run.get("relatedTasks")))
517
- + _line("Claude model", models.get("claude"))
518
- + _line("Codex model", models.get("codex"))
519
- + _line("Antigravity model", models.get("antigravity"))
520
- + _line("Executor provider", executor.get("provider"))
521
- + _line("Executor model", executor.get("model"))
529
+ + "".join(
530
+ _line("Role count", f"{role}={count}")
531
+ for role, count in counts.items()
532
+ if role in adjustable
533
+ )
534
+ + "".join(_line("Role model", f"{role}={model_ref}") for role, model_ref in rows)
522
535
  )
523
536
 
524
537
 
@@ -6,7 +6,7 @@
6
6
  "mode": "static",
7
7
  "roleId": "analyser",
8
8
  "dutyId": "analysis-worker",
9
- "min": 2,
9
+ "min": 1,
10
10
  "recommended": 3,
11
11
  "max": 5
12
12
  },
@@ -1,14 +1,6 @@
1
1
  # Change Impact Analysis Profile
2
2
 
3
3
  - Purpose: assess the read-only impact of a proposed change, including preserved behavior, affected dependencies, and constraints that a later planning phase must resolve
4
- - Required workers:
5
- - claude
6
- - codex
7
- - report-writer
8
- - Optional workers (opt-in via `--workers`):
9
- - antigravity
10
- - grok — optional adversarial analyser/critic through the Grok CLI wrapper
11
- - kimi — optional long-context analyser/critic through the Kimi CLI wrapper
12
4
  {{INCLUDE:_common-contract.md}}
13
5
  - Phase 1.5 questions:
14
6
  - What user intent and preserved behavior define the proposed change boundary?
@@ -6,7 +6,7 @@
6
6
  "mode": "static",
7
7
  "roleId": "analyser",
8
8
  "dutyId": "diagnosis-worker",
9
- "min": 2,
9
+ "min": 1,
10
10
  "recommended": 3,
11
11
  "max": 5
12
12
  },
@@ -1,14 +1,6 @@
1
1
  # Error Analysis Profile
2
2
 
3
3
  - Purpose: analyse reported errors or incidents and identify likely causes, missing evidence, and validation paths
4
- - Required workers:
5
- - claude
6
- - codex
7
- - report-writer
8
- - Optional workers (opt-in via `--workers`):
9
- - antigravity — when added to the roster it joins the analyser set; omitted by default
10
- - grok — optional adversarial analyser/critic through the Grok CLI wrapper
11
- - kimi — optional long-context analyser/critic through the Kimi CLI wrapper
12
4
  {{INCLUDE:_common-contract.md}}
13
5
  - Brief consumption (phase-specific addendum — shared rules live in `_common-contract.md` under "Brief handoff contract"):
14
6
  - Apply the shared reporter-confirmation precondition exactly as written. In this phase, unresolved `intent-check:` / `conversion-block:` rows use `Blocks=next-phase`; any unconfirmed inference may be used as a labelled hypothesis only.
@@ -26,6 +18,12 @@
26
18
  - **Graph-aware scope:** a graph edge can explain ordering or duplication, but it is not proof of cause by itself. Cite code/log evidence before claiming an upstream related task caused the current symptom.
27
19
  - **Sharp next diagnostic:** end with the single highest-value diagnostic command, log capture, or file inspection that should happen next, plus the expected signal that would confirm or reject the leading cause.
28
20
  - **Fix-design boundary:** do not design the implementation fix beyond what is necessary to validate the cause. Record whether the cause is credible and the next diagnostic. The common Phase Routing settles the next phase.
21
+ - Census aspects:
22
+ - requirement pass-observation: Which observation would show the symptom resolved for this requirement?
23
+ - task symptom-lock: Which single observable failure condition does the reported symptom translate to?
24
+ - task reproduction: Was the symptom reproduced, not reproduced, or blocked before reproduction, and on what evidence?
25
+ - task cause-falsification: For each cause candidate, which evidence that could falsify it was checked?
26
+ - task next-diagnostic: Which diagnostic should run next, and which signal would confirm or reject the leading cause?
29
27
  - Structured diagnosis and routing contract:
30
28
  - `errorAnalysis` is the source of truth for reproduction status, `EA-NNN` cause candidates, the sharp next diagnostic, and the next route.
31
29
  - Record the route selected by the lead through the common Phase Routing rules, its `routing.leadingCauseId`, and the matching direction.
@@ -6,7 +6,7 @@
6
6
  "mode": "static",
7
7
  "roleId": "analyser",
8
8
  "dutyId": "analysis-worker",
9
- "min": 2,
9
+ "min": 1,
10
10
  "recommended": 3,
11
11
  "max": 5
12
12
  },
@@ -1,14 +1,6 @@
1
1
  # Feature Analysis Profile
2
2
 
3
3
  - Purpose: analyse a confirmed feature target's behavior, rules, state changes, external calls, and test coverage scope without designing or changing an implementation
4
- - Required workers:
5
- - claude
6
- - codex
7
- - report-writer
8
- - Optional workers (opt-in via `--workers`):
9
- - antigravity
10
- - grok — optional adversarial analyser/critic through the Grok CLI wrapper
11
- - kimi — optional long-context analyser/critic through the Kimi CLI wrapper
12
4
  {{INCLUDE:_common-contract.md}}
13
5
  - Phase 1.5 questions:
14
6
  - What confirmed target and user intent bound this analysis?
@@ -6,7 +6,7 @@
6
6
  "mode": "static",
7
7
  "roleId": "verifier",
8
8
  "dutyId": "acceptance-verifier",
9
- "min": 2,
9
+ "min": 1,
10
10
  "recommended": 2,
11
11
  "max": 5
12
12
  },
@@ -1,14 +1,6 @@
1
1
  # Final Verification Profile
2
2
 
3
3
  - Purpose: judge the delivered implementation on three axes before final acceptance — does it cover every requirement (under-delivery), does it carry work no requirement asked for (over-delivery), and does it actually do what it claims (defects, and tests that verify nothing). Whether the run followed okstra's own procedure is not one of the axes: the runtime and `validators/validate-run.py` own that, and a finding about it is not an acceptance judgement
4
- - Required workers:
5
- - claude
6
- - codex
7
- - report-writer
8
- - Optional workers (opt-in via `--workers`):
9
- - antigravity — when added to the roster it joins the analyser set; omitted by default
10
- - grok — optional read-only adversarial analyser/critic through the Grok CLI wrapper
11
- - kimi — optional read-only long-context analyser/critic through the Kimi CLI wrapper
12
4
  {{INCLUDE:_common-contract.md}}
13
5
  - Primary focus areas (each maps to a deliverable section below):
14
6
  - Acceptance-gating — a failure here pushes the verdict toward `blocked` / `conditional-accept`:
@@ -56,6 +48,8 @@
56
48
  - **Design-preparation carry-in:** read only source implementation report `missingInformation` rows whose `source` starts with `design-prep:`. Recommend `ifStillOpen: block` as an acceptance blocker and `ifStillOpen: follow-up` as residual risk. This phase does not read planning PREP sidecars directly or mutate planning snapshots.
57
49
  - **Could-not-verify honesty:** use `not-configured`, `env-unavailable`, `rejected`, `gap`, or `blocked` as appropriate. Never convert unavailable evidence into an executed/pass claim.
58
50
  - **Source-mutation prohibition:** verification may write only assigned okstra run artifacts. Do not edit source, schema, deployment, lockfile, or configuration files; route detected defects to a later phase.
51
+ - Census aspects:
52
+ - file added-surface: Does this changed file add behaviour, configuration, or surface no requirement asked for, and does what it adds work?
59
53
  - Required deliverable shape (final report, in addition to the standard sections):
60
54
  - **Source Implementation Report(s)** (**Enforced:** `phases/final_verification/validation.py` `validate_verification_target_match` (called by `validators/validate-run.py`) compares `verificationScope`, `worktreePath`, `implementationBaseRef`, `capturedHeadSha`, and the `stageReports` stage set against the digest-verified `instruction-set/verification-target.md`; a snapshot whose digest no longer checks out is ignored rather than trusted. `verificationScope` in particular gates both stage-group eligibility and release-handoff routing, so it is not the report's to restate): the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base/head refs, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). Every analyser prompt carries the same compact target identity (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification head ref:** / **Verification target path:** / **Verification target digest:**`) and reads the sidecar on demand for the complete diff stat. A worker that cannot confirm its analysis ran against that worktree's delivered diff MUST record a `tool-failure`.
61
55
  - **Verdict vocabulary**: Section 7 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). `blocksReleaseHandoff` is a gate, not a note: `false` means this condition alone would not stop the release, and a `conditional-accept` whose conditions are all `false` may route to `release-handoff` (the conditions travel into the PR body as unresolved items). Declare `true` for anything that must be settled before release. The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is release-ready by `okstra_ctl.release_gate.release_handoff_allowed`. **Any Acceptance Blocker therefore forces the verdict off `accepted` (to `conditional-accept` or `blocked`); the gates below cite this rule instead of restating the arithmetic.**
@@ -7,7 +7,7 @@
7
7
  "production credentials, deploy commands, infra mutation (`terraform apply`, `kubectl apply` against non-local cluster, etc.)",
8
8
  "external API write calls (POST/PUT/PATCH/DELETE) to third-party services other than localhost test fixtures",
9
9
  "source edits or Bash mutations performed by any verifier role (`Antigravity verifier`, `Codex verifier`, `Claude verifier` are read-only — recommend, do not apply)",
10
- "dispatching parallel sub-agents beyond the required worker roster",
10
+ "dispatching parallel sub-agents beyond the run's selected roster",
11
11
  "silent scope expansion: every file edited outside the approved plan list MUST appear in the `Out-of-plan edits` block with rationale",
12
12
  "leaving placeholders such as TBD / TODO / \"implement later\" / \"handle edge cases\" in newly-added lines of this run (check via `git diff <base>..HEAD | grep -E '^\\+[^+].*\\b(TBD|TODO|FIXME|XXX|implement later|handle edge cases|similar to|placeholder)\\b'`; pre-existing strings in untouched regions are out of scope)",
13
13
  "lead substituting its own verdict when every verifier present in the resolved roster returned a non-result terminal status (`timeout`/`error`/`not-run`); in that case the run MUST end as `blocked` with routing recommendation back to `error-analysis`, never with a lead-only verdict",
@@ -14,7 +14,7 @@
14
14
  "mode": "static",
15
15
  "roleId": "verifier",
16
16
  "dutyId": "implementation-verifier",
17
- "min": 2,
17
+ "min": 1,
18
18
  "recommended": 2,
19
19
  "max": 3
20
20
  },
@@ -3,12 +3,6 @@
3
3
  - Purpose: realise the approved `implementation-planning` deliverable as actual source changes, with cross-model verification, while keeping the run reversible
4
4
  - **Run-level fixed cost:** the verifier set, Phase 5.5 convergence, and the Phase 6 report-writer run exactly once per implementation run, over this run's single stage diff — never once per step.
5
5
  - **Fix run (profile carries a "Fix-Run Carry" block):** the executor's scope is the carried blocking findings plus the previous routing recommendation — it MUST NOT re-execute plan steps the previous run completed. Verifiers apply the "Fix-run incremental scope" section of `_implementation-verifier.md`; the report writer applies "Fix-run incremental authoring" in `report-writer.md`. The full validation-command re-run is NOT reduced.
6
- - Required workers:
7
- - claude
8
- - codex
9
- - report-writer
10
- - Optional workers (opt-in via `--workers`):
11
- - antigravity — when added to the roster it joins the verifier set; when omitted only the default Claude+Codex verifiers participate. `--executor antigravity` requires `antigravity` in the roster: the direct CLI demands it explicitly in `--workers`, while the wizard adds it automatically when you pick antigravity as the executor.
12
6
  - **Executor binding (resolved at run-prep time, fixed for this run):**
13
7
  - Executor display name: `{{EXECUTOR_DISPLAY_NAME}}`
14
8
  - Executor worker ID: `{{EXECUTOR_WORKER_ID}}`
@@ -73,7 +73,7 @@ taskType: "{{FM_TASK_TYPE}}"
73
73
 
74
74
  - `git push`, publish, deploy, migration execution, third-party write APIs, production credentials
75
75
  - Edits or Bash mutations performed by any verifier role
76
- - Sub-agent dispatch beyond the required worker roster
76
+ - Sub-agent dispatch beyond the run's selected roster
77
77
  - Silent scope expansion — every file touched outside the plan's list MUST be recorded in `Out-of-plan edits` (**Enforced (declared edits only):** `_validate_out_of_plan_edits_are_real`)
78
78
 
79
79
  ## Verifier Worker Inputs
@@ -9,7 +9,7 @@ from .validation import (
9
9
  MAX_RANKED_OPTIONS,
10
10
  MAX_RAW_CANDIDATES_PER_ANALYSER,
11
11
  MIN_CRITERION_VALUE,
12
- MIN_FEASIBLE_VOTES,
12
+ required_feasible_votes,
13
13
  )
14
14
  from okstra_ctl.exact_coverage import COVERAGE_VERDICT_PRECEDENCE
15
15
 
@@ -51,8 +51,9 @@ def requirement_instructions(original_requirement_ids: Sequence[str], participat
51
51
  "otherwise).",
52
52
  "A ranked option is valid only when every participating analyser "
53
53
  "supplied one feasibility vote, at least "
54
- f"{MIN_FEASIBLE_VOTES} votes are `feasible`, and both `safetyBlockers` "
55
- "and `unresolvedFeasibilityFacts` are empty.",
54
+ f"{required_feasible_votes(participating_analysers)} of them are "
55
+ "`feasible`, and both `safetyBlockers` and `unresolvedFeasibilityFacts` "
56
+ "are empty.",
56
57
  ("Classify each `unresolvedFeasibilityFacts` entry with `resolutionKind`: "
57
58
  "`user-decision` requires nonempty `clarificationRefs` linking that fact "
58
59
  "to actual C-NNN records, including answered records; `technical-verification` "
@@ -1,9 +1,7 @@
1
- """방향 비교가 필요한 요구사항과 독립 분석자 수를 확인한다."""
1
+ """방향 비교가 필요한 요구사항을 확인한다."""
2
2
  from __future__ import annotations
3
3
 
4
4
  from pathlib import Path
5
- from typing import Sequence
6
-
7
5
  from okstra_ctl.prepare_error import PrepareError
8
6
  from okstra_ctl.scope_provenance import brief_end_state_id_sequence
9
7
 
@@ -14,14 +12,3 @@ def validate_brief_requirements(brief_path: Path) -> None:
14
12
  "regenerate the brief with stable end-state IDs before starting "
15
13
  "implementation-option-selection"
16
14
  )
17
-
18
-
19
- def validate_analyser_roster(
20
- workers: Sequence[str]
21
- ) -> None:
22
- """Require independent analysis from three workers before option selection."""
23
- analyser_count = sum(worker != "report-writer" for worker in workers)
24
- if analyser_count < 3:
25
- raise PrepareError(
26
- "implementation-option-selection requires at least 3 analyser workers"
27
- )
@@ -6,7 +6,7 @@
6
6
  "mode": "static",
7
7
  "roleId": "designer",
8
8
  "dutyId": "direction-selection-worker",
9
- "min": 3,
9
+ "min": 1,
10
10
  "recommended": 3,
11
11
  "max": 5
12
12
  },
@@ -1,14 +1,6 @@
1
1
  # Implementation Option Selection Profile
2
2
 
3
3
  - Purpose: compare feasible implementation directions before planning, preserving a read-only record of the evidence and trade-offs that selects the direction to plan
4
- - Required workers:
5
- - claude
6
- - codex
7
- - antigravity
8
- - report-writer
9
- - Optional workers (opt-in via `--workers`):
10
- - grok
11
- - kimi
12
4
  {{INCLUDE:_common-contract.md}}
13
5
  - Brief consumption:
14
6
  - Apply the shared reporter-confirmation precondition exactly as written. Unresolved `intent-check:` and `conversion-block:` rows use `Blocks=next-phase`.
@@ -18,6 +10,11 @@
18
10
  - In `candidate-comparison` mode only, submit at most three candidates. A candidate must be feasible from inspected evidence, not from an assumed future change.
19
11
  - In `preselected-validation` mode, receive one preselected direction from the lead and validate its evidence, counterevidence, criterion scores, and requirement mappings, and state the same feasibility verdict with its rationale and counterevidence. The worker must not generate new candidates.
20
12
  - Do not produce detailed file lists, stage maps, execution commands, or a plan approval request.
13
+ - Census aspects:
14
+ - requirement mapping: Which candidate or preselected direction satisfies this requirement, and how?
15
+ - requirement support: Which inspected evidence supports that mapping?
16
+ - requirement counter-evidence: What is the strongest evidence against that mapping?
17
+ - task feasibility: Is each candidate feasible, not feasible, or uncertain on inspected evidence?
21
18
  - Pre-selection context exploration:
22
19
  - In `candidate-comparison` mode, inspect the code paths, interfaces, tests, and constraints needed to distinguish candidates before assigning scores.
23
20
  - In `preselected-validation` mode, inspect the code paths, interfaces, tests, and constraints needed to validate the one preselected direction.