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
@@ -784,7 +784,7 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
784
784
  catalog = _worker_catalog(ctx)
785
785
  worker_dispatch_plan = _worker_dispatch_plan(ctx)
786
786
  workers = []
787
- for row in _optional_worker_roles(ctx):
787
+ for row in _critic_role_rows(ctx):
788
788
  workers.append({
789
789
  **{key: row[key] for key in (
790
790
  "workerId", "role", "agent", "provider", "runner",
@@ -1180,7 +1180,7 @@ def migrate_legacy_run_artifacts(ctx: dict) -> None:
1180
1180
  # --------------------------------------------------------------------------- #
1181
1181
 
1182
1182
 
1183
- def _required_worker_roles(ctx: dict, reviewers: list[str]) -> list[dict]:
1183
+ def _worker_role_rows(ctx: dict, reviewers: list[str]) -> list[dict]:
1184
1184
  catalog = _worker_catalog(ctx)
1185
1185
  return [
1186
1186
  {
@@ -1199,7 +1199,7 @@ def _required_worker_roles(ctx: dict, reviewers: list[str]) -> list[dict]:
1199
1199
  ]
1200
1200
 
1201
1201
 
1202
- def _optional_worker_roles(ctx: dict) -> list[dict]:
1202
+ def _critic_role_rows(ctx: dict) -> list[dict]:
1203
1203
  """선택해 배정한 비평 역할. 선택 후에는 실행 또는 미실행 사유가 필요하다.
1204
1204
 
1205
1205
  `invocationAssignments`의 `critic/*` 배정만 선언하므로 역할을 선택하지
@@ -1412,7 +1412,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1412
1412
  reviewers = _resolve_workers(ctx)
1413
1413
  worker_dispatch_plan = _worker_dispatch_plan(ctx)
1414
1414
  catalog = _worker_catalog(ctx)
1415
- required_worker_roles = _required_worker_roles(ctx, reviewers)
1415
+ worker_role_rows = _worker_role_rows(ctx, reviewers)
1416
1416
  worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
1417
1417
  lead_agent = _lead_agent(ctx)
1418
1418
  lead_role = _lead_role(ctx)
@@ -1557,13 +1557,10 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1557
1557
  "finalReportFormat": "markdown",
1558
1558
  "finalReportFilename": ctx.get("FINAL_REPORT_FILENAME", ""),
1559
1559
  "finalStatusFilename": ctx.get("FINAL_STATUS_FILENAME", ""),
1560
- "minimumPreferredWorkerResults": len(reviewers),
1561
- "requiredWorkerAttempts": reviewers,
1562
- "requiredWorkerRoles": required_worker_roles,
1563
- "optionalWorkerRoles": _optional_worker_roles(ctx),
1560
+ "workerRoles": worker_role_rows,
1561
+ "criticRoles": _critic_role_rows(ctx),
1564
1562
  "requiredAgentStatusEntries": required_agent_status_entries,
1565
1563
  "requireDistinctLeadFromWorkerSession": True,
1566
- "requireAllRequiredWorkerAttempts": True,
1567
1564
  "requireAntigravityWorkerAttempt": "antigravity" in reviewers,
1568
1565
  "requireCollectedWorkerStatusesBeforeFinalVerdict": True,
1569
1566
  "disallowLeadSoloAnalysisAsWorkerResult": True,
@@ -1574,6 +1571,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1574
1571
  "Safe or Reasonable Areas",
1575
1572
  "Uncertain Points",
1576
1573
  "Recommended Next Actions",
1574
+ "Coverage Verdicts",
1577
1575
  ],
1578
1576
  "finalReportSections": [
1579
1577
  "Problem or Validation Summary",
@@ -1736,7 +1734,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1736
1734
  reviewers = _resolve_workers(ctx)
1737
1735
  worker_dispatch_plan = _worker_dispatch_plan(ctx)
1738
1736
  catalog = _worker_catalog(ctx)
1739
- required_worker_roles = _required_worker_roles(ctx, reviewers)
1737
+ worker_role_rows = _worker_role_rows(ctx, reviewers)
1740
1738
  worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
1741
1739
  lead_agent = _lead_agent(ctx)
1742
1740
  lead_role = _lead_role(ctx)
@@ -1903,18 +1901,15 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1903
1901
  "leadModelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
1904
1902
  "leadExecutionMode": "synthesis-only",
1905
1903
  "finalSynthesisOwner": lead_role,
1906
- "requiredWorkerAttempts": reviewers,
1907
- "requiredWorkerRoles": required_worker_roles,
1908
- "optionalWorkerRoles": _optional_worker_roles(ctx),
1904
+ "workerRoles": worker_role_rows,
1905
+ "criticRoles": _critic_role_rows(ctx),
1909
1906
  "requiredAgentStatusEntries": [lead_role]
1910
1907
  + [catalog[item]["role"] for item in reviewers],
1911
1908
  "requireDistinctLeadFromWorkerSession": True,
1912
- "requireAllRequiredWorkerAttempts": True,
1913
1909
  "requireAntigravityWorkerAttempt": "antigravity" in reviewers,
1914
1910
  "requireCollectedWorkerStatusesBeforeFinalVerdict": True,
1915
1911
  "disallowLeadSoloAnalysisAsWorkerResult": True,
1916
1912
  "disallowGenericParallelOnlyExecution": True,
1917
- "preferredCompletedWorkerResults": len(reviewers),
1918
1913
  "executor": _executor_contract(ctx),
1919
1914
  },
1920
1915
  "validation": {
@@ -2696,7 +2691,6 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2696
2691
  model_assignment_lines = [
2697
2692
  fmt_assignment(lead_role, lead_model, lead_model_execution)
2698
2693
  ]
2699
- worker_role_labels: list[str] = []
2700
2694
  execution_status_entries = [f"`{lead_role}`"]
2701
2695
  execution_status_table_lines = [
2702
2696
  "| 에이전트 | 역할 | 모델 | 상태 | 핵심 발견 요약 |",
@@ -2714,35 +2708,11 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2714
2708
  model_assignment_lines.append(
2715
2709
  fmt_assignment(m["role"], m["model"], m["modelExecutionValue"])
2716
2710
  )
2717
- worker_role_labels.append(f"`{m['role']}`")
2718
2711
  execution_status_entries.append(f"`{m['role']}`")
2719
2712
  execution_status_table_lines.append(
2720
2713
  f"| {m['agentLabel']} | {m['role']} | {m['model']} | completed / timeout / error / not-run | {m['role']}의 핵심 발견 요약 |"
2721
2714
  )
2722
2715
 
2723
- if worker_role_labels:
2724
- if len(worker_role_labels) == 1:
2725
- worker_role_sentence = (
2726
- f"- {worker_role_labels[0]} is the required worker role."
2727
- )
2728
- else:
2729
- worker_role_sentence = (
2730
- f"- {', '.join(worker_role_labels[:-1])}, "
2731
- f"and {worker_role_labels[-1]} are the required worker roles."
2732
- )
2733
- preferred_results_sentence = (
2734
- f"- Aim to collect completed results from all "
2735
- f"{len(worker_role_labels)} required workers."
2736
- )
2737
- else:
2738
- worker_role_sentence = "- No worker roles were selected for this run."
2739
- preferred_results_sentence = "- No worker results are expected for this run."
2740
- worker_attempt_sentence = (
2741
- "- `Antigravity worker` is mandatory to attempt for this workflow."
2742
- if "antigravity" in selected
2743
- else "- `Antigravity worker` is not selected for this run, so no Antigravity attempt is required."
2744
- )
2745
-
2746
2716
  task_type = ctx.get("TASK_TYPE", "")
2747
2717
  adapter_setup_facts: list[str] = []
2748
2718
  if lead_runtime == "claude-code":
@@ -2865,9 +2835,6 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2865
2835
  ctx["WORKER_RESULT_PATH_LINES"] = "\n".join(worker_result_lines)
2866
2836
  ctx["MODEL_ASSIGNMENT_LINES"] = "\n".join(model_assignment_lines)
2867
2837
  ctx["TEAM_ROLE_LINES"] = "\n".join(team_role_lines)
2868
- ctx["REQUIRED_WORKER_ROLE_SENTENCE"] = worker_role_sentence
2869
- ctx["ANTIGRAVITY_ATTEMPT_SENTENCE"] = worker_attempt_sentence
2870
- ctx["PREFERRED_WORKER_RESULTS_SENTENCE"] = preferred_results_sentence
2871
2838
  ctx["EXECUTION_STATUS_EXACT_ENTRIES"] = ", ".join(execution_status_entries)
2872
2839
  ctx["EXECUTION_STATUS_TABLE_ROWS"] = "\n".join(execution_status_table_lines)
2873
2840
 
@@ -817,6 +817,9 @@ def render_to_file(
817
817
  raise _with_excerpt_drift_hint(exc, data_path, str(task_type or "")) from exc
818
818
 
819
819
  output_path.parent.mkdir(parents=True, exist_ok=True)
820
+ from .business_flow.hooks import markdown_report_links
821
+
822
+ rendered += markdown_report_links(data_path)
820
823
  tmp = output_path.with_suffix(output_path.suffix + f".tmp.{os.getpid()}")
821
824
  tmp.write_text(rendered, encoding="utf-8")
822
825
  tmp.replace(output_path)
@@ -25,6 +25,7 @@ from .report_synthesis_packet import (
25
25
  verify_report_synthesis_packet_sources,
26
26
  )
27
27
  from .conformance import strip_stage_declaration_label
28
+ from .coverage_census import census_warning_rows, read_census_state
28
29
  from .design_prep import DesignPrepError, materialize_design_prep_requests
29
30
  from .phases.implementation_option_selection.validation import validate_blocked_answer_channel
30
31
  from .phases.technical_verification.validation import validate_technical_verification_report
@@ -498,7 +499,7 @@ def _compose(
498
499
  data["verificationScope"] = scope
499
500
  data["agentActivity"] = activities
500
501
  data["evidence"] = {"primary": _promoted_evidence(convergence_data)}
501
- data["missingInformation"] = []
502
+ data["missingInformation"] = _census_missing_information(project_root, manifest)
502
503
  # 스냅샷 복사가 _attach_metadata 보다 먼저다 — frontmatter 가
503
504
  # selectedDirectionRef.snapshotPath 를 읽는다.
504
505
  _apply_selected_direction_snapshot(data, project_root, manifest)
@@ -509,6 +510,19 @@ def _compose(
509
510
  return data
510
511
 
511
512
 
513
+ def _census_missing_information(
514
+ project_root: Path, manifest: Mapping[str, Any],
515
+ ) -> list[dict[str, Any]]:
516
+ """Coverage-census warnings are the only rows this field carries under the
517
+ v3 contract, so they number from `R-001`."""
518
+ state = read_census_state(project_root, manifest)
519
+ if state is None:
520
+ return []
521
+ _project_id, _task_group, task_id = _identity(manifest)
522
+ rows = census_warning_rows(state, ticket_id=task_id or "unknown")
523
+ return [{"id": f"R-{index:03d}", **row} for index, row in enumerate(rows, start=1)]
524
+
525
+
512
526
  def _selected_direction_inputs(
513
527
  project_root: Path, manifest: Mapping[str, Any]
514
528
  ) -> tuple[Path, Path]:
@@ -51,6 +51,7 @@ from typing import Any, Callable, Mapping, Sequence
51
51
  from . import group_context
52
52
  from .agent.activity import ActivityProjectionError, project_agent_activity
53
53
  from .report_contract import apply_execution_roles
54
+ from .coverage_census import census_warnings, read_census_state
54
55
  from .report_assembly import ReportAssemblyError, assemble_report
55
56
  from .dispatch_state import (
56
57
  DispatchError,
@@ -722,9 +723,35 @@ def run_finalize(
722
723
  # closeout 이 실제로 건넬 명령. 표를 리드의 기억에 맡기지 않는다.
723
724
  payload["nextCommand"] = closeout_command(payload)
724
725
  payload["reportPaths"] = _closeout_report_paths(ctx)
726
+ for step in steps:
727
+ if step["name"] == "business-flow":
728
+ payload["businessProcess"] = step["result"]
729
+ # 선택 단계라 exitCode 는 0 이다. 실패를 중첩 JSON 에만 두면 리드가 못 본다.
730
+ failed = [
731
+ {"id": row.get("id", ""), "mode": row.get("mode", ""), "error": row.get("error", "")}
732
+ for row in step["result"].get("executions") or []
733
+ if isinstance(row, dict) and row.get("status") == "failed"
734
+ ]
735
+ if step["result"].get("error"):
736
+ failed.append({"id": "", "mode": "", "error": str(step["result"]["error"])})
737
+ if failed:
738
+ payload["businessFlowWarnings"] = failed
739
+ census = _census_summary(ctx)
740
+ if census is not None:
741
+ payload["censusWarnings"] = census
725
742
  return payload
726
743
 
727
744
 
745
+ def _census_summary(ctx: FinalizeContext) -> dict[str, Any] | None:
746
+ """Unverdicted census cells per worker, for the lead's completion line."""
747
+ try:
748
+ manifest = load_owned_object(ctx.manifest_path, artifact="run manifest")
749
+ except JsonBoundaryError as exc:
750
+ return {"auditProblem": f"the run manifest could not be read: {exc}"}
751
+ state = read_census_state(ctx.project_root, manifest)
752
+ return census_warnings(state) if state is not None else None
753
+
754
+
728
755
  def _execute_finalize_steps(
729
756
  ctx: FinalizeContext, commands: Sequence[tuple[str, list[str]]],
730
757
  before_step: Callable[[str], None] | None,
@@ -768,11 +795,34 @@ def _execute_finalize_steps(
768
795
  first_failure = f"{name} failed with exit code {result.returncode}"
769
796
  if name == STEP_VALIDATE_RUN and result.returncode == 0:
770
797
  validated = True
798
+ if not first_failure:
799
+ business = _finalize_business_effects(ctx)
800
+ if business is not None:
801
+ steps.append({"name": "business-flow", "exitCode": 0, "result": business})
771
802
  if validated and not first_failure:
772
803
  _record_run_end(ctx.team_state_path)
773
804
  return steps, first_failure
774
805
 
775
806
 
807
+ def _finalize_business_effects(ctx: FinalizeContext) -> dict[str, Any] | None:
808
+ from .business_flow.hooks import finalize_business_flow
809
+
810
+ result = None
811
+ try:
812
+ result = finalize_business_flow(ctx.project_root, _load_manifest(ctx.manifest_path), ctx.data_path)
813
+ if result is not None and result.get("executions"):
814
+ from .render_final_report import render_to_file
815
+ from .report_html.render import render_v2_html_view
816
+ from .report_html.models import HtmlRunMeta
817
+
818
+ render_to_file(ctx.data_path, ctx.markdown_path)
819
+ render_v2_html_view(ctx.data_path, run_meta=HtmlRunMeta(
820
+ ctx.task_key, ctx.task_type, ctx.seq, str(ctx.markdown_path)))
821
+ return result
822
+ except Exception as exc:
823
+ return {**result, "viewError": str(exc)} if result is not None else {"status": "failed", "error": str(exc), "executions": []}
824
+
825
+
776
826
  def _finalize_recovery(ctx: FinalizeContext, result: Mapping[str, Any]) -> dict[str, Any]:
777
827
  """새 실행 대신 실패한 처리부터 재개할 명령과 원본 소유자 오류를 전달한다."""
778
828
  order = V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
@@ -226,6 +226,9 @@ def render_v2_html_view(
226
226
  "css": (root / "html/assets/base.css").read_text(encoding="utf-8"),
227
227
  "js": response_js + "\n" + base_js,
228
228
  }
229
+ from ..business_flow.hooks import report_links
230
+
231
+ context["businessFlow"] = report_links(data_path)
229
232
  output_path = _html_path(data_path)
230
233
  document = env.get_template(route.template_name).render(**context)
231
234
  document = inject_report_index(document, label=translate("base.contents"))
@@ -364,8 +364,7 @@ _SOURCE_FIELDS = (
364
364
  _READ_CHUNK_BYTES = 16_000
365
365
  _SHARED_TEXT_MIN_BYTES = 256
366
366
  _ANALYSER_OPERATION_SECTIONS = frozenset({
367
- "Required workers", "Optional workers", "Team contract",
368
- "Worker interaction model", "Tooling — read-only MCP availability",
367
+ "Team contract", "Worker interaction model", "Tooling — read-only MCP availability",
369
368
  "Run-scoped worker-resource lifecycle",
370
369
  })
371
370