briefloop 0.11.12__py3-none-any.whl

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 (424) hide show
  1. briefloop-0.11.12.dist-info/METADATA +473 -0
  2. briefloop-0.11.12.dist-info/RECORD +424 -0
  3. briefloop-0.11.12.dist-info/WHEEL +5 -0
  4. briefloop-0.11.12.dist-info/entry_points.txt +3 -0
  5. briefloop-0.11.12.dist-info/licenses/LICENSE +22 -0
  6. briefloop-0.11.12.dist-info/top_level.txt +1 -0
  7. multi_agent_brief/__init__.py +36 -0
  8. multi_agent_brief/analysis_blocks/__init__.py +11 -0
  9. multi_agent_brief/analysis_blocks/builder.py +206 -0
  10. multi_agent_brief/analysis_blocks/renderer.py +237 -0
  11. multi_agent_brief/analysis_blocks/schemas.py +52 -0
  12. multi_agent_brief/analysis_modules/__init__.py +8 -0
  13. multi_agent_brief/analysis_modules/base.py +87 -0
  14. multi_agent_brief/analysis_modules/market_competitor/__init__.py +68 -0
  15. multi_agent_brief/analysis_modules/market_competitor/auditor.py +290 -0
  16. multi_agent_brief/analysis_modules/market_competitor/config.py +187 -0
  17. multi_agent_brief/analysis_modules/market_competitor/event_builder.py +226 -0
  18. multi_agent_brief/analysis_modules/market_competitor/renderer.py +199 -0
  19. multi_agent_brief/analysis_modules/market_competitor/schemas.py +518 -0
  20. multi_agent_brief/analysis_modules/policy_regulatory/__init__.py +7 -0
  21. multi_agent_brief/analysis_modules/policy_regulatory/audit.py +195 -0
  22. multi_agent_brief/analysis_modules/policy_regulatory/module.py +387 -0
  23. multi_agent_brief/analysis_modules/policy_regulatory/schemas.py +129 -0
  24. multi_agent_brief/analysis_modules/registry.py +73 -0
  25. multi_agent_brief/audience/__init__.py +19 -0
  26. multi_agent_brief/audience/profiles.py +246 -0
  27. multi_agent_brief/audience_memory/__init__.py +23 -0
  28. multi_agent_brief/audience_memory/profile.py +358 -0
  29. multi_agent_brief/audit/__init__.py +36 -0
  30. multi_agent_brief/audit/case_applicability.py +181 -0
  31. multi_agent_brief/audit/deterministic.py +471 -0
  32. multi_agent_brief/audit/editorial_governance.py +404 -0
  33. multi_agent_brief/audit/final_quality.py +764 -0
  34. multi_agent_brief/audit/harness.py +242 -0
  35. multi_agent_brief/audit/interfaces.py +96 -0
  36. multi_agent_brief/audit/limitation_hygiene.py +238 -0
  37. multi_agent_brief/audit/proposal_boundary.py +34 -0
  38. multi_agent_brief/audit/redaction.py +27 -0
  39. multi_agent_brief/audit/rule_packs.py +96 -0
  40. multi_agent_brief/audit/semantic.py +435 -0
  41. multi_agent_brief/capabilities/__init__.py +22 -0
  42. multi_agent_brief/capabilities/catalog.py +215 -0
  43. multi_agent_brief/capabilities/detect.py +199 -0
  44. multi_agent_brief/capabilities/models.py +60 -0
  45. multi_agent_brief/capabilities/recommend.py +206 -0
  46. multi_agent_brief/cli/__init__.py +2 -0
  47. multi_agent_brief/cli/analysis_commands.py +140 -0
  48. multi_agent_brief/cli/approval_commands.py +107 -0
  49. multi_agent_brief/cli/audit_commands.py +59 -0
  50. multi_agent_brief/cli/capability_commands.py +548 -0
  51. multi_agent_brief/cli/claude_commands.py +159 -0
  52. multi_agent_brief/cli/competitors_commands.py +182 -0
  53. multi_agent_brief/cli/controls_commands.py +137 -0
  54. multi_agent_brief/cli/deliver_commands.py +942 -0
  55. multi_agent_brief/cli/eval_cases_commands.py +99 -0
  56. multi_agent_brief/cli/experiments_commands.py +395 -0
  57. multi_agent_brief/cli/feedback_commands.py +192 -0
  58. multi_agent_brief/cli/finalize_commands.py +153 -0
  59. multi_agent_brief/cli/gates_commands.py +174 -0
  60. multi_agent_brief/cli/hermes_commands.py +356 -0
  61. multi_agent_brief/cli/improve_commands.py +224 -0
  62. multi_agent_brief/cli/init_commands.py +564 -0
  63. multi_agent_brief/cli/init_wizard.py +1473 -0
  64. multi_agent_brief/cli/input_commands.py +237 -0
  65. multi_agent_brief/cli/main.py +288 -0
  66. multi_agent_brief/cli/onboard_commands.py +193 -0
  67. multi_agent_brief/cli/product_commands.py +1449 -0
  68. multi_agent_brief/cli/provenance_commands.py +100 -0
  69. multi_agent_brief/cli/release_commands.py +68 -0
  70. multi_agent_brief/cli/repair_commands.py +169 -0
  71. multi_agent_brief/cli/run_commands.py +292 -0
  72. multi_agent_brief/cli/runtime_commands.py +81 -0
  73. multi_agent_brief/cli/secrets_commands.py +232 -0
  74. multi_agent_brief/cli/semantic_support_commands.py +80 -0
  75. multi_agent_brief/cli/sources_commands.py +647 -0
  76. multi_agent_brief/cli/start_commands.py +8 -0
  77. multi_agent_brief/cli/state_commands.py +446 -0
  78. multi_agent_brief/cli/status_commands.py +26 -0
  79. multi_agent_brief/cli/workbuddy_commands.py +103 -0
  80. multi_agent_brief/configs/artifact_contracts.yaml +514 -0
  81. multi_agent_brief/configs/orchestrator_contract.yaml +181 -0
  82. multi_agent_brief/configs/policy_packs/default.yaml +65 -0
  83. multi_agent_brief/configs/policy_profiles/evidence_extract_default.yaml +58 -0
  84. multi_agent_brief/configs/policy_profiles/finance_default.yaml +44 -0
  85. multi_agent_brief/configs/policy_profiles/internet_default.yaml +44 -0
  86. multi_agent_brief/configs/policy_profiles/manufacturing_default.yaml +33 -0
  87. multi_agent_brief/configs/policy_profiles/solar_manufacturing_default.yaml +64 -0
  88. multi_agent_brief/configs/report_packs/evidence_extract.yaml +48 -0
  89. multi_agent_brief/configs/report_packs/management_monthly.yaml +36 -0
  90. multi_agent_brief/configs/report_packs/market_weekly.yaml +36 -0
  91. multi_agent_brief/configs/report_packs/solar_industry_periodic.yaml +50 -0
  92. multi_agent_brief/configs/report_templates/evidence_extract.yaml +55 -0
  93. multi_agent_brief/configs/report_templates/management_monthly.yaml +48 -0
  94. multi_agent_brief/configs/report_templates/market_weekly.yaml +49 -0
  95. multi_agent_brief/configs/report_templates/solar_industry_periodic.yaml +75 -0
  96. multi_agent_brief/configs/stage_specs.yaml +180 -0
  97. multi_agent_brief/contracts/__init__.py +41 -0
  98. multi_agent_brief/contracts/base.py +99 -0
  99. multi_agent_brief/contracts/errors.py +41 -0
  100. multi_agent_brief/contracts/migrations/__init__.py +5 -0
  101. multi_agent_brief/contracts/migrations/claim_v1_to_v2.py +33 -0
  102. multi_agent_brief/contracts/registry.py +159 -0
  103. multi_agent_brief/contracts/role_topology.py +25 -0
  104. multi_agent_brief/contracts/schemas/__init__.py +35 -0
  105. multi_agent_brief/contracts/schemas/analysis_pack.py +135 -0
  106. multi_agent_brief/contracts/schemas/atomic_claim_graph.py +304 -0
  107. multi_agent_brief/contracts/schemas/audit_report.py +80 -0
  108. multi_agent_brief/contracts/schemas/candidate_item.py +51 -0
  109. multi_agent_brief/contracts/schemas/claim.py +197 -0
  110. multi_agent_brief/contracts/schemas/claim_draft.py +386 -0
  111. multi_agent_brief/contracts/schemas/claim_support_matrix.py +334 -0
  112. multi_agent_brief/contracts/schemas/evidence_span_registry.py +270 -0
  113. multi_agent_brief/contracts/schemas/policy_profile.py +247 -0
  114. multi_agent_brief/contracts/schemas/report_spec.py +257 -0
  115. multi_agent_brief/contracts/schemas/semantic_assessment_report.py +480 -0
  116. multi_agent_brief/contracts/schemas/source_evidence_pack_manifest.py +233 -0
  117. multi_agent_brief/contracts/schemas/source_item.py +96 -0
  118. multi_agent_brief/contracts/semantic_assessment_status.py +12 -0
  119. multi_agent_brief/contracts/source_metadata.py +252 -0
  120. multi_agent_brief/contracts/target_contract.py +460 -0
  121. multi_agent_brief/contracts/validator.py +225 -0
  122. multi_agent_brief/controls/__init__.py +23 -0
  123. multi_agent_brief/controls/contract.py +106 -0
  124. multi_agent_brief/controls/switchboard.py +1076 -0
  125. multi_agent_brief/core/__init__.py +2 -0
  126. multi_agent_brief/core/citations.py +14 -0
  127. multi_agent_brief/core/claim_ledger.py +94 -0
  128. multi_agent_brief/core/config.py +191 -0
  129. multi_agent_brief/core/env.py +69 -0
  130. multi_agent_brief/core/previous.py +83 -0
  131. multi_agent_brief/core/schemas.py +181 -0
  132. multi_agent_brief/core/selection.py +189 -0
  133. multi_agent_brief/delivery/__init__.py +6 -0
  134. multi_agent_brief/delivery/base.py +36 -0
  135. multi_agent_brief/delivery/feishu.py +204 -0
  136. multi_agent_brief/delivery/gws.py +237 -0
  137. multi_agent_brief/evaluation_cases/__init__.py +15 -0
  138. multi_agent_brief/evaluation_cases/contract.py +429 -0
  139. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/config.yaml +18 -0
  140. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/improvement/ledger.jsonl +2 -0
  141. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/sources.yaml +2 -0
  142. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/user.md +4 -0
  143. multi_agent_brief/evaluation_cases/fixtures/cases/company_event_missing_latest_official_check/workspace/config.yaml +13 -0
  144. multi_agent_brief/evaluation_cases/fixtures/cases/company_event_missing_latest_official_check/workspace/sources.yaml +9 -0
  145. multi_agent_brief/evaluation_cases/fixtures/cases/company_event_missing_latest_official_check/workspace/user.md +4 -0
  146. multi_agent_brief/evaluation_cases/fixtures/cases/control_switchboard_selection_is_not_execution/workspace/config.yaml +12 -0
  147. multi_agent_brief/evaluation_cases/fixtures/cases/control_switchboard_selection_is_not_execution/workspace/sources.yaml +6 -0
  148. multi_agent_brief/evaluation_cases/fixtures/cases/control_switchboard_selection_is_not_execution/workspace/user.md +4 -0
  149. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/config.yaml +6 -0
  150. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/input/human_feedback.md +1 -0
  151. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/sources.yaml +2 -0
  152. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/user.md +3 -0
  153. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/config.yaml +8 -0
  154. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/brief.md +14 -0
  155. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/audit_report.json +1 -0
  156. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/audited_brief.md +5 -0
  157. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/candidate_claims.json +1 -0
  158. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/claim_ledger.json +1 -0
  159. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/screened_candidates.json +1 -0
  160. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/sources.yaml +2 -0
  161. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/user.md +3 -0
  162. multi_agent_brief/evaluation_cases/fixtures/cases/formal_release_missing_human_approval/workspace/config.yaml +13 -0
  163. multi_agent_brief/evaluation_cases/fixtures/cases/formal_release_missing_human_approval/workspace/sources.yaml +9 -0
  164. multi_agent_brief/evaluation_cases/fixtures/cases/formal_release_missing_human_approval/workspace/user.md +4 -0
  165. multi_agent_brief/evaluation_cases/fixtures/cases/guidance_manifestation_not_observable/workspace/config.yaml +4 -0
  166. multi_agent_brief/evaluation_cases/fixtures/cases/guidance_manifestation_not_observable/workspace/sources.yaml +2 -0
  167. multi_agent_brief/evaluation_cases/fixtures/cases/guidance_manifestation_not_observable/workspace/user.md +3 -0
  168. multi_agent_brief/evaluation_cases/fixtures/cases/media_only_legal_policy_blocks_research_review/workspace/config.yaml +13 -0
  169. multi_agent_brief/evaluation_cases/fixtures/cases/media_only_legal_policy_blocks_research_review/workspace/sources.yaml +9 -0
  170. multi_agent_brief/evaluation_cases/fixtures/cases/media_only_legal_policy_blocks_research_review/workspace/user.md +4 -0
  171. multi_agent_brief/evaluation_cases/fixtures/cases/mixed_metric_scope_support_blocker/workspace/config.yaml +13 -0
  172. multi_agent_brief/evaluation_cases/fixtures/cases/mixed_metric_scope_support_blocker/workspace/sources.yaml +9 -0
  173. multi_agent_brief/evaluation_cases/fixtures/cases/mixed_metric_scope_support_blocker/workspace/user.md +5 -0
  174. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/config.yaml +6 -0
  175. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/audited_brief.md +3 -0
  176. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/candidate_claims.json +1 -0
  177. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/claim_ledger.json +1 -0
  178. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/feedback_issues.json +25 -0
  179. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/repair_plan.json +27 -0
  180. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/screened_candidates.json +1 -0
  181. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/sources.yaml +2 -0
  182. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/user.md +3 -0
  183. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/config.yaml +8 -0
  184. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/audit_report.json +1 -0
  185. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/audited_brief.md +3 -0
  186. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/candidate_claims.json +1 -0
  187. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/claim_ledger.json +11 -0
  188. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/quality_gate_report.json +22 -0
  189. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/screened_candidates.json +1 -0
  190. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/sources.yaml +2 -0
  191. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/user.md +3 -0
  192. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/config.yaml +13 -0
  193. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/audit_report.json +1 -0
  194. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/audited_brief.md +5 -0
  195. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/candidate_claims.json +1 -0
  196. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/claim_ledger.json +40 -0
  197. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/screened_candidates.json +1 -0
  198. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/sources.yaml +3 -0
  199. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/user.md +3 -0
  200. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/config.yaml +8 -0
  201. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/brief.md +3 -0
  202. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/audit_report.json +1 -0
  203. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/audited_brief.md +3 -0
  204. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/candidate_claims.json +1 -0
  205. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/claim_ledger.json +13 -0
  206. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/screened_candidates.json +1 -0
  207. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/sources.yaml +2 -0
  208. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/user.md +3 -0
  209. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/config.yaml +13 -0
  210. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/output/intermediate/release_readiness_report.json +31 -0
  211. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/sources.yaml +9 -0
  212. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/user.md +4 -0
  213. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/config.yaml +18 -0
  214. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/improvement/ledger.jsonl +3 -0
  215. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/improvement/memory.md +3 -0
  216. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/output/intermediate/improvement_memory_snapshot.md +3 -0
  217. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/sources.yaml +2 -0
  218. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/user.md +4 -0
  219. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/config.yaml +12 -0
  220. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/input/sources/source-001.txt +1 -0
  221. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/brief.md +37 -0
  222. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/delivery/brief.md +37 -0
  223. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/atomic_claim_graph.json +17 -0
  224. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/audit_report.json +5 -0
  225. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/audited_brief.md +37 -0
  226. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/claim_ledger.json +20 -0
  227. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/evidence_span_registry.json +20 -0
  228. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/finalize_report.json +40 -0
  229. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/gates/auditor_quality_gate_report.json +42 -0
  230. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/gates/finalize_quality_gate_report.json +42 -0
  231. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/screened_candidates.json +33 -0
  232. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/source_appendix.md +12 -0
  233. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/source_appendix_trace.md +10 -0
  234. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/report_spec.yaml +24 -0
  235. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/sources.yaml +4 -0
  236. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/user.md +3 -0
  237. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/config.yaml +13 -0
  238. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/input/sources/README.md +4 -0
  239. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/output/intermediate/source_evidence_pack_manifest.json +26 -0
  240. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/sources.yaml +9 -0
  241. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/user.md +4 -0
  242. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/config.yaml +9 -0
  243. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/audit_report.json +1 -0
  244. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/audited_brief.md +3 -0
  245. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/candidate_claims.json +1 -0
  246. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/claim_ledger.json +13 -0
  247. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/screened_candidates.json +1 -0
  248. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/sources.yaml +2 -0
  249. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/user.md +3 -0
  250. multi_agent_brief/evaluation_cases/fixtures/cases/third_party_price_snapshot_formal_block/workspace/config.yaml +13 -0
  251. multi_agent_brief/evaluation_cases/fixtures/cases/third_party_price_snapshot_formal_block/workspace/sources.yaml +9 -0
  252. multi_agent_brief/evaluation_cases/fixtures/cases/third_party_price_snapshot_formal_block/workspace/user.md +4 -0
  253. multi_agent_brief/evaluation_cases/fixtures/cases/trajectory_retry_budget_exhausted/workspace/config.yaml +4 -0
  254. multi_agent_brief/evaluation_cases/fixtures/cases/trajectory_retry_budget_exhausted/workspace/sources.yaml +2 -0
  255. multi_agent_brief/evaluation_cases/fixtures/cases/trajectory_retry_budget_exhausted/workspace/user.md +3 -0
  256. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/config.yaml +18 -0
  257. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/improvement/ledger.jsonl +1 -0
  258. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/sources.yaml +2 -0
  259. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/user.md +4 -0
  260. multi_agent_brief/evaluation_cases/fixtures/cases/unauthorized_institution_branding_blocks_release/workspace/config.yaml +18 -0
  261. multi_agent_brief/evaluation_cases/fixtures/cases/unauthorized_institution_branding_blocks_release/workspace/sources.yaml +9 -0
  262. multi_agent_brief/evaluation_cases/fixtures/cases/unauthorized_institution_branding_blocks_release/workspace/user.md +5 -0
  263. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/config.yaml +8 -0
  264. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/audit_report.json +1 -0
  265. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/audited_brief.md +8 -0
  266. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/candidate_claims.json +1 -0
  267. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/claim_ledger.json +1 -0
  268. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/screened_candidates.json +1 -0
  269. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/sources.yaml +2 -0
  270. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/user.md +3 -0
  271. multi_agent_brief/evaluation_cases/fixtures/manifest.yaml +861 -0
  272. multi_agent_brief/evaluation_cases/fixtures.py +20 -0
  273. multi_agent_brief/evaluation_cases/runner.py +1603 -0
  274. multi_agent_brief/experiments/__init__.py +45 -0
  275. multi_agent_brief/experiments/experiment_080.py +5835 -0
  276. multi_agent_brief/experiments/schemas.py +63 -0
  277. multi_agent_brief/experiments/target_contract.py +10 -0
  278. multi_agent_brief/feedback/__init__.py +17 -0
  279. multi_agent_brief/feedback/feedback_contract.py +482 -0
  280. multi_agent_brief/feedback/feedback_state.py +898 -0
  281. multi_agent_brief/hermes/__init__.py +25 -0
  282. multi_agent_brief/hermes/adapter.py +832 -0
  283. multi_agent_brief/improvement/__init__.py +83 -0
  284. multi_agent_brief/improvement/contract.py +1114 -0
  285. multi_agent_brief/improvement/memory.py +451 -0
  286. multi_agent_brief/improvement/product_definition.py +228 -0
  287. multi_agent_brief/improvement/state.py +961 -0
  288. multi_agent_brief/inputs/__init__.py +2 -0
  289. multi_agent_brief/inputs/classifier.py +322 -0
  290. multi_agent_brief/inputs/contracts.py +59 -0
  291. multi_agent_brief/inputs/extractor.py +310 -0
  292. multi_agent_brief/install/__init__.py +2 -0
  293. multi_agent_brief/install/writer.py +97 -0
  294. multi_agent_brief/onboarding/__init__.py +11 -0
  295. multi_agent_brief/onboarding/io.py +103 -0
  296. multi_agent_brief/onboarding/mapper.py +484 -0
  297. multi_agent_brief/onboarding/schema.py +46 -0
  298. multi_agent_brief/orchestrator/__init__.py +2 -0
  299. multi_agent_brief/orchestrator/fact_layer_import.py +352 -0
  300. multi_agent_brief/orchestrator/handoff.py +2209 -0
  301. multi_agent_brief/orchestrator/role_topology.py +42 -0
  302. multi_agent_brief/orchestrator/run_archive.py +888 -0
  303. multi_agent_brief/orchestrator/run_integrity.py +346 -0
  304. multi_agent_brief/orchestrator/runtime_state/__init__.py +70 -0
  305. multi_agent_brief/orchestrator/runtime_state/_io.py +167 -0
  306. multi_agent_brief/orchestrator/runtime_state/_transactions.py +298 -0
  307. multi_agent_brief/orchestrator/runtime_state/artifact_registry.py +1825 -0
  308. multi_agent_brief/orchestrator/runtime_state/atomic_claim_graph.py +96 -0
  309. multi_agent_brief/orchestrator/runtime_state/claim_ledger_freeze.py +510 -0
  310. multi_agent_brief/orchestrator/runtime_state/claim_metadata_enrichment.py +836 -0
  311. multi_agent_brief/orchestrator/runtime_state/claim_support_matrix.py +582 -0
  312. multi_agent_brief/orchestrator/runtime_state/completion_gates.py +672 -0
  313. multi_agent_brief/orchestrator/runtime_state/contracts_loader.py +66 -0
  314. multi_agent_brief/orchestrator/runtime_state/decisions.py +239 -0
  315. multi_agent_brief/orchestrator/runtime_state/errors.py +61 -0
  316. multi_agent_brief/orchestrator/runtime_state/event_log.py +256 -0
  317. multi_agent_brief/orchestrator/runtime_state/evidence_span_registry.py +233 -0
  318. multi_agent_brief/orchestrator/runtime_state/fact_layer.py +755 -0
  319. multi_agent_brief/orchestrator/runtime_state/identity.py +90 -0
  320. multi_agent_brief/orchestrator/runtime_state/lifecycle.py +675 -0
  321. multi_agent_brief/orchestrator/runtime_state/manifest.py +82 -0
  322. multi_agent_brief/orchestrator/runtime_state/operations.py +224 -0
  323. multi_agent_brief/orchestrator/runtime_state/paths.py +38 -0
  324. multi_agent_brief/orchestrator/runtime_state/repair.py +984 -0
  325. multi_agent_brief/orchestrator/runtime_state/safety_surfaces.py +147 -0
  326. multi_agent_brief/orchestrator/runtime_state/semantic_assessment_report.py +692 -0
  327. multi_agent_brief/orchestrator/runtime_state/semantic_support_acceptance.py +683 -0
  328. multi_agent_brief/orchestrator/runtime_state/source_evidence_pack.py +131 -0
  329. multi_agent_brief/orchestrator/runtime_state/stage_completion.py +1320 -0
  330. multi_agent_brief/orchestrator/runtime_state/trajectory.py +200 -0
  331. multi_agent_brief/orchestrator/runtime_state/workflow.py +350 -0
  332. multi_agent_brief/orchestrator/source_evidence.py +43 -0
  333. multi_agent_brief/orchestrator/timing.py +410 -0
  334. multi_agent_brief/orchestrator_contract.py +96 -0
  335. multi_agent_brief/outputs/__init__.py +2 -0
  336. multi_agent_brief/outputs/atomic_reader_projection.py +288 -0
  337. multi_agent_brief/outputs/docx.py +32 -0
  338. multi_agent_brief/outputs/finalize.py +1036 -0
  339. multi_agent_brief/outputs/ib_docx.py +983 -0
  340. multi_agent_brief/outputs/naming.py +38 -0
  341. multi_agent_brief/outputs/pdf.py +20 -0
  342. multi_agent_brief/outputs/reader_final_gate.py +509 -0
  343. multi_agent_brief/outputs/reader_projection.py +397 -0
  344. multi_agent_brief/outputs/source_appendix.py +753 -0
  345. multi_agent_brief/outputs/templates/__init__.py +112 -0
  346. multi_agent_brief/product/__init__.py +49 -0
  347. multi_agent_brief/product/bundle_projection.py +686 -0
  348. multi_agent_brief/product/citation_profile.py +145 -0
  349. multi_agent_brief/product/guidance_manifestation.py +421 -0
  350. multi_agent_brief/product/materiality_selection.py +472 -0
  351. multi_agent_brief/product/policy_gate_adapter.py +116 -0
  352. multi_agent_brief/product/policy_profile.py +39 -0
  353. multi_agent_brief/product/policy_projection.py +161 -0
  354. multi_agent_brief/product/policy_registry.py +83 -0
  355. multi_agent_brief/product/policy_resolver.py +225 -0
  356. multi_agent_brief/product/quality_closeout.py +188 -0
  357. multi_agent_brief/product/quality_panel.py +2217 -0
  358. multi_agent_brief/product/release_approval.py +1034 -0
  359. multi_agent_brief/product/report_pack.py +130 -0
  360. multi_agent_brief/product/report_pack_aliases.py +68 -0
  361. multi_agent_brief/product/report_registry.py +101 -0
  362. multi_agent_brief/product/report_spec.py +181 -0
  363. multi_agent_brief/product/support_wording.py +525 -0
  364. multi_agent_brief/product/template_conformance.py +516 -0
  365. multi_agent_brief/product/template_projection.py +123 -0
  366. multi_agent_brief/product/template_registry.py +244 -0
  367. multi_agent_brief/product/template_render_plan.py +315 -0
  368. multi_agent_brief/product/template_renderer.py +224 -0
  369. multi_agent_brief/product/trajectory_regulation.py +441 -0
  370. multi_agent_brief/provenance/__init__.py +5 -0
  371. multi_agent_brief/provenance/builder.py +841 -0
  372. multi_agent_brief/provenance/contract.py +21 -0
  373. multi_agent_brief/provenance/io.py +144 -0
  374. multi_agent_brief/provenance/model.py +110 -0
  375. multi_agent_brief/provenance/references.py +43 -0
  376. multi_agent_brief/provenance/validator.py +135 -0
  377. multi_agent_brief/quality_gates/__init__.py +19 -0
  378. multi_agent_brief/quality_gates/contract.py +581 -0
  379. multi_agent_brief/quality_gates/state.py +2498 -0
  380. multi_agent_brief/repair/__init__.py +5 -0
  381. multi_agent_brief/repair/router.py +842 -0
  382. multi_agent_brief/runtime_assets.py +431 -0
  383. multi_agent_brief/sources/__init__.py +16 -0
  384. multi_agent_brief/sources/api_filings.py +236 -0
  385. multi_agent_brief/sources/api_news.py +174 -0
  386. multi_agent_brief/sources/base.py +142 -0
  387. multi_agent_brief/sources/cached_package.py +119 -0
  388. multi_agent_brief/sources/cli_provider.py +263 -0
  389. multi_agent_brief/sources/coverage.py +336 -0
  390. multi_agent_brief/sources/decider.py +756 -0
  391. multi_agent_brief/sources/doctor.py +373 -0
  392. multi_agent_brief/sources/evidence_pack.py +364 -0
  393. multi_agent_brief/sources/feishu_provider.py +326 -0
  394. multi_agent_brief/sources/filing_resolver.py +400 -0
  395. multi_agent_brief/sources/industry_packs.py +130 -0
  396. multi_agent_brief/sources/join.py +206 -0
  397. multi_agent_brief/sources/local_signal.py +81 -0
  398. multi_agent_brief/sources/local_signal_planner.py +636 -0
  399. multi_agent_brief/sources/manual.py +259 -0
  400. multi_agent_brief/sources/mcp_provider.py +309 -0
  401. multi_agent_brief/sources/mineru_provider.py +601 -0
  402. multi_agent_brief/sources/normalizer.py +95 -0
  403. multi_agent_brief/sources/opencli_provider.py +282 -0
  404. multi_agent_brief/sources/planner.py +178 -0
  405. multi_agent_brief/sources/registry.py +367 -0
  406. multi_agent_brief/sources/rss.py +190 -0
  407. multi_agent_brief/sources/search_backends/__init__.py +31 -0
  408. multi_agent_brief/sources/search_backends/base.py +51 -0
  409. multi_agent_brief/sources/search_backends/brave.py +192 -0
  410. multi_agent_brief/sources/search_backends/capabilities.py +94 -0
  411. multi_agent_brief/sources/search_backends/exa.py +180 -0
  412. multi_agent_brief/sources/search_backends/firecrawl.py +164 -0
  413. multi_agent_brief/sources/search_backends/serper.py +241 -0
  414. multi_agent_brief/sources/search_backends/tavily.py +124 -0
  415. multi_agent_brief/sources/sourcehub.py +505 -0
  416. multi_agent_brief/sources/web_search.py +267 -0
  417. multi_agent_brief/status.py +966 -0
  418. multi_agent_brief/tools/__init__.py +3 -0
  419. multi_agent_brief/tools/draft_cleanup.py +198 -0
  420. multi_agent_brief/workbuddy/__init__.py +1 -0
  421. multi_agent_brief/workbuddy/diagnose.py +569 -0
  422. multi_agent_brief/workbuddy/skill_pack.py +309 -0
  423. multi_agent_brief/workspace/__init__.py +1 -0
  424. multi_agent_brief/workspace/init_profile.py +45 -0
@@ -0,0 +1,2209 @@
1
+ """Runtime handoff domain model and artifact writer.
2
+
3
+ This module builds handoff payloads and writes runtime handoff/control files.
4
+ CLI modules should dispatch to these helpers rather than owning the domain
5
+ logic.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ from copy import deepcopy
14
+ from dataclasses import asdict, dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from multi_agent_brief.contracts.target_contract import (
19
+ assessment_target,
20
+ assessment_target_manifest,
21
+ load_experiment_080_condition_metadata,
22
+ )
23
+ from multi_agent_brief.orchestrator_contract import (
24
+ CONTRACT_REFERENCES,
25
+ ORCHESTRATOR_LOOP,
26
+ resolve_repo_workdir,
27
+ )
28
+ from multi_agent_brief.orchestrator.runtime_state import (
29
+ RUNTIME_STATE_FILES,
30
+ RuntimeStateError,
31
+ append_event,
32
+ check_runtime_state,
33
+ complete_stage_transaction,
34
+ initialize_runtime_state,
35
+ load_artifact_contracts,
36
+ load_stage_specs,
37
+ record_decision,
38
+ record_handoff_written,
39
+ )
40
+ from multi_agent_brief.contracts.role_topology import ROLE_TOPOLOGY_VALUES
41
+ from multi_agent_brief.orchestrator.fact_layer_import import require_fast_rerun_handoff_ready
42
+ from multi_agent_brief.audience_memory import (
43
+ AUDIENCE_MEMORY_FILES,
44
+ create_audience_profile_snapshot,
45
+ profile_data_from_workspace_config,
46
+ )
47
+ from multi_agent_brief.controls.contract import CONTROL_SWITCHBOARD_FILES
48
+ from multi_agent_brief.controls.contract import ControlSwitchboardError
49
+ from multi_agent_brief.controls.switchboard import build_control_switchboard
50
+ from multi_agent_brief.feedback.feedback_contract import FEEDBACK_STATE_FILES
51
+ from multi_agent_brief.improvement.memory import freeze_improvement_memory_for_run
52
+ from multi_agent_brief.improvement.state import ImprovementLedgerError
53
+ from multi_agent_brief.quality_gates.contract import QUALITY_GATE_STATE_FILES
54
+ from multi_agent_brief.provenance.contract import PROVENANCE_STATE_FILES
55
+ from multi_agent_brief.product.policy_projection import project_workspace_policy_profile
56
+ from multi_agent_brief.product.template_conformance import project_workspace_report_template_conformance
57
+ from multi_agent_brief.product.template_projection import project_workspace_report_template
58
+ from multi_agent_brief.product.template_render_plan import project_workspace_report_template_render_plan
59
+ from multi_agent_brief.sources.sourcehub import project_sourcehub_handoff
60
+
61
+
62
+ RUNTIME_AUTO = "auto"
63
+ RUNTIME_HERMES = "hermes"
64
+ RUNTIME_CLAUDE = "claude"
65
+ RUNTIME_OPENCODE = "opencode"
66
+ RUNTIME_CODEX = "codex"
67
+ RUNTIME_CODEBUDDY = "codebuddy"
68
+ RUNTIME_OPERATOR = "operator"
69
+ RUNTIME_MANUAL = "manual"
70
+ VALID_RUNTIMES = (
71
+ RUNTIME_AUTO,
72
+ RUNTIME_HERMES,
73
+ RUNTIME_CLAUDE,
74
+ RUNTIME_OPENCODE,
75
+ RUNTIME_CODEX,
76
+ RUNTIME_CODEBUDDY,
77
+ RUNTIME_OPERATOR,
78
+ RUNTIME_MANUAL,
79
+ )
80
+ RUNTIME_RESOLVED = {RUNTIME_AUTO: RUNTIME_HERMES} # auto resolves to hermes in v0.5.5
81
+ LEGACY_RUNTIME_ALIASES = {RUNTIME_MANUAL: RUNTIME_OPERATOR}
82
+ RUNTIME_RECIPE_FULL = "full"
83
+ RUNTIME_RECIPE_FAST_RERUN = "fast-rerun"
84
+ VALID_RUNTIME_RECIPES = (RUNTIME_RECIPE_FULL, RUNTIME_RECIPE_FAST_RERUN)
85
+ EXPECTED_WORKFLOW_ARTIFACTS = [
86
+ "output/intermediate/candidate_claims.json",
87
+ "output/intermediate/screened_candidates.json",
88
+ "output/intermediate/claim_ledger.json",
89
+ "output/intermediate/audited_brief.md",
90
+ "output/intermediate/audit_report.json",
91
+ "output/delivery/brief.md",
92
+ ]
93
+ REPAIR_GUIDANCE_NOTE = (
94
+ "Repair guidance is bounded runtime guidance: repeated retry/repair budgets are "
95
+ "enforced by `workflow_state.json.next_allowed_decisions` after `state check` or "
96
+ "`state decide`; when trajectory regulation narrows decisions, use only "
97
+ "request_human_review or block_run. If a repair would touch more than two sections, "
98
+ "narrow the scope before delegating or request human review. "
99
+ "Audit warnings, overstatement findings, support-calibration findings, and quality-gate "
100
+ "findings do not authorize direct edits to frozen artifacts. "
101
+ "For owner-stage artifact repair, first run `briefloop repair route --workspace "
102
+ "<workspace>` and `briefloop repair start --workspace <workspace>`, then delegate "
103
+ "only the repair_owner role and allow edits only to allowed_artifacts. After the owner "
104
+ "edits, run `briefloop repair complete --workspace <workspace> --reason \"<reason>\"` "
105
+ "and rerun downstream stages from must_rerun_from."
106
+ )
107
+ DECISION_RECORDING_NOTE = (
108
+ "Record every successful stage completion before moving on: run "
109
+ "`briefloop state stage-complete --workspace <workspace> --stage <stage_id> "
110
+ "--reason \"<reason>\"`. Use low-level `briefloop state decide` only for "
111
+ "retry_stage, request_human_review, or block_run decisions. "
112
+ "`delegate_repair` requires the repair route/start transaction and cannot be executed "
113
+ "through `state decide`. "
114
+ "Use only decisions allowed by `workflow_state.json.next_allowed_decisions`. "
115
+ "If the transaction is rejected, stop and correct the stage state instead of continuing."
116
+ )
117
+ ORCHESTRATOR_CONTEXT_READING_NOTE = (
118
+ "Read runtime state files before selecting the next stage:\n"
119
+ "- output/intermediate/runtime_manifest.json\n"
120
+ "- output/intermediate/workflow_state.json\n"
121
+ "- output/intermediate/artifact_registry.json\n"
122
+ "- output/intermediate/event_log.jsonl\n\n"
123
+ "Read audience memory snapshot for this run:\n"
124
+ "- output/intermediate/audience_profile_snapshot.md\n"
125
+ "Summarize relevant taste guidance for delegated roles. Do not treat audience_profile.md as source evidence, and do not use mid-run profile edits until the next run.\n\n"
126
+ "Read the Orchestrator control switchboard:\n"
127
+ "- output/intermediate/orchestrator_control_switchboard.json\n"
128
+ "Record control selections with briefloop controls select. Selection is not execution; explicitly run the selected CLI/subagent/human action after selection and approval."
129
+ )
130
+ FINALIZE_GATE_NOTE = (
131
+ "Before finalize, after the auditor stage completes, run "
132
+ "`briefloop gates check --workspace <workspace> --stage auditor` and "
133
+ "`briefloop state check --workspace <workspace> --strict`; this "
134
+ "creates or refreshes `output/intermediate/gates/auditor_quality_gate_report.json` "
135
+ "and a legacy latest projection at `output/intermediate/quality_gate_report.json`. If "
136
+ "there are blocking findings, do not finalize. Use feedback records plus "
137
+ "`briefloop repair route` / `briefloop repair start`, "
138
+ "request_human_review, or block_run. Complete auditor with "
139
+ "`briefloop state stage-complete --workspace <workspace> --stage auditor "
140
+ "--reason \"Audit and quality gates passed.\"` only when audit readiness and "
141
+ "quality gates pass. After the finalize tool writes delivery artifacts under "
142
+ "`output/delivery/`, run `briefloop gates check --workspace <workspace> "
143
+ "--stage finalize --brief <workspace>/output/brief.md`, then run "
144
+ "`briefloop state finalize-complete --workspace <workspace> --reason "
145
+ "\"Reader artifacts finalized and clean.\"`. Finalize/formatter reads "
146
+ "`output/intermediate/audited_brief.md` as frozen input and must not edit it; "
147
+ "if reader-clean requires wording changes in the audited brief, stop and "
148
+ "route repair to Editor."
149
+ )
150
+ ROLE_TOPOLOGY_HANDOFF_NOTE = (
151
+ "Role topology note: read configs/policy_packs/default.yaml before delegating. "
152
+ "With role_topology=default, Scout performs discovery and screening in one role: "
153
+ "write output/intermediate/candidate_claims.json first, then "
154
+ "output/intermediate/screened_candidates.json, and record "
155
+ "`state stage-complete --stage scout`; the screener stage is satisfied by topology "
156
+ "and must not be run as a separate specialist. Do not delegate Screener and "
157
+ "do not call `state stage-complete --stage screener` in default topology. "
158
+ "With role_topology=strict, Scout writes only candidate_claims.json, then "
159
+ "delegate Screener to write screened_candidates.json. In all modes both "
160
+ "artifacts remain required evidence for Claim Ledger. Runtime may split "
161
+ "Scout work internally, but chunk outputs are scratch material only; only "
162
+ "deterministic joined artifacts count."
163
+ )
164
+ RUNTIME_WEBSEARCH_ZERO_RESULT_NOTE = (
165
+ "Runtime WebSearch zero-result guard: if runtime WebSearch reports `Did 0 searches`, "
166
+ "or every query returns an empty result set, stop and request human review. "
167
+ "Do not switch to source-planner or continue with stale sources."
168
+ )
169
+ INPUT_DIRECTORY_ROLE_NOTE = (
170
+ "Directory role still controls claim eligibility: eligible evidence files "
171
+ "under input/sources count as evidence; binary inputs require extracted "
172
+ "Markdown before use. input/context, input/instructions, and input/feedback "
173
+ "remain non-evidence."
174
+ )
175
+ STAGE_COMPLETION_PROTOCOL_SCHEMA = "multi-agent-brief-stage-completion-protocol/v1"
176
+ STAGE_COMPLETION_PROTOCOL_RULES = [
177
+ "Stage completion is artifact-based, not statement-based.",
178
+ "A natural-language acknowledgement such as 'I completed the stage' is not sufficient unless the required artifact paths are present and validated.",
179
+ "Before moving to the next stage, verify required output artifacts exist at the declared paths and have the expected shape where validators exist.",
180
+ "If a required artifact is missing, stale, or invalid, stop the stage and record retry_stage, request_human_review, or block_run instead of continuing.",
181
+ "source_candidates.yaml is a source plan only, not source evidence; Scout must extract candidates from actual source content or materialized source files.",
182
+ "Runtime WebSearch results must be materialized before source-discovery completion as durable source files with URL, source title/name, published date or retrieved_at, and raw excerpt/snippet.",
183
+ "After state stage-complete succeeds, that stage's output artifacts are frozen for downstream stages; later stages must not rewrite them in place.",
184
+ "Never edit workflow_state.json, artifact_registry.json, runtime_manifest.json, or event_log.jsonl directly; only deterministic briefloop transactions may write runtime control files.",
185
+ "If state stage-complete, repair complete, or finalize-complete fails, stop and report the exact CLI error instead of patching control files or advancing state manually.",
186
+ "If a downstream stage finds schema mismatch or invalid frozen upstream artifacts, route repair back to the owner stage instead of editing the artifact directly.",
187
+ "Formatter/finalize may only write reader delivery artifacts and finalize control records; it must not patch output/intermediate/audited_brief.md.",
188
+ "Every stage handoff to a child agent must include complete context, required input artifact paths, required output artifact paths, and forbidden actions.",
189
+ "Scout chunk outputs are scratch material only; if Scout work is split across chunks or child agents, join chunks deterministically before writing candidate_claims.json.",
190
+ "Record successful stage transitions with briefloop state stage-complete only after artifact-level completion evidence is available.",
191
+ "Record finalize completion with briefloop state finalize-complete after delivery artifacts and finalize_report.json are clean.",
192
+ ]
193
+ DEFAULT_STAGE_FORBIDDEN_ACTIONS = [
194
+ "Do not claim stage completion based on prose acknowledgement alone.",
195
+ "Do not proceed to the next stage without naming the produced artifact path.",
196
+ "Do not mutate upstream input artifacts except through the stage's declared output artifacts.",
197
+ "Do not treat source_candidates.yaml as source evidence or use it to support claims.",
198
+ "Do not rewrite a previous stage's artifact after that stage has completed; schema mismatch must route back to the owner stage for repair.",
199
+ "Do not invent source evidence, claim support, citations, or validation results.",
200
+ "Do not edit workflow_state.json, artifact_registry.json, runtime_manifest.json, or event_log.jsonl directly.",
201
+ "Do not bypass workflow_state.json next_allowed_decisions or skip state stage-complete/finalize-complete.",
202
+ ]
203
+ OPERATOR_ARTIFACT_OWNERSHIP_SCHEMA = "briefloop.operator_artifact_ownership.v1"
204
+ OPERATOR_ARTIFACT_OWNERSHIP = {
205
+ "schema_version": OPERATOR_ARTIFACT_OWNERSHIP_SCHEMA,
206
+ "boundary": (
207
+ "Artifact ownership map for compact operator handoff. It is guidance only; "
208
+ "deterministic CLI transactions and validators remain authoritative."
209
+ ),
210
+ "agent_owned_drafts": [
211
+ {
212
+ "path": "output/intermediate/candidate_claims.json",
213
+ "owner": "scout",
214
+ "write_rule": "Scout/operator-authored draft artifact; complete through `briefloop state stage-complete --stage scout`.",
215
+ },
216
+ {
217
+ "path": "output/intermediate/screened_candidates.json",
218
+ "owner": "scout_or_screener",
219
+ "write_rule": "Scout owns this in default topology; Screener owns it only in strict topology.",
220
+ },
221
+ {
222
+ "path": "output/intermediate/claim_drafts.json",
223
+ "owner": "claim-ledger",
224
+ "write_rule": "Claim Ledger/operator-authored draft input for the freeze transaction.",
225
+ },
226
+ {
227
+ "path": "output/intermediate/audited_brief.md",
228
+ "owner": "analyst_then_editor",
229
+ "write_rule": "Analyst drafts from frozen Claim Ledger; Editor polishes without adding facts.",
230
+ },
231
+ {
232
+ "path": "output/intermediate/audit_report.json",
233
+ "owner": "auditor",
234
+ "write_rule": "Auditor/operator-authored audit artifact before gates/finalize.",
235
+ },
236
+ ],
237
+ "cli_owned_outputs": [
238
+ {
239
+ "path": "output/intermediate/claim_ledger.json",
240
+ "command": "briefloop state freeze-claim-ledger --workspace <workspace>",
241
+ },
242
+ {
243
+ "path": "output/intermediate/analyst_draft_snapshot.md",
244
+ "command": "briefloop state stage-complete --workspace <workspace> --stage analyst --reason \"<reason>\"",
245
+ },
246
+ {
247
+ "path": "output/intermediate/gates/auditor_quality_gate_report.json",
248
+ "command": "briefloop gates check --workspace <workspace> --stage auditor",
249
+ },
250
+ {
251
+ "path": "output/intermediate/gates/finalize_quality_gate_report.json",
252
+ "command": "briefloop gates check --workspace <workspace> --stage finalize --brief <brief>",
253
+ },
254
+ {
255
+ "path": "output/intermediate/finalize_report.json",
256
+ "command": "briefloop finalize --config <workspace>/config.yaml",
257
+ },
258
+ {
259
+ "path": "output/delivery/brief.md",
260
+ "command": "briefloop finalize --config <workspace>/config.yaml",
261
+ },
262
+ {
263
+ "path": "output/intermediate/quality_panel.json",
264
+ "command": "briefloop quality summarize --workspace <workspace>",
265
+ },
266
+ {
267
+ "path": "output/intermediate/quality_summary.md",
268
+ "command": "briefloop quality summarize --workspace <workspace>",
269
+ },
270
+ {
271
+ "path": "output/intermediate/quality_panel.html",
272
+ "command": "briefloop quality summarize --workspace <workspace>",
273
+ },
274
+ ],
275
+ "read_only_diagnostics": [
276
+ "output/intermediate/agent_handoff.md",
277
+ "output/intermediate/agent_handoff.json",
278
+ "output/intermediate/artifact_registry.json",
279
+ "output/intermediate/runtime_manifest.json",
280
+ "output/intermediate/event_log.jsonl",
281
+ "output/intermediate/quality_gate_report.json",
282
+ "output/intermediate/run_integrity_report.json",
283
+ "experiment/080/*_scorecard.json",
284
+ ],
285
+ "forbidden_direct_edits": [
286
+ "output/intermediate/workflow_state.json",
287
+ "output/intermediate/artifact_registry.json",
288
+ "output/intermediate/runtime_manifest.json",
289
+ "output/intermediate/event_log.jsonl",
290
+ "output/intermediate/gates/*_quality_gate_report.json",
291
+ "output/intermediate/quality_gate_report.json",
292
+ "output/intermediate/run_integrity_report.json",
293
+ "experiment/080/*_scorecard.json",
294
+ ],
295
+ }
296
+
297
+
298
+ @dataclass
299
+ class AgentHandoff:
300
+ runtime: str
301
+ recipe: str
302
+ workspace: str
303
+ repo_workdir: str
304
+ venv_activate: str
305
+ doctor_status: str = "not_run"
306
+ next_steps: str = ""
307
+ prompt: str = ""
308
+ expected_artifacts: list[str] = field(default_factory=list)
309
+ runtime_state_files: dict[str, str] = field(default_factory=lambda: dict(RUNTIME_STATE_FILES))
310
+ audience_memory_files: dict[str, str] = field(default_factory=lambda: dict(AUDIENCE_MEMORY_FILES))
311
+ improvement_memory_files: dict[str, str] = field(default_factory=dict)
312
+ control_switchboard_files: dict[str, str] = field(default_factory=lambda: dict(CONTROL_SWITCHBOARD_FILES))
313
+ feedback_state_files: dict[str, str] = field(default_factory=lambda: dict(FEEDBACK_STATE_FILES))
314
+ quality_gate_state_files: dict[str, str] = field(default_factory=lambda: dict(QUALITY_GATE_STATE_FILES))
315
+ provenance_state_files: dict[str, str] = field(default_factory=lambda: dict(PROVENANCE_STATE_FILES))
316
+ contract_references: dict[str, str] = field(default_factory=lambda: dict(CONTRACT_REFERENCES))
317
+ stage_completion_protocol: dict[str, Any] = field(default_factory=dict)
318
+ assessment_target_manifest: dict[str, Any] = field(default_factory=dict)
319
+ policy_profile_projection: dict[str, Any] = field(default_factory=dict)
320
+ sourcehub_projection: dict[str, Any] = field(default_factory=dict)
321
+ report_template_projection: dict[str, Any] = field(default_factory=dict)
322
+ report_template_conformance_projection: dict[str, Any] = field(default_factory=dict)
323
+ report_template_render_plan_projection: dict[str, Any] = field(default_factory=dict)
324
+ runtime_capabilities: dict[str, Any] = field(default_factory=dict)
325
+ artifact_ownership: dict[str, Any] = field(default_factory=dict)
326
+ legacy_runtime_alias: str | None = None
327
+ notes: list[str] = field(default_factory=list)
328
+
329
+ def to_dict(self) -> dict[str, Any]:
330
+ return asdict(self)
331
+
332
+
333
+ def _load_config(workspace: Path) -> dict[str, Any] | None:
334
+ import yaml
335
+ config_path = workspace / "config.yaml"
336
+ if not config_path.exists():
337
+ return None
338
+ return yaml.safe_load(config_path.read_text(encoding="utf-8"))
339
+
340
+
341
+ def _find_venv_activate(repo: Path) -> str:
342
+ if sys.platform == "win32":
343
+ cand = repo / ".venv" / "Scripts" / "activate"
344
+ return str(cand) if cand.exists() else str(cand)
345
+ cand = repo / ".venv" / "bin" / "activate"
346
+ return str(cand) if cand.exists() else str(cand)
347
+
348
+
349
+ def normalize_runtime(runtime: str) -> tuple[str, str | None]:
350
+ """Resolve legacy runtime aliases without writing alias names to handoff."""
351
+ value = str(runtime or "").strip().lower()
352
+ alias_target = LEGACY_RUNTIME_ALIASES.get(value)
353
+ if alias_target:
354
+ return alias_target, value
355
+ return value, None
356
+
357
+
358
+ def legacy_runtime_alias_warning(runtime: str) -> str | None:
359
+ resolved, alias = normalize_runtime(runtime)
360
+ if not alias:
361
+ return None
362
+ return (
363
+ f'runtime "{alias}" is a legacy alias for "{resolved}". '
364
+ f"Use --runtime {resolved} for host-agnostic compact workflow."
365
+ )
366
+
367
+
368
+ def _run_doctor(workspace: Path) -> tuple[int, str]:
369
+ config = workspace / "config.yaml"
370
+ if not config.exists():
371
+ return 1, "no_config"
372
+ import subprocess
373
+ env = os.environ.copy()
374
+ package_root = Path(__file__).resolve().parents[2]
375
+ current_pythonpath = env.get("PYTHONPATH")
376
+ env["PYTHONPATH"] = (
377
+ f"{package_root}{os.pathsep}{current_pythonpath}"
378
+ if current_pythonpath
379
+ else str(package_root)
380
+ )
381
+ result = subprocess.run(
382
+ [sys.executable, "-m", "multi_agent_brief.cli.main", "doctor", "--config", str(config)],
383
+ capture_output=True, text=True,
384
+ cwd=str(workspace),
385
+ env=env,
386
+ )
387
+ output = result.stdout + result.stderr
388
+ if result.returncode == 0 and "FAIL" not in output:
389
+ return 0, "passed"
390
+ return result.returncode, "failed"
391
+
392
+
393
+ def _hermes_handoff(workspace: Path, repo: Path, venv: str) -> AgentHandoff:
394
+ from multi_agent_brief.hermes import render_hermes_prompt
395
+ prompt = render_hermes_prompt(
396
+ workspace=workspace,
397
+ repo_workdir=repo,
398
+ venv_path=venv,
399
+ )
400
+ return AgentHandoff(
401
+ runtime=RUNTIME_HERMES,
402
+ recipe=RUNTIME_RECIPE_FULL,
403
+ workspace=str(workspace.resolve()),
404
+ repo_workdir=str(repo.resolve()),
405
+ venv_activate=venv,
406
+ next_steps=(
407
+ "Paste the prompt below into Hermes. The Hermes parent agent is the "
408
+ "Orchestrator main agent and will run the contract-guided delegated workflow."
409
+ ),
410
+ prompt=prompt,
411
+ expected_artifacts=list(EXPECTED_WORKFLOW_ARTIFACTS),
412
+ notes=[
413
+ "Install the BriefLoop Hermes plugin: cp -R integrations/hermes-plugin/mabw ~/.hermes/plugins/mabw && hermes plugins enable mabw",
414
+ "Then in Hermes, use the BriefLoop plugin tools: /mabw <workspace> → mabw_create_onboarding → mabw_init_workspace → mabw_run_handoff → read agent_handoff.md → continue delegated workflow. The /mabw tool name is a compatibility surface.",
415
+ "Read configs/orchestrator_contract.yaml, configs/stage_specs.yaml, configs/artifact_contracts.yaml, and configs/policy_packs/default.yaml before delegation.",
416
+ "Read output/intermediate/runtime_manifest.json, workflow_state.json, artifact_registry.json, and event_log.jsonl before selecting the next stage.",
417
+ "Read output/intermediate/audience_profile_snapshot.md at run start for reader taste context; do not treat audience_profile.md as source evidence.",
418
+ "Read output/intermediate/orchestrator_control_switchboard.json; record enable/defer/reject selections before explicitly executing any selected control.",
419
+ DECISION_RECORDING_NOTE,
420
+ FINALIZE_GATE_NOTE,
421
+ REPAIR_GUIDANCE_NOTE,
422
+ INPUT_DIRECTORY_ROLE_NOTE,
423
+ f"Orchestrator loop: {ORCHESTRATOR_LOOP}",
424
+ "Each delegate_task child needs complete goal, context, input paths, and output paths.",
425
+ "Parent must verify each artifact before proceeding to the next child.",
426
+ ],
427
+ )
428
+
429
+
430
+ def _claude_handoff(workspace: Path, repo: Path, venv: str) -> AgentHandoff:
431
+ ws_path = str(workspace.resolve())
432
+ return AgentHandoff(
433
+ runtime=RUNTIME_CLAUDE,
434
+ recipe=RUNTIME_RECIPE_FULL,
435
+ workspace=ws_path,
436
+ repo_workdir=str(repo.resolve()),
437
+ venv_activate=venv,
438
+ next_steps=(
439
+ f"In Claude Code, continue the delegated stage workflow with: /generate-brief {ws_path}. "
440
+ "The command context is the Orchestrator main agent. "
441
+ "Use /briefloop run only to create or refresh handoff files."
442
+ ),
443
+ prompt=(
444
+ f"Use /generate-brief {ws_path} as the delegated stage workflow command.\n"
445
+ f"Use /briefloop run {ws_path} only to create or refresh handoff files; it does not execute specialists or complete stages.\n"
446
+ "Read contract references before delegation:\n"
447
+ "- configs/orchestrator_contract.yaml\n"
448
+ "- configs/stage_specs.yaml\n"
449
+ "- configs/artifact_contracts.yaml\n"
450
+ "- configs/policy_packs/default.yaml\n\n"
451
+ f"Orchestrator loop: {ORCHESTRATOR_LOOP}\n\n"
452
+ "Delegated stage order:\n"
453
+ "default: scout(discovery+screening) → claim-ledger → analyst → editor/Delivery Editor → auditor → finalize.\n"
454
+ "strict: scout → screener → claim-ledger → analyst → editor/Delivery Editor → auditor → finalize.\n\n"
455
+ f"{ROLE_TOPOLOGY_HANDOFF_NOTE}\n\n"
456
+ f"{ORCHESTRATOR_CONTEXT_READING_NOTE}\n\n"
457
+ f"{DECISION_RECORDING_NOTE}\n\n"
458
+ f"{FINALIZE_GATE_NOTE}\n\n"
459
+ f"{REPAIR_GUIDANCE_NOTE}\n\n"
460
+ f"Repository: {repo.resolve()}\n"
461
+ f"Workspace: {ws_path}\n"
462
+ f"Activate venv: source {venv}"
463
+ ),
464
+ expected_artifacts=list(EXPECTED_WORKFLOW_ARTIFACTS),
465
+ notes=[
466
+ "Claude Code must be opened from the repository root.",
467
+ "The /generate-brief command handles the Orchestrator-led delegated stage workflow.",
468
+ "The /briefloop run command creates or refreshes handoff files only.",
469
+ ],
470
+ )
471
+
472
+
473
+ def _opencode_handoff(workspace: Path, repo: Path, venv: str) -> AgentHandoff:
474
+ ws_path = str(workspace.resolve())
475
+ return AgentHandoff(
476
+ runtime=RUNTIME_OPENCODE,
477
+ recipe=RUNTIME_RECIPE_FULL,
478
+ workspace=ws_path,
479
+ repo_workdir=str(repo.resolve()),
480
+ venv_activate=venv,
481
+ next_steps=f"In OpenCode, use the BriefLoop command for {ws_path}. brief-orchestrator is the primary Orchestrator main agent.",
482
+ prompt=(
483
+ f"Workspace: {ws_path}\n"
484
+ f"Repository: {repo.resolve()}\n"
485
+ f"Activate venv: source {venv}\n\n"
486
+ "Run the OpenCode BriefLoop command through brief-orchestrator.\n"
487
+ "Read contract references before delegation:\n"
488
+ "- configs/orchestrator_contract.yaml\n"
489
+ "- configs/stage_specs.yaml\n"
490
+ "- configs/artifact_contracts.yaml\n"
491
+ "- configs/policy_packs/default.yaml\n\n"
492
+ f"Orchestrator loop: {ORCHESTRATOR_LOOP}\n\n"
493
+ "Delegated stage order:\n"
494
+ "default: scout(discovery+screening) → claim-ledger → analyst → editor/Delivery Editor → auditor → finalize.\n"
495
+ "strict: scout → screener → claim-ledger → analyst → editor/Delivery Editor → auditor → finalize.\n\n"
496
+ f"{ROLE_TOPOLOGY_HANDOFF_NOTE}\n\n"
497
+ f"{ORCHESTRATOR_CONTEXT_READING_NOTE}\n\n"
498
+ f"{DECISION_RECORDING_NOTE}\n\n"
499
+ f"{FINALIZE_GATE_NOTE}\n\n"
500
+ f"{REPAIR_GUIDANCE_NOTE}"
501
+ ),
502
+ expected_artifacts=list(EXPECTED_WORKFLOW_ARTIFACTS),
503
+ notes=[
504
+ "OpenCode agent configs are in .opencode/.",
505
+ ],
506
+ )
507
+
508
+
509
+ def _codex_handoff(workspace: Path, repo: Path, venv: str) -> AgentHandoff:
510
+ ws_path = str(workspace.resolve())
511
+ role_mapping = (
512
+ "Codex custom agent mapping:\n"
513
+ "- scout -> .codex/agents/scout.toml (default: discovery + screening; strict: discovery only)\n"
514
+ "- screener -> .codex/agents/screener.toml (strict topology or explicit repair/review only)\n"
515
+ "- claim-ledger -> .codex/agents/claim-ledger.toml\n"
516
+ "- analyst -> .codex/agents/analyst.toml\n"
517
+ "- editor -> .codex/agents/editor.toml\n"
518
+ "- auditor -> .codex/agents/auditor.toml\n"
519
+ "- formatter/finalize -> Python finalize tool, not a drafting agent\n"
520
+ )
521
+ writer_flow_protocol = (
522
+ "Codex writer flow protocol:\n"
523
+ "- When the user asks to inspect a folder, produce a Workspace Card before taking action: "
524
+ "workspace path, BriefLoop config found/missing, Codex runtime kit installed/not installed, "
525
+ "trust status, input source count, demo-looking sources yes/no, existing output/control state, "
526
+ "current workflow_state, and recommended next action.\n"
527
+ "- Trust status is one Workspace Card line, not the main answer.\n"
528
+ "- Do not launch the interactive terminal onboarding wizard inside Codex chat.\n"
529
+ "- For workspace creation, collect onboarding fields in one batch, write onboarding.json, "
530
+ "show the values to be written, then run briefloop init --from-onboarding.\n"
531
+ "- Before initializing into an existing directory, check output/intermediate/runtime_manifest.json, "
532
+ "workflow_state.json, artifact_registry.json, event_log.jsonl, and output/runs/. If present, ask whether "
533
+ "to create a new workspace, overwrite config only while keeping old output, or reset old output/control state "
534
+ "before running.\n"
535
+ "- After init or config inspection, show a Source Mode Card: manual local files enabled/disabled, "
536
+ "runtime WebSearch enabled/disabled, external API search enabled/disabled, existing source files count, "
537
+ "and demo-looking source files yes/no.\n"
538
+ "- If using Codex/runtime WebSearch, write collected public sources into input/sources/ as durable source files "
539
+ "with URL, source title/name, published date or retrieved_at, and raw excerpt/snippet. Summary-only notes "
540
+ "are discovery hints, not evidence.\n"
541
+ "- Do not call sources decide --search unless web_search.mode is external_api.\n"
542
+ "- Do not call sources decide --merge on source_plan_only artifacts.\n"
543
+ "- source_candidates.yaml is planning/review only, not evidence.\n"
544
+ "- If runtime_tool search and old demo-looking source files both exist, ask whether to keep or remove "
545
+ "the old source files before running.\n"
546
+ "- During production runs, report progress after every successful stage-complete transaction in this form: "
547
+ "[stage] produced <artifact> -> stage-complete passed -> next <stage>.\n"
548
+ "- Final status must list the delivery bundle and control status: gates, finalize_report, finalize-complete, "
549
+ "and archive.\n"
550
+ )
551
+ return AgentHandoff(
552
+ runtime=RUNTIME_CODEX,
553
+ recipe=RUNTIME_RECIPE_FULL,
554
+ workspace=ws_path,
555
+ repo_workdir=str(repo.resolve()),
556
+ venv_activate=venv,
557
+ next_steps=(
558
+ f"In Codex, use the root session as the Orchestrator main agent for {ws_path}. "
559
+ "Spawn named Codex custom agents directly for specialist stages."
560
+ ),
561
+ prompt=(
562
+ f"Workspace: {ws_path}\n"
563
+ f"Repository: {repo.resolve()}\n"
564
+ f"Activate venv: source {venv}\n\n"
565
+ "You are the Orchestrator main agent in the root Codex session.\n"
566
+ "Do not invoke an orchestrator subagent that then invokes other subagents; "
567
+ "Codex child-agent depth should stay at one.\n"
568
+ "Do not use the Claude/OpenCode slash-command workflow; that is not the Codex runtime path.\n"
569
+ "Codex custom agents are in .codex/agents/. Spawn the named Codex custom agent for each specialist stage.\n"
570
+ "Codex loads project .codex/config.toml and custom agents only after the workspace is trusted in Codex.\n"
571
+ "If Codex cannot see these custom agents, stop and ask the user to install Codex runtime assets.\n\n"
572
+ f"{writer_flow_protocol}\n"
573
+ "Read contract references before delegation:\n"
574
+ "- configs/orchestrator_contract.yaml\n"
575
+ "- configs/stage_specs.yaml\n"
576
+ "- configs/artifact_contracts.yaml\n"
577
+ "- configs/policy_packs/default.yaml\n\n"
578
+ f"Orchestrator loop: {ORCHESTRATOR_LOOP}\n\n"
579
+ f"{ROLE_TOPOLOGY_HANDOFF_NOTE}\n\n"
580
+ f"{role_mapping}\n"
581
+ "Do not call the next specialist until `briefloop state stage-complete` succeeds for the current stage.\n"
582
+ "Finalize is a Python delivery/rendering tool. After finalize writes delivery artifacts, record completion with `briefloop state finalize-complete`.\n\n"
583
+ f"{ORCHESTRATOR_CONTEXT_READING_NOTE}\n\n"
584
+ f"{DECISION_RECORDING_NOTE}\n\n"
585
+ f"{FINALIZE_GATE_NOTE}\n\n"
586
+ f"{REPAIR_GUIDANCE_NOTE}"
587
+ ),
588
+ expected_artifacts=list(EXPECTED_WORKFLOW_ARTIFACTS),
589
+ notes=[
590
+ "Codex agent configs are in .codex/agents/.",
591
+ "Codex must trust the workspace before project .codex/config.toml and custom agents load.",
592
+ "The root Codex session is the Orchestrator main agent; spawn specialist custom agents directly.",
593
+ "If Codex cannot see custom agents, run `briefloop runtime install --workspace <workspace> --runtime codex --repo-workdir <repo>` from a source clone.",
594
+ ],
595
+ )
596
+
597
+
598
+ def _operator_runtime_capabilities(legacy_alias: str | None = None) -> dict[str, Any]:
599
+ return {
600
+ "runtime": RUNTIME_OPERATOR,
601
+ "legacy_runtime_alias": legacy_alias,
602
+ "delegation_assumed": False,
603
+ "compact_workflow_allowed": True,
604
+ "host_specific_adapter": False,
605
+ "must_not_claim_subagents_ran": True,
606
+ "fallback_if_no_delegation": "compact_operator_workflow_or_stop",
607
+ }
608
+
609
+
610
+ CODEBUDDY_ROLE_AGENTS = [
611
+ "briefloop-scout",
612
+ "briefloop-screener",
613
+ "briefloop-claim-ledger",
614
+ "briefloop-analyst",
615
+ "briefloop-editor",
616
+ "briefloop-auditor",
617
+ "briefloop-formatter",
618
+ ]
619
+ CODEBUDDY_SKILL_PATH = Path(".codebuddy") / "skills" / "briefloop" / "SKILL.md"
620
+ CODEBUDDY_AGENT_ROOT = Path(".codebuddy") / "agents"
621
+
622
+
623
+ def _codebuddy_runtime_capabilities() -> dict[str, Any]:
624
+ return {
625
+ "runtime": RUNTIME_CODEBUDDY,
626
+ "host": "codebuddy",
627
+ "delegation_supported": True,
628
+ "subagent_names": list(CODEBUDDY_ROLE_AGENTS),
629
+ "nested_subagents_supported": False,
630
+ "skill_path": ".codebuddy/skills/briefloop/SKILL.md",
631
+ "role_agent_path_glob": ".codebuddy/agents/briefloop-*.md",
632
+ "main_session_runs_cli_transactions": True,
633
+ "role_agents_run_cli_transactions": False,
634
+ "must_not_claim_uninvoked_subagents_ran": True,
635
+ }
636
+
637
+
638
+ def _require_codebuddy_source_assets(repo: Path) -> None:
639
+ missing: list[str] = []
640
+ skill_path = repo / CODEBUDDY_SKILL_PATH
641
+ if not skill_path.is_file():
642
+ missing.append(CODEBUDDY_SKILL_PATH.as_posix())
643
+ for role in CODEBUDDY_ROLE_AGENTS:
644
+ rel_path = CODEBUDDY_AGENT_ROOT / f"{role}.md"
645
+ if not (repo / rel_path).is_file():
646
+ missing.append(rel_path.as_posix())
647
+ if missing:
648
+ missing_list = ", ".join(missing)
649
+ raise ValueError(
650
+ "CodeBuddy runtime is source-clone-only and requires local CodeBuddy "
651
+ f"assets under repo_workdir. Missing: {missing_list}. "
652
+ "Use --runtime operator or pass --repo-workdir pointing to a BriefLoop "
653
+ "source checkout with .codebuddy assets."
654
+ )
655
+
656
+
657
+ def _codebuddy_handoff(workspace: Path, repo: Path, venv: str) -> AgentHandoff:
658
+ _require_codebuddy_source_assets(repo)
659
+ ws_path = str(workspace.resolve())
660
+ role_agents = "\n".join(f"- {name}" for name in CODEBUDDY_ROLE_AGENTS)
661
+ role_sequence = "\n".join(
662
+ [
663
+ "1. Ask `briefloop-scout` to draft Scout-owned artifacts from the current handoff.",
664
+ "2. Run deterministic BriefLoop validation and stage transactions in the main CodeBuddy session.",
665
+ "3. Default topology: Scout already writes `output/intermediate/screened_candidates.json`; do not invoke `briefloop-screener` or complete screener separately.",
666
+ "4. Strict topology only: ask `briefloop-screener` to draft `output/intermediate/screened_candidates.json`, then run deterministic BriefLoop validation and screener stage transactions in the main CodeBuddy session.",
667
+ "5. Ask `briefloop-claim-ledger` to draft `output/intermediate/claim_drafts.json` from screened candidates.",
668
+ f"6. Run `briefloop state freeze-claim-ledger --workspace {ws_path}` and `briefloop state stage-complete --workspace {ws_path} --stage claim-ledger --reason \"Claim Ledger was frozen from claim drafts.\"` in the main CodeBuddy session.",
669
+ "7. Ask `briefloop-analyst` to draft Analyst-owned artifacts from frozen `output/intermediate/claim_ledger.json`.",
670
+ "8. Run deterministic BriefLoop validation and stage transactions in the main CodeBuddy session.",
671
+ "9. Ask `briefloop-editor` to polish the audited brief without adding facts.",
672
+ "10. Run deterministic BriefLoop validation and stage transactions in the main CodeBuddy session.",
673
+ "11. Ask `briefloop-auditor` to draft the audit report.",
674
+ "12. Run deterministic auditor gate and stage-complete commands in the main CodeBuddy session.",
675
+ "13. Ask `briefloop-formatter` for finalize readiness only; it must not write files or run CLI commands.",
676
+ "14. Run finalize, finalize gate, finalize-complete, quality, and delivery commands from the main CodeBuddy session only when allowed.",
677
+ ]
678
+ )
679
+ return AgentHandoff(
680
+ runtime=RUNTIME_CODEBUDDY,
681
+ recipe=RUNTIME_RECIPE_FULL,
682
+ workspace=ws_path,
683
+ repo_workdir=str(repo.resolve()),
684
+ venv_activate=venv,
685
+ next_steps=(
686
+ f"In CodeBuddy, use the project Skill `.codebuddy/skills/briefloop/` for {ws_path}. "
687
+ "The main CodeBuddy session is the Orchestrator main agent and must invoke BriefLoop role sub-agents explicitly."
688
+ ),
689
+ prompt=(
690
+ f"CodeBuddy runtime for workspace: {ws_path}\n"
691
+ f"Repository: {repo.resolve()}\n"
692
+ f"Activate venv: source {venv}\n\n"
693
+ "Use the project Skill at `.codebuddy/skills/briefloop/SKILL.md` in the main CodeBuddy session. "
694
+ "Do not add or use `context: fork` for the BriefLoop Skill. The main CodeBuddy session is the Orchestrator main agent.\n\n"
695
+ "CodeBuddy role sub-agents are project-level assets:\n"
696
+ f"{role_agents}\n\n"
697
+ "Do not perform Scout, Screener, Claim Ledger, Analyst, Editor, Auditor, or Formatter work in the main conversation. "
698
+ "Explicitly invoke the matching CodeBuddy role sub-agent for role-owned artifact work. "
699
+ "Role sub-agents may draft only the artifacts assigned by the current handoff. "
700
+ "Role sub-agents must not run `briefloop` or `multi-agent-brief` CLI commands, edit control files, run gates, finalize, deliver, or authorize release. "
701
+ "CodeBuddy sub-agents cannot spawn other sub-agents; keep delegation depth at one.\n\n"
702
+ "The main CodeBuddy session owns deterministic BriefLoop CLI transactions. "
703
+ "After each role sub-agent returns, re-read `output/intermediate/agent_handoff.md` and "
704
+ "`output/intermediate/agent_handoff.json`, then run only the deterministic validation, "
705
+ "stage-complete, gate, repair, finalize, quality, or delivery command allowed by current state and user intent.\n\n"
706
+ "Read contract references before delegation:\n"
707
+ "- configs/orchestrator_contract.yaml\n"
708
+ "- configs/stage_specs.yaml\n"
709
+ "- configs/artifact_contracts.yaml\n"
710
+ "- configs/policy_packs/default.yaml\n\n"
711
+ f"Orchestrator loop: {ORCHESTRATOR_LOOP}\n\n"
712
+ f"{ROLE_TOPOLOGY_HANDOFF_NOTE}\n\n"
713
+ "CodeBuddy stage workflow:\n"
714
+ f"{role_sequence}\n\n"
715
+ f"{ORCHESTRATOR_CONTEXT_READING_NOTE}\n\n"
716
+ f"{DECISION_RECORDING_NOTE}\n\n"
717
+ f"{FINALIZE_GATE_NOTE}\n\n"
718
+ f"{REPAIR_GUIDANCE_NOTE}"
719
+ ),
720
+ expected_artifacts=list(EXPECTED_WORKFLOW_ARTIFACTS),
721
+ runtime_capabilities=_codebuddy_runtime_capabilities(),
722
+ notes=[
723
+ "CodeBuddy project Skill adapter is in .codebuddy/skills/briefloop/.",
724
+ "CodeBuddy role sub-agents are in .codebuddy/agents/briefloop-*.md.",
725
+ "The main CodeBuddy session is the Orchestrator and runs deterministic BriefLoop CLI transactions.",
726
+ "Role sub-agents draft handoff-assigned artifacts only and must not run CLI transactions.",
727
+ "This runtime handoff does not add gate authority, delivery approval, release authority, or semantic proof.",
728
+ ],
729
+ )
730
+
731
+
732
+ def _operator_artifact_ownership() -> dict[str, Any]:
733
+ return deepcopy(OPERATOR_ARTIFACT_OWNERSHIP)
734
+
735
+
736
+ def _operator_handoff(
737
+ workspace: Path,
738
+ repo: Path,
739
+ venv: str,
740
+ *,
741
+ legacy_alias: str | None = None,
742
+ ) -> AgentHandoff:
743
+ ws_path = str(workspace.resolve())
744
+ legacy_note = (
745
+ f'\nLegacy alias: runtime "{legacy_alias}" was resolved to "operator".'
746
+ if legacy_alias
747
+ else ""
748
+ )
749
+ return AgentHandoff(
750
+ runtime=RUNTIME_OPERATOR,
751
+ recipe=RUNTIME_RECIPE_FULL,
752
+ workspace=ws_path,
753
+ repo_workdir=str(repo.resolve()),
754
+ venv_activate=venv,
755
+ next_steps=(
756
+ "Use the operator runtime as a host-agnostic compact workflow. After all artifacts are ready, "
757
+ f"run: briefloop finalize --config {ws_path}/config.yaml"
758
+ ),
759
+ prompt=(
760
+ f"Operator runtime for workspace: {ws_path}\n"
761
+ f"Repository: {repo.resolve()}\n"
762
+ f"Activate venv: source {venv}\n\n"
763
+ "The operator is the Orchestrator main agent for this compact workflow. "
764
+ "This is a host-agnostic compact operator workflow. It does not assume "
765
+ "subagent/delegate capability. If your host provides a real child-agent "
766
+ "or delegate tool, you may delegate the named role. If not, one operator "
767
+ "may perform one role at a time as operator-authored artifact work, but "
768
+ "must not claim subagents ran.\n"
769
+ f"{legacy_note}\n\n"
770
+ "Read contract references before delegation:\n"
771
+ "- configs/orchestrator_contract.yaml\n"
772
+ "- configs/stage_specs.yaml\n"
773
+ "- configs/artifact_contracts.yaml\n"
774
+ "- configs/policy_packs/default.yaml\n\n"
775
+ f"Orchestrator loop: {ORCHESTRATOR_LOOP}\n\n"
776
+ f"{ROLE_TOPOLOGY_HANDOFF_NOTE}\n\n"
777
+ f"{ORCHESTRATOR_CONTEXT_READING_NOTE}\n\n"
778
+ f"{DECISION_RECORDING_NOTE}\n\n"
779
+ f"{FINALIZE_GATE_NOTE}\n\n"
780
+ f"{REPAIR_GUIDANCE_NOTE}\n\n"
781
+ "Run each step in order, verifying each artifact before continuing:\n\n"
782
+ f"1. briefloop doctor --config {ws_path}/config.yaml\n"
783
+ f"2. briefloop sources decide --config {ws_path}/config.yaml (if configured)\n"
784
+ " If runtime WebSearch reports `Did 0 searches`, or every query returns an empty result set, stop and request human review. Do not switch to source-planner or continue with stale sources.\n"
785
+ f"3. briefloop inputs extract --config {ws_path}/config.yaml (if PDF/DOCX/image inputs exist)\n"
786
+ f"4. briefloop inputs classify --config {ws_path}/config.yaml\n"
787
+ "5. Delegate the Scout role only if your host provides real delegation. Otherwise perform Scout as operator-authored artifact work. Runtime may split Scout work internally, but chunk outputs are scratch only; write the deterministic joined output/intermediate/candidate_claims.json once. Default topology: also write output/intermediate/screened_candidates.json from the joined candidate universe, then stage-complete scout. Strict topology: write only candidate_claims.json.\n"
788
+ "6. Default topology: do not delegate Screener and do not call state stage-complete --stage screener; topology satisfaction records Screener after Scout completes. Strict topology only: delegate the Screener role only if your host provides real delegation, otherwise perform Screener as operator-authored artifact work to write output/intermediate/screened_candidates.json.\n"
789
+ "7. Delegate the Claim Ledger role only if your host provides real delegation. Otherwise perform Claim Ledger as operator-authored artifact work to write output/intermediate/claim_drafts.json, then run "
790
+ f"briefloop state freeze-claim-ledger --workspace {ws_path} to create output/intermediate/claim_ledger.json, "
791
+ f"then run briefloop state stage-complete --workspace {ws_path} --stage claim-ledger --reason \"Claim Ledger was frozen from claim drafts.\"\n"
792
+ "8. Delegate the Analyst role only if your host provides real delegation. Otherwise perform Analyst as operator-authored artifact work. Read frozen output/intermediate/claim_ledger.json only, never claim_drafts.json, and write output/intermediate/audited_brief.md as a working draft. If output/intermediate/atomic_claim_graph.json is present and valid, use it only as an optional experimental structural decomposition aid; it is not source evidence or proof of support. Do not cite atom IDs, create/edit/repair/extend the graph, or introduce material atoms absent from the frozen Claim Ledger and valid graph. Analyst stage-complete freezes output/intermediate/analyst_draft_snapshot.md\n"
793
+ "9. Delegate the Editor / Delivery Editor role only if your host provides real delegation. Otherwise perform Editor as operator-authored artifact work to own and polish the final output/intermediate/audited_brief.md without adding facts. If output/intermediate/atomic_claim_graph.json is present and valid, use it only as an optional experimental structural decomposition aid; if absent or invalid, do not repair it. Do not cite atom IDs, create/edit/repair/extend the graph, or introduce material atoms absent from the frozen Claim Ledger and valid graph\n"
794
+ "10. Delegate the Auditor role only if your host provides real delegation. Otherwise perform Auditor as operator-authored artifact work to audit against frozen output/intermediate/claim_ledger.json, including overstatement and support-strength calibration, and write output/intermediate/audit_report.json\n"
795
+ f"11. briefloop gates check --workspace {ws_path} --stage auditor\n"
796
+ f"12. briefloop state check --workspace {ws_path} --strict\n"
797
+ f"13. briefloop state stage-complete --workspace {ws_path} --stage auditor --reason \"Audit and quality gates passed.\"\n"
798
+ f"14. briefloop finalize --config {ws_path}/config.yaml "
799
+ "(read audited_brief.md as frozen input; do not edit it during finalize)\n"
800
+ f"15. briefloop gates check --workspace {ws_path} --stage finalize --brief {ws_path}/output/brief.md\n"
801
+ f"16. briefloop state finalize-complete --workspace {ws_path} --reason \"Reader artifacts finalized and clean.\""
802
+ ),
803
+ expected_artifacts=list(EXPECTED_WORKFLOW_ARTIFACTS),
804
+ runtime_capabilities=_operator_runtime_capabilities(legacy_alias=legacy_alias),
805
+ artifact_ownership=_operator_artifact_ownership(),
806
+ legacy_runtime_alias=legacy_alias,
807
+ notes=[
808
+ "Operator runtime is host-agnostic: it does not assume subagents ran.",
809
+ "Delegate named roles only if the host provides real delegation; otherwise one operator performs one role at a time.",
810
+ "Each role step must complete before the next begins.",
811
+ "Verify each artifact exists and is non-empty before continuing.",
812
+ "Use inputs extract to convert PDF/DOCX/image inputs into .mineru.md before Scout reads evidence files.",
813
+ INPUT_DIRECTORY_ROLE_NOTE,
814
+ REPAIR_GUIDANCE_NOTE,
815
+ DECISION_RECORDING_NOTE,
816
+ FINALIZE_GATE_NOTE,
817
+ "The 'auditor' step and required gates check must run before finalize.",
818
+ ],
819
+ )
820
+
821
+
822
+ def _manual_handoff(workspace: Path, repo: Path, venv: str) -> AgentHandoff:
823
+ return _operator_handoff(workspace, repo, venv, legacy_alias=RUNTIME_MANUAL)
824
+
825
+
826
+ _HANDOFF_BUILDERS = {
827
+ RUNTIME_HERMES: _hermes_handoff,
828
+ RUNTIME_CLAUDE: _claude_handoff,
829
+ RUNTIME_OPENCODE: _opencode_handoff,
830
+ RUNTIME_CODEX: _codex_handoff,
831
+ RUNTIME_CODEBUDDY: _codebuddy_handoff,
832
+ RUNTIME_OPERATOR: _operator_handoff,
833
+ RUNTIME_MANUAL: _manual_handoff,
834
+ }
835
+
836
+
837
+ def build_handoff(
838
+ *,
839
+ workspace: str | Path,
840
+ repo_workdir: str | Path,
841
+ runtime: str,
842
+ recipe: str = RUNTIME_RECIPE_FULL,
843
+ venv: str | None = None,
844
+ run_doctor: bool = True,
845
+ ) -> AgentHandoff:
846
+ ws = Path(workspace).resolve()
847
+ repo = resolve_repo_workdir(repo_workdir, workspace=ws)
848
+ venv_activate = venv or _find_venv_activate(repo)
849
+
850
+ normalized_runtime, legacy_alias = normalize_runtime(runtime)
851
+
852
+ # resolve auto -> hermes in v0.5.5
853
+ resolved = RUNTIME_RESOLVED.get(normalized_runtime, normalized_runtime)
854
+
855
+ if resolved not in _HANDOFF_BUILDERS:
856
+ raise ValueError(f"Unknown runtime '{runtime}'. Valid: {', '.join(VALID_RUNTIMES)}")
857
+ if recipe not in VALID_RUNTIME_RECIPES:
858
+ raise ValueError(f"Unknown runtime recipe '{recipe}'. Valid: {', '.join(VALID_RUNTIME_RECIPES)}")
859
+
860
+ builder = _HANDOFF_BUILDERS[resolved]
861
+ if resolved == RUNTIME_OPERATOR:
862
+ handoff = _operator_handoff(ws, repo, venv_activate, legacy_alias=legacy_alias)
863
+ else:
864
+ handoff = builder(ws, repo, venv_activate)
865
+ handoff.recipe = recipe
866
+ if recipe == RUNTIME_RECIPE_FAST_RERUN:
867
+ _apply_fast_rerun_recipe(handoff, ws)
868
+
869
+ handoff.stage_completion_protocol = _build_stage_completion_protocol(repo)
870
+ protocol_text = _render_stage_completion_protocol_prompt(handoff.stage_completion_protocol)
871
+ handoff.prompt = f"{handoff.prompt}\n\n{protocol_text}"
872
+ _apply_experiment_080_assessment_target(handoff, ws)
873
+ _apply_sourcehub_projection(handoff, ws)
874
+ _apply_policy_profile_projection(handoff, ws)
875
+ _apply_report_template_projection(handoff, ws)
876
+ _apply_report_template_conformance_projection(handoff, ws)
877
+ _apply_report_template_render_plan_projection(handoff, ws)
878
+ _apply_trajectory_decision_narrowing_projection(handoff, ws)
879
+
880
+ if run_doctor:
881
+ rc, status = _run_doctor(ws)
882
+ handoff.doctor_status = status
883
+ if status == "passed":
884
+ handoff.notes.insert(0, f"Doctor: passed")
885
+
886
+ handoff.notes.append(RUNTIME_WEBSEARCH_ZERO_RESULT_NOTE)
887
+ handoff.notes.append(
888
+ "Feedback loop controls are optional: feedback_issues.json and repair_plan.json are created only by briefloop feedback ingest/plan/resolve."
889
+ )
890
+ handoff.notes.append(
891
+ "output/intermediate/gates/auditor_quality_gate_report.json and output/intermediate/gates/finalize_quality_gate_report.json are created only by briefloop gates check; output/intermediate/quality_gate_report.json is a legacy/latest projection, not an authoritative completion artifact."
892
+ )
893
+ handoff.notes.append(
894
+ "Provenance projection is optional: provenance_graph.json is created only by briefloop provenance build and is an audit/debug view, not semantic proof."
895
+ )
896
+ handoff.notes.append(
897
+ "Audience memory is runtime context: audience_profile_snapshot.md is frozen per run and exposed through handoff; it is not source evidence or an artifact gate."
898
+ )
899
+ handoff.notes.append(
900
+ "Control switchboard is runtime control context: orchestrator_control_switchboard.json recommends controls and control_selections.json records Orchestrator selections; selection is not execution."
901
+ )
902
+ handoff.notes.append(
903
+ "Stage completion protocol is embedded in agent_handoff.json/agent_handoff.md; do not depend on any sidecar REFERENCE_RUN_ORCHESTRATOR_PROTOCOL.md file."
904
+ )
905
+
906
+ return handoff
907
+
908
+
909
+ def _apply_trajectory_decision_narrowing_projection(handoff: AgentHandoff, workspace: Path) -> None:
910
+ narrowing = _read_workflow_trajectory_narrowing(workspace)
911
+ if not narrowing:
912
+ return
913
+ allowed = narrowing.get("allowed_decisions") if isinstance(narrowing.get("allowed_decisions"), list) else []
914
+ reasons = narrowing.get("reasons") if isinstance(narrowing.get("reasons"), list) else []
915
+ handoff.notes.insert(
916
+ 0,
917
+ "Trajectory regulation narrowed current-stage decisions: "
918
+ f"stage={narrowing.get('stage_id') or 'unknown'}; "
919
+ f"allowed={', '.join(str(item) for item in allowed) or 'request_human_review, block_run'}; "
920
+ f"reasons={', '.join(str(item) for item in reasons) or 'trajectory_budget_exceeded'}. "
921
+ "Do not call `state decide --decision retry_stage`; record request_human_review or block_run.",
922
+ )
923
+
924
+
925
+ def _read_workflow_trajectory_narrowing(workspace: Path) -> dict[str, Any] | None:
926
+ path = workspace / RUNTIME_STATE_FILES["workflow_state"]
927
+ try:
928
+ payload = json.loads(path.read_text(encoding="utf-8"))
929
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
930
+ return None
931
+ if not isinstance(payload, dict):
932
+ return None
933
+ narrowing = payload.get("trajectory_regulation")
934
+ if not isinstance(narrowing, dict):
935
+ return None
936
+ if narrowing.get("status") != "decision_narrowed":
937
+ return None
938
+ return narrowing
939
+
940
+
941
+ def _apply_sourcehub_projection(handoff: AgentHandoff, workspace: Path) -> None:
942
+ projection = project_sourcehub_handoff(workspace)
943
+ handoff.sourcehub_projection = projection
944
+ if projection.get("status") == "not_available":
945
+ return
946
+ runtime_web = (
947
+ projection.get("runtime_web_search")
948
+ if isinstance(projection.get("runtime_web_search"), dict)
949
+ else {}
950
+ )
951
+ tasks = runtime_web.get("search_tasks") if isinstance(runtime_web.get("search_tasks"), list) else []
952
+ task_text = "; ".join(
953
+ str(item.get("query") or "").strip()
954
+ for item in tasks[:5]
955
+ if isinstance(item, dict) and item.get("query")
956
+ ) or "none"
957
+ text = (
958
+ "SourceHub Lite projection: "
959
+ f"status={projection.get('status')}; "
960
+ f"source_config={projection.get('source_config_path') or 'sources.yaml'}; "
961
+ f"runtime_web_search_mode={runtime_web.get('mode') or 'unknown'}; "
962
+ f"runtime_web_search_tasks={runtime_web.get('task_count', 0)}; "
963
+ f"queries={task_text}; "
964
+ "boundary=sourcehub_lite_handoff_projection_only; runtime_effect=handoff_only. "
965
+ "If runtime web-search tasks are present, the Orchestrator must use the "
966
+ "runtime search tool and materialize useful results into input/sources/ "
967
+ "before source-discovery completion. Do not call Python --search unless "
968
+ "web_search.mode is external_api. Do not treat source_candidates.yaml or "
969
+ "search summaries as evidence, and do not use SourceHub setup to bypass "
970
+ "Claim Ledger, gates, or human delivery."
971
+ )
972
+ handoff.prompt = f"{handoff.prompt}\n\n{text}"
973
+ handoff.notes.append(text)
974
+
975
+
976
+ def _apply_policy_profile_projection(handoff: AgentHandoff, workspace: Path) -> None:
977
+ projection = project_workspace_policy_profile(workspace)
978
+ handoff.policy_profile_projection = projection
979
+ if projection.get("status") == "not_available":
980
+ return
981
+ resolved = projection.get("resolved_policy_profile") or projection.get("policy_profile") or "unknown"
982
+ source = projection.get("source") or "unknown"
983
+ text = (
984
+ "PolicyProfile projection: "
985
+ f"status={projection.get('status')}; "
986
+ f"resolved_policy_profile={resolved}; "
987
+ f"source={source}; "
988
+ "boundary=product_policy_profile_projection_only; runtime_effect=none. "
989
+ "Use this as product metadata only. Do not adapt gates, treat it as "
990
+ "source evidence, infer compliance/truth/release authority, or bypass "
991
+ "the Claim Ledger / quality gates / human delivery approval spine."
992
+ )
993
+ handoff.prompt = f"{handoff.prompt}\n\n{text}"
994
+ handoff.notes.append(text)
995
+
996
+
997
+ def _apply_report_template_projection(handoff: AgentHandoff, workspace: Path) -> None:
998
+ projection = project_workspace_report_template(workspace)
999
+ handoff.report_template_projection = projection
1000
+ if projection.get("status") == "not_available":
1001
+ return
1002
+ status = projection.get("status")
1003
+ section_order = projection.get("section_order") if isinstance(projection.get("section_order"), list) else []
1004
+ section_text = ", ".join(str(item) for item in section_order) or "none"
1005
+ usage_text = (
1006
+ "Use this as a stable product section-order contract when drafting."
1007
+ if status == "resolved"
1008
+ else "Do not use this as a section-order contract; validate and fix report_spec.yaml first."
1009
+ )
1010
+ text = (
1011
+ "ReportTemplate projection: "
1012
+ f"status={status}; "
1013
+ f"template_id={projection.get('template_id') or 'unknown'}; "
1014
+ f"report_type={projection.get('report_type') or 'unknown'}; "
1015
+ f"section_order={section_text}; "
1016
+ "boundary=product_report_template_projection_only; runtime_effect=none. "
1017
+ f"{usage_text} "
1018
+ "It does not render templates, rewrite content, bypass gates, deliver "
1019
+ "reports, or authorize publication."
1020
+ )
1021
+ handoff.prompt = f"{handoff.prompt}\n\n{text}"
1022
+ handoff.notes.append(text)
1023
+
1024
+
1025
+ def _apply_report_template_conformance_projection(handoff: AgentHandoff, workspace: Path) -> None:
1026
+ projection = project_workspace_report_template_conformance(workspace)
1027
+ handoff.report_template_conformance_projection = projection
1028
+ if projection.get("status") in {"not_available", "no_targets"}:
1029
+ return
1030
+ counts = projection.get("summary_counts") if isinstance(projection.get("summary_counts"), dict) else {}
1031
+ targets = projection.get("targets") if isinstance(projection.get("targets"), list) else []
1032
+ diagnostics = _report_template_conformance_diagnostics(targets)
1033
+ guidance = (
1034
+ "Keep existing section structure aligned with the resolved ReportTemplate order."
1035
+ if projection.get("status") == "pass"
1036
+ else "Use these diagnostics as structure guidance only; revise through the owner role if section headings need changes."
1037
+ )
1038
+ text = (
1039
+ "ReportTemplate conformance projection: "
1040
+ f"status={projection.get('status')}; "
1041
+ f"present_targets={counts.get('present_target_count', 0)}; "
1042
+ f"warnings={counts.get('warning_target_count', 0)}; "
1043
+ f"missing_sections={counts.get('missing_section_count', 0)}; "
1044
+ f"out_of_order={counts.get('out_of_order_section_count', 0)}; "
1045
+ f"extra_headings={counts.get('extra_heading_count', 0)}; "
1046
+ f"reader_contract_warnings={counts.get('reader_block_warning_count', 0)}; "
1047
+ f"diagnostics={diagnostics or 'none'}; "
1048
+ "boundary=product_report_template_conformance_projection_only; runtime_effect=none. "
1049
+ f"{guidance} "
1050
+ "This does not render templates, rewrite content, bypass gates, deliver reports, "
1051
+ "or authorize publication."
1052
+ )
1053
+ handoff.prompt = f"{handoff.prompt}\n\n{text}"
1054
+ handoff.notes.append(text)
1055
+
1056
+
1057
+ def _apply_report_template_render_plan_projection(handoff: AgentHandoff, workspace: Path) -> None:
1058
+ projection = project_workspace_report_template_render_plan(workspace)
1059
+ handoff.report_template_render_plan_projection = projection
1060
+ if projection.get("status") == "not_available":
1061
+ return
1062
+ counts = projection.get("summary_counts") if isinstance(projection.get("summary_counts"), dict) else {}
1063
+ selected_source = projection.get("selected_source_artifact") or "none"
1064
+ unresolved = [
1065
+ str(item.get("section"))
1066
+ for item in (projection.get("unresolved_sections") or [])[:5]
1067
+ if isinstance(item, dict) and item.get("section")
1068
+ ]
1069
+ unresolved_text = ",".join(unresolved) if unresolved else "none"
1070
+ text = (
1071
+ "ReportTemplate render plan projection: "
1072
+ f"status={projection.get('status')}; "
1073
+ f"template_id={projection.get('template_id') or 'unknown'}; "
1074
+ f"source={selected_source}; "
1075
+ f"sections={counts.get('section_count', 0)}; "
1076
+ f"unresolved={counts.get('unresolved_section_count', 0)}; "
1077
+ f"planned_targets={counts.get('planned_delivery_target_count', 0)}; "
1078
+ f"unresolved_sections={unresolved_text}; "
1079
+ "boundary=product_report_template_render_plan_projection_only; runtime_effect=none. "
1080
+ "Use this as render planning diagnostics only. It does not render templates, "
1081
+ "rewrite content, call finalize, bypass gates, deliver reports, or authorize publication."
1082
+ )
1083
+ handoff.prompt = f"{handoff.prompt}\n\n{text}"
1084
+ handoff.notes.append(text)
1085
+
1086
+
1087
+ def _report_template_conformance_diagnostics(targets: list[Any]) -> str:
1088
+ parts: list[str] = []
1089
+ for target in targets:
1090
+ if not isinstance(target, dict) or target.get("status") not in {"warning", "unreadable"}:
1091
+ continue
1092
+ artifact = str(target.get("target_artifact") or "unknown")
1093
+ missing = [str(item) for item in (target.get("missing_sections") or [])[:5]]
1094
+ out_of_order = [str(item) for item in (target.get("out_of_order_sections") or [])[:5]]
1095
+ extra = [str(item) for item in (target.get("extra_headings") or [])[:5]]
1096
+ reader_warnings = [
1097
+ str(item.get("type"))
1098
+ for item in (target.get("reader_block_warnings") or [])[:5]
1099
+ if isinstance(item, dict) and item.get("type")
1100
+ ]
1101
+ bits: list[str] = []
1102
+ if missing:
1103
+ bits.append("missing=" + ",".join(missing))
1104
+ if out_of_order:
1105
+ bits.append("out_of_order=" + ",".join(out_of_order))
1106
+ if extra:
1107
+ bits.append("extra=" + ",".join(extra))
1108
+ if reader_warnings:
1109
+ bits.append("reader_contract=" + ",".join(reader_warnings))
1110
+ if target.get("error"):
1111
+ bits.append("error=" + str(target.get("error")))
1112
+ if bits:
1113
+ parts.append(f"{artifact}[" + ";".join(bits) + "]")
1114
+ return " | ".join(parts[:3])
1115
+
1116
+
1117
+ def _build_stage_completion_protocol(repo: Path) -> dict[str, Any]:
1118
+ stages = load_stage_specs(repo)
1119
+ artifacts = load_artifact_contracts(repo)
1120
+ artifact_by_id = {str(item.get("artifact_id")): item for item in artifacts if item.get("artifact_id")}
1121
+ stage_ids = {
1122
+ str(stage.get("stage_id") or "")
1123
+ for stage in stages
1124
+ if stage.get("stage_id")
1125
+ }
1126
+ stage_protocol: list[dict[str, Any]] = []
1127
+
1128
+ for stage in stages:
1129
+ stage_id = str(stage.get("stage_id") or "")
1130
+ consumes = [str(item) for item in (stage.get("consumes") or [])]
1131
+ expected = [str(item) for item in (stage.get("expected_artifacts") or [])]
1132
+ required_inputs: list[dict[str, Any]] = []
1133
+ context_inputs: list[str] = []
1134
+ required_outputs: list[dict[str, Any]] = []
1135
+
1136
+ for item in consumes:
1137
+ artifact = artifact_by_id.get(item)
1138
+ if artifact:
1139
+ required_inputs.append(_protocol_artifact_ref(item, artifact))
1140
+ else:
1141
+ context_inputs.append(item)
1142
+
1143
+ for item in expected:
1144
+ artifact = artifact_by_id.get(item)
1145
+ if artifact:
1146
+ required_outputs.append(_protocol_artifact_ref(item, artifact))
1147
+ else:
1148
+ required_outputs.append({
1149
+ "artifact_id": item,
1150
+ "path": item,
1151
+ "required": True,
1152
+ "format": "",
1153
+ })
1154
+
1155
+ topology_satisfaction = _protocol_topology_satisfaction(
1156
+ stage_id,
1157
+ stage.get("topology_satisfaction") or {},
1158
+ artifact_by_id,
1159
+ stage_ids=stage_ids,
1160
+ )
1161
+ freeze_input_artifacts: list[dict[str, Any]] = []
1162
+ pre_completion_transactions: list[dict[str, Any]] = []
1163
+ if stage_id == "claim-ledger" and "claim_drafts" in artifact_by_id:
1164
+ claim_drafts_ref = _protocol_artifact_ref("claim_drafts", artifact_by_id["claim_drafts"])
1165
+ claim_ledger_ref = _protocol_artifact_ref("claim_ledger", artifact_by_id["claim_ledger"])
1166
+ freeze_input_artifacts.append(claim_drafts_ref)
1167
+ pre_completion_transactions.append({
1168
+ "transaction": "freeze_claim_ledger",
1169
+ "command": "briefloop state freeze-claim-ledger --workspace <workspace>",
1170
+ "reads": [claim_drafts_ref],
1171
+ "writes": [claim_ledger_ref],
1172
+ "must_run_before": "state stage-complete --stage claim-ledger",
1173
+ })
1174
+ if stage_id == "analyst" and "audited_brief" in artifact_by_id and "analyst_draft_snapshot" in artifact_by_id:
1175
+ audited_brief_ref = _protocol_artifact_ref("audited_brief", artifact_by_id["audited_brief"])
1176
+ snapshot_ref = _protocol_artifact_ref(
1177
+ "analyst_draft_snapshot",
1178
+ artifact_by_id["analyst_draft_snapshot"],
1179
+ )
1180
+ freeze_input_artifacts.append(audited_brief_ref)
1181
+ pre_completion_transactions.append({
1182
+ "transaction": "snapshot_analyst_draft",
1183
+ "command": "briefloop state stage-complete --workspace <workspace> --stage analyst",
1184
+ "reads": [audited_brief_ref],
1185
+ "writes": [snapshot_ref],
1186
+ "must_run_before": "delegate editor",
1187
+ })
1188
+ parallel_chunk_contract: dict[str, Any] | None = None
1189
+ if stage_id == "scout":
1190
+ parallel_chunk_contract = {
1191
+ "status": "runtime_internal_optional",
1192
+ "scratch_only": True,
1193
+ "final_joined_artifacts": {
1194
+ "always": ["output/intermediate/candidate_claims.json"],
1195
+ "default_topology": ["output/intermediate/screened_candidates.json"],
1196
+ },
1197
+ "join_requirement": (
1198
+ "Join chunk outputs deterministically before writing workflow artifacts; "
1199
+ "stable ordering must use source identity, source path or URL, source date, topic, and evidence text."
1200
+ ),
1201
+ "forbidden": [
1202
+ "append to candidate_claims.json from chunk workers",
1203
+ "treat chunk outputs as workflow artifacts",
1204
+ "drop duplicates silently during chunk join",
1205
+ ],
1206
+ }
1207
+ contract_notes: list[str] = []
1208
+ if stage_id in {"analyst", "editor"}:
1209
+ contract_notes.extend([
1210
+ (
1211
+ "Optional atomic graph: output/intermediate/atomic_claim_graph.json may be read only "
1212
+ "when present and valid as an optional experimental structural decomposition aid for "
1213
+ "frozen Claim Ledger claims; it is not source evidence or proof of support."
1214
+ ),
1215
+ (
1216
+ "No-new-atom boundary: do not cite atom IDs, do not create/edit/repair/extend "
1217
+ "atomic_claim_graph.json, and do not introduce material atoms absent from the frozen "
1218
+ "Claim Ledger and valid graph; if the graph is absent or invalid, do not repair it."
1219
+ ),
1220
+ ])
1221
+ independent_topologies = [
1222
+ topology
1223
+ for topology in _ordered_role_topologies()
1224
+ if topology not in topology_satisfaction
1225
+ ] if topology_satisfaction else []
1226
+ stage_protocol.append({
1227
+ "stage_id": stage_id,
1228
+ "owner": str(stage.get("owner") or ""),
1229
+ "category": str(stage.get("category") or ""),
1230
+ "required_input_artifacts": required_inputs,
1231
+ "context_inputs": context_inputs,
1232
+ "required_output_artifacts": required_outputs,
1233
+ "freeze_input_artifacts": freeze_input_artifacts,
1234
+ "pre_completion_transactions": pre_completion_transactions,
1235
+ "parallel_chunk_contract": parallel_chunk_contract,
1236
+ "contract_notes": contract_notes,
1237
+ "topology_satisfaction": topology_satisfaction,
1238
+ "independent_completion_topologies": independent_topologies,
1239
+ "allowed_decisions": [str(item) for item in (stage.get("allowed_decisions") or [])],
1240
+ "forbidden_actions": list(DEFAULT_STAGE_FORBIDDEN_ACTIONS),
1241
+ "completion_condition": (
1242
+ "The stage is complete only when every required output artifact exists at the declared path, "
1243
+ "passes available shape/schema validation, and the Orchestrator can cite those artifact paths."
1244
+ ),
1245
+ })
1246
+
1247
+ return {
1248
+ "schema_version": STAGE_COMPLETION_PROTOCOL_SCHEMA,
1249
+ "status": "canonical_handoff_protocol",
1250
+ "rules": list(STAGE_COMPLETION_PROTOCOL_RULES),
1251
+ "stages": stage_protocol,
1252
+ }
1253
+
1254
+
1255
+ def _ordered_role_topologies() -> list[str]:
1256
+ preferred = ["default", "strict", "human_assisted"]
1257
+ return [
1258
+ item for item in preferred if item in ROLE_TOPOLOGY_VALUES
1259
+ ] + sorted(str(item) for item in ROLE_TOPOLOGY_VALUES if item not in preferred)
1260
+
1261
+
1262
+ def _protocol_topology_satisfaction(
1263
+ stage_id: str,
1264
+ rules: dict[str, Any],
1265
+ artifact_by_id: dict[str, dict[str, Any]],
1266
+ *,
1267
+ stage_ids: set[str],
1268
+ ) -> dict[str, dict[str, Any]]:
1269
+ result: dict[str, dict[str, Any]] = {}
1270
+ for topology, rule in sorted(rules.items()):
1271
+ if not isinstance(rule, dict):
1272
+ continue
1273
+ satisfied_by = str(rule.get("satisfied_by") or "")
1274
+ required_artifacts: list[dict[str, Any]] = []
1275
+ for artifact_id in rule.get("required_artifacts") or []:
1276
+ artifact_key = str(artifact_id)
1277
+ artifact = artifact_by_id.get(artifact_key)
1278
+ if artifact:
1279
+ required_artifacts.append(_protocol_artifact_ref(artifact_key, artifact))
1280
+ else:
1281
+ required_artifacts.append({
1282
+ "artifact_id": artifact_key,
1283
+ "path": artifact_key,
1284
+ "required": True,
1285
+ "format": "",
1286
+ })
1287
+ projected_rule = {
1288
+ "satisfied_by": satisfied_by,
1289
+ "required_artifacts": required_artifacts,
1290
+ }
1291
+ if satisfied_by in stage_ids:
1292
+ projected_rule["forbidden_replay_actions"] = [
1293
+ f"delegate {stage_id}",
1294
+ f"state stage-complete --stage {stage_id}",
1295
+ ]
1296
+ result[str(topology)] = projected_rule
1297
+ return result
1298
+
1299
+
1300
+ def _protocol_artifact_ref(artifact_id: str, artifact: dict[str, Any]) -> dict[str, Any]:
1301
+ return {
1302
+ "artifact_id": artifact_id,
1303
+ "path": str(artifact.get("path") or ""),
1304
+ "required": bool(artifact.get("required", False)),
1305
+ "format": str(artifact.get("format") or ""),
1306
+ }
1307
+
1308
+
1309
+ def _render_stage_completion_protocol_prompt(protocol: dict[str, Any]) -> str:
1310
+ lines = [
1311
+ "Stage completion protocol:",
1312
+ "- This protocol is embedded in the handoff; do not rely on a sidecar REFERENCE_RUN_ORCHESTRATOR_PROTOCOL.md file.",
1313
+ ]
1314
+ for rule in protocol.get("rules") or []:
1315
+ lines.append(f"- {rule}")
1316
+ lines.extend(["", "Per-stage artifact proof requirements:"])
1317
+ for stage in protocol.get("stages") or []:
1318
+ stage_id = stage.get("stage_id")
1319
+ inputs = _protocol_paths(stage.get("required_input_artifacts") or [])
1320
+ outputs = _protocol_paths(stage.get("required_output_artifacts") or [])
1321
+ context = ", ".join(stage.get("context_inputs") or []) or "none"
1322
+ topology_satisfaction = stage.get("topology_satisfaction") or {}
1323
+ independent_topologies = ", ".join(stage.get("independent_completion_topologies") or []) or "none"
1324
+ freeze_inputs = _protocol_paths(stage.get("freeze_input_artifacts") or [])
1325
+ lines.append(f"- {stage_id}:")
1326
+ lines.append(f" required input artifacts: {inputs}")
1327
+ lines.append(f" context inputs: {context}")
1328
+ if topology_satisfaction:
1329
+ lines.append(f" topology satisfaction: {_render_topology_satisfaction(topology_satisfaction)}")
1330
+ lines.append(f" independent completion topologies: {independent_topologies}")
1331
+ if freeze_inputs != "none":
1332
+ lines.append(f" independent completion requires control output ({independent_topologies}): {outputs}")
1333
+ else:
1334
+ lines.append(f" independent MUST produce ({independent_topologies}): {outputs}")
1335
+ else:
1336
+ if freeze_inputs != "none":
1337
+ lines.append(f" stage-complete control output: {outputs}")
1338
+ else:
1339
+ lines.append(f" MUST produce: {outputs}")
1340
+ chunk_contract = stage.get("parallel_chunk_contract")
1341
+ if isinstance(chunk_contract, dict):
1342
+ lines.append(
1343
+ " optional chunk parallelism: runtime-internal scratch only; "
1344
+ f"{chunk_contract.get('join_requirement')}"
1345
+ )
1346
+ forbidden = ", ".join(str(item) for item in (chunk_contract.get("forbidden") or []))
1347
+ if forbidden:
1348
+ lines.append(f" chunk forbidden: {forbidden}")
1349
+ for note in stage.get("contract_notes") or []:
1350
+ lines.append(f" contract note: {note}")
1351
+ if freeze_inputs != "none":
1352
+ lines.append(f" role MUST produce freeze input: {freeze_inputs}")
1353
+ for transaction in stage.get("pre_completion_transactions") or []:
1354
+ reads = _protocol_paths(transaction.get("reads") or [])
1355
+ writes = _protocol_paths(transaction.get("writes") or [])
1356
+ lines.append(
1357
+ " control transaction before stage-complete: "
1358
+ f"{transaction.get('command')} (reads {reads}; writes {writes})"
1359
+ )
1360
+ lines.append(" forbidden: no prose-only completion, no upstream mutation, no invented evidence, no skipped completion transaction.")
1361
+ return "\n".join(lines)
1362
+
1363
+
1364
+ def _render_topology_satisfaction(rules: dict[str, Any]) -> str:
1365
+ parts: list[str] = []
1366
+ for topology in _ordered_role_topologies():
1367
+ rule = rules.get(topology)
1368
+ if not isinstance(rule, dict):
1369
+ continue
1370
+ artifacts = _protocol_paths(rule.get("required_artifacts") or [])
1371
+ forbidden = ", ".join(str(item) for item in (rule.get("forbidden_replay_actions") or []))
1372
+ replay_text = f"; do not replay {forbidden}" if forbidden else ""
1373
+ parts.append(
1374
+ f"{topology}: satisfied by {rule.get('satisfied_by') or 'unknown'} "
1375
+ f"when {artifacts} exist{replay_text}"
1376
+ )
1377
+ return "; ".join(parts) if parts else "none"
1378
+
1379
+
1380
+ def _protocol_paths(items: list[dict[str, Any]]) -> str:
1381
+ paths = [
1382
+ f"{item.get('artifact_id')} at {item.get('path')}"
1383
+ for item in items
1384
+ if item.get("artifact_id")
1385
+ ]
1386
+ return ", ".join(paths) if paths else "none"
1387
+
1388
+
1389
+ def _append_artifact_ownership_markdown(md_content: list[str], ownership: dict[str, Any]) -> None:
1390
+ if not ownership:
1391
+ return
1392
+ md_content.extend([
1393
+ "## Artifact Ownership",
1394
+ "",
1395
+ f"- Schema: `{ownership.get('schema_version', '')}`",
1396
+ f"- Boundary: {ownership.get('boundary', '')}",
1397
+ "",
1398
+ "### Agent-Owned Drafts",
1399
+ "",
1400
+ ])
1401
+ for item in ownership.get("agent_owned_drafts") or []:
1402
+ if isinstance(item, dict):
1403
+ md_content.append(
1404
+ f"- `{item.get('path')}`: owner=`{item.get('owner')}`; {item.get('write_rule')}"
1405
+ )
1406
+ md_content.extend(["", "### CLI-Owned Outputs", ""])
1407
+ for item in ownership.get("cli_owned_outputs") or []:
1408
+ if isinstance(item, dict):
1409
+ md_content.append(f"- `{item.get('path')}` via `{item.get('command')}`")
1410
+ md_content.extend(["", "### Read-Only Diagnostics", ""])
1411
+ for path in ownership.get("read_only_diagnostics") or []:
1412
+ md_content.append(f"- `{path}`")
1413
+ md_content.extend(["", "### Forbidden Direct Edits", ""])
1414
+ for path in ownership.get("forbidden_direct_edits") or []:
1415
+ md_content.append(f"- `{path}`")
1416
+ md_content.append("")
1417
+
1418
+
1419
+ def _apply_fast_rerun_recipe(handoff: AgentHandoff, workspace: Path) -> None:
1420
+ summary = require_fast_rerun_handoff_ready(workspace)
1421
+ imported_stages = ", ".join(
1422
+ f"{stage.get('stage_id')}={stage.get('display_status')}"
1423
+ for stage in summary.get("imported_stages") or []
1424
+ )
1425
+ freshness = summary.get("freshness_at_import") if isinstance(summary.get("freshness_at_import"), dict) else {}
1426
+ freshness_status = freshness.get("status") or "unknown"
1427
+ guidance = [
1428
+ "Runtime recipe: fast-rerun.",
1429
+ "same frozen evidence, new writing -- verified by hash.",
1430
+ f"Source run: {summary.get('source_run_id')}",
1431
+ f"Imported fact-layer hash: {summary.get('fact_layer_sha256')}",
1432
+ f"Source archive manifest hash: {summary.get('source_archive_manifest_sha256')}",
1433
+ f"Imported files: {summary.get('imported_file_count')}",
1434
+ f"Freshness at import: {freshness_status}; target delivery freshness is checked again at finalize-complete.",
1435
+ f"Imported-satisfied upstream stages: {imported_stages}.",
1436
+ "Start model-backed content work at Analyst.",
1437
+ "Do not regenerate source-discovery, input-governance, Scout, Screener, or Claim Ledger.",
1438
+ "Do not synthesize or backfill upstream stage-complete, decision_recorded, "
1439
+ "or stage_status_changed events for imported stages.",
1440
+ "Do not add facts outside the imported Claim Ledger.",
1441
+ "Continue through Analyst, Editor, Auditor, gates, finalize, finalize-complete, human delivery, and archive.",
1442
+ "Timing comparability is downstream_only: upstream fact-layer stages were "
1443
+ "satisfied by import and must not be compared directly with full runs.",
1444
+ "If runtime_manifest.fact_layer_import is missing or invalid, stop and run "
1445
+ "`briefloop state import-fact-layer` first; do not silently fall back to a full run.",
1446
+ "This recipe is Experimental/internal in v0.8.1 and is not quality-equivalent to a full workflow.",
1447
+ ]
1448
+ text = "\n".join(guidance)
1449
+ handoff.prompt = f"{handoff.prompt}\n\n{text}"
1450
+ handoff.notes.append(text)
1451
+ handoff.next_steps = (
1452
+ "Use this fast-rerun handoff only after the imported fact layer is valid. "
1453
+ "Start at Analyst; do not rerun imported upstream fact-layer stages."
1454
+ )
1455
+
1456
+
1457
+ def _apply_experiment_080_assessment_target(handoff: AgentHandoff, workspace: Path) -> None:
1458
+ metadata = load_experiment_080_condition_metadata(workspace)
1459
+ if not isinstance(metadata, dict):
1460
+ return
1461
+ target = assessment_target(metadata)
1462
+ manifest = assessment_target_manifest(target)
1463
+ handoff.assessment_target_manifest = manifest
1464
+ if target != "auditable_brief":
1465
+ return
1466
+ handoff.prompt = _without_auditable_delivery_steps(handoff.prompt)
1467
+ handoff.notes = [
1468
+ note
1469
+ for note in handoff.notes
1470
+ if not _is_auditable_delivery_step_note(note)
1471
+ ]
1472
+ handoff.stage_completion_protocol = _without_auditable_delivery_protocol(handoff.stage_completion_protocol)
1473
+ text = (
1474
+ "Experiment target: auditable_brief.\n"
1475
+ "TARGET COMPLETE: auditable_brief when Analyst, Editor, Auditor, and auditor gates are complete and clean.\n"
1476
+ "This is an internal auditable-draft experiment target, not management-ready delivery.\n"
1477
+ "For this target, auditor quality gates must have status pass before auditor stage-complete; "
1478
+ "warning is not enough even when there are 0 blocking findings. Repair warnings before completing auditor.\n"
1479
+ "After auditor stage-complete succeeds, stop the runtime workflow and register/score the run with "
1480
+ "`briefloop experiments 080 register-run` and `briefloop experiments 080 score-run`.\n"
1481
+ "Do not run finalize, finalize-complete, deliver, DOCX/PDF rendering, reader-clean delivery checks, "
1482
+ "or delivery archive for this target unless the operator explicitly starts a delivery_brief run."
1483
+ )
1484
+ handoff.prompt = f"{handoff.prompt}\n\n{text}"
1485
+ handoff.notes.append(text)
1486
+ handoff.expected_artifacts = [
1487
+ artifact for artifact in handoff.expected_artifacts if artifact != "output/delivery/brief.md"
1488
+ ]
1489
+ handoff.artifact_ownership = _without_auditable_delivery_artifact_ownership(handoff.artifact_ownership)
1490
+ handoff.runtime_capabilities = _without_auditable_delivery_runtime_capabilities(handoff.runtime_capabilities)
1491
+ handoff.next_steps = (
1492
+ "080 auditable_brief target: stop after auditor stage-complete; next allowed commands are "
1493
+ "experiments 080 register-run and score-run."
1494
+ )
1495
+
1496
+
1497
+ def _without_auditable_delivery_steps(text: str) -> str:
1498
+ replacements = {
1499
+ " → auditor → finalize": " → auditor",
1500
+ " -> auditor -> finalize": " -> auditor",
1501
+ "auditor → finalize": "auditor",
1502
+ "auditor -> finalize": "auditor",
1503
+ }
1504
+ for old, new in replacements.items():
1505
+ text = text.replace(old, new)
1506
+ blocked_fragments = (
1507
+ "briefloop finalize",
1508
+ "state finalize-complete",
1509
+ "--stage finalize",
1510
+ "formatter/finalize ->",
1511
+ "Finalize is a Python delivery/rendering tool",
1512
+ "Final status must list the delivery bundle",
1513
+ "Continue through Analyst, Editor, Auditor, gates, finalize",
1514
+ "After all artifacts are ready",
1515
+ "Before finalize,",
1516
+ "After the finalize tool writes",
1517
+ "The 'auditor' step and required gates check must run before finalize",
1518
+ "Record finalize completion",
1519
+ "Formatter/finalize may only write",
1520
+ "briefloop-formatter",
1521
+ "finalize readiness",
1522
+ "Run finalize, finalize gate",
1523
+ )
1524
+ kept: list[str] = []
1525
+ skipping_finalize_block = False
1526
+ for line in text.splitlines():
1527
+ if line.startswith("- finalize:"):
1528
+ skipping_finalize_block = True
1529
+ continue
1530
+ if skipping_finalize_block and line.startswith("- ") and not line.startswith(" "):
1531
+ skipping_finalize_block = False
1532
+ if skipping_finalize_block:
1533
+ continue
1534
+ if any(fragment in line for fragment in blocked_fragments):
1535
+ continue
1536
+ kept.append(line)
1537
+ return "\n".join(kept).strip()
1538
+
1539
+
1540
+ def _is_auditable_delivery_step_note(note: str) -> bool:
1541
+ blocked_fragments = (
1542
+ FINALIZE_GATE_NOTE,
1543
+ "finalize",
1544
+ "delivery bundle",
1545
+ "delivery artifacts",
1546
+ "formatter",
1547
+ )
1548
+ return any(fragment in note for fragment in blocked_fragments)
1549
+
1550
+
1551
+ def _without_auditable_delivery_protocol(protocol: dict[str, Any]) -> dict[str, Any]:
1552
+ if not isinstance(protocol, dict):
1553
+ return protocol
1554
+ rewritten = deepcopy(protocol)
1555
+ rewritten["stages"] = [
1556
+ stage
1557
+ for stage in rewritten.get("stages", [])
1558
+ if not (isinstance(stage, dict) and stage.get("stage_id") == "finalize")
1559
+ ]
1560
+ rewritten["rules"] = [
1561
+ rule
1562
+ for rule in rewritten.get("rules", [])
1563
+ if "finalize" not in str(rule).lower() and "reader delivery" not in str(rule).lower()
1564
+ ]
1565
+ return rewritten
1566
+
1567
+
1568
+ def _without_auditable_delivery_artifact_ownership(ownership: dict[str, Any]) -> dict[str, Any]:
1569
+ if not isinstance(ownership, dict):
1570
+ return ownership
1571
+ rewritten = deepcopy(ownership)
1572
+ rewritten["cli_owned_outputs"] = [
1573
+ item
1574
+ for item in rewritten.get("cli_owned_outputs", [])
1575
+ if not _is_auditable_delivery_owned_output(item)
1576
+ ]
1577
+ return rewritten
1578
+
1579
+
1580
+ def _without_auditable_delivery_runtime_capabilities(capabilities: dict[str, Any]) -> dict[str, Any]:
1581
+ if not isinstance(capabilities, dict):
1582
+ return capabilities
1583
+ rewritten = dict(capabilities)
1584
+ subagents = rewritten.get("subagent_names")
1585
+ if isinstance(subagents, list):
1586
+ rewritten["subagent_names"] = [
1587
+ name for name in subagents
1588
+ if "formatter" not in str(name).lower()
1589
+ ]
1590
+ return rewritten
1591
+
1592
+
1593
+ def _is_auditable_delivery_owned_output(item: Any) -> bool:
1594
+ if not isinstance(item, dict):
1595
+ return False
1596
+ path = str(item.get("path") or "").lower()
1597
+ command = str(item.get("command") or "").lower()
1598
+ return (
1599
+ "finalize" in path
1600
+ or path.startswith("output/delivery/")
1601
+ or "briefloop finalize" in command
1602
+ or "--stage finalize" in command
1603
+ or "state finalize-complete" in command
1604
+ )
1605
+
1606
+
1607
+ def render_handoff_cli(handoff: AgentHandoff) -> str:
1608
+ lines = [
1609
+ "=" * 60,
1610
+ f" Runtime: {handoff.runtime}",
1611
+ f" Recipe: {handoff.recipe}",
1612
+ f" Workspace: {handoff.workspace}",
1613
+ f" Doctor: {handoff.doctor_status}",
1614
+ "=" * 60,
1615
+ "",
1616
+ handoff.next_steps,
1617
+ "",
1618
+ ]
1619
+ if handoff.notes:
1620
+ lines.append("Notes:")
1621
+ for n in handoff.notes:
1622
+ lines.append(f" - {n}")
1623
+ lines.append("")
1624
+ if handoff.legacy_runtime_alias:
1625
+ lines.append(f"Legacy runtime alias: {handoff.legacy_runtime_alias}")
1626
+ lines.append("")
1627
+ if handoff.assessment_target_manifest:
1628
+ lines.append(f"Assessment target: {handoff.assessment_target_manifest.get('assessment_target')}")
1629
+ lines.append("")
1630
+ return "\n".join(lines)
1631
+
1632
+
1633
+ def write_handoff_artifacts(handoff: AgentHandoff, workspace: Path) -> tuple[Path, Path]:
1634
+ intermediate = workspace / "output" / "intermediate"
1635
+ intermediate.mkdir(parents=True, exist_ok=True)
1636
+
1637
+ md_path = intermediate / "agent_handoff.md"
1638
+ md_content = [
1639
+ "# Agent Handoff",
1640
+ "",
1641
+ f"- Runtime: {handoff.runtime}",
1642
+ f"- Recipe: {handoff.recipe}",
1643
+ f"- Workspace: {handoff.workspace}",
1644
+ f"- Repository: {handoff.repo_workdir}",
1645
+ f"- Venv activate: {handoff.venv_activate}",
1646
+ f"- Doctor: {handoff.doctor_status}",
1647
+ "",
1648
+ "## Next Steps",
1649
+ "",
1650
+ handoff.next_steps,
1651
+ "",
1652
+ ]
1653
+ if handoff.legacy_runtime_alias:
1654
+ md_content.extend([
1655
+ f"- Legacy runtime alias: `{handoff.legacy_runtime_alias}`",
1656
+ "",
1657
+ ])
1658
+ if handoff.runtime_capabilities:
1659
+ md_content.extend([
1660
+ "## Runtime Capabilities",
1661
+ "",
1662
+ ])
1663
+ for key, value in handoff.runtime_capabilities.items():
1664
+ rendered = "null" if value is None else str(value)
1665
+ md_content.append(f"- `{key}`: `{rendered}`")
1666
+ md_content.append("")
1667
+ _append_artifact_ownership_markdown(md_content, handoff.artifact_ownership)
1668
+ if handoff.assessment_target_manifest:
1669
+ md_content.extend([
1670
+ "## Assessment Target",
1671
+ "",
1672
+ f"- Target: `{handoff.assessment_target_manifest.get('assessment_target')}`",
1673
+ f"- Semantics: `{handoff.assessment_target_manifest.get('target_status_semantics')}`",
1674
+ f"- Target artifact: `{handoff.assessment_target_manifest.get('target_artifact')}`",
1675
+ f"- Timing: `{handoff.assessment_target_manifest.get('timing_semantics')}`",
1676
+ "- Excluded claim scope:",
1677
+ ])
1678
+ excluded = handoff.assessment_target_manifest.get("excluded_claim_scope") or []
1679
+ if excluded:
1680
+ for item in excluded:
1681
+ md_content.append(f" - `{item}`")
1682
+ else:
1683
+ md_content.append(" - none")
1684
+ forbidden = handoff.assessment_target_manifest.get("forbidden_downstream_actions") or []
1685
+ if forbidden:
1686
+ md_content.extend(["", "- Forbidden downstream actions for this target:"])
1687
+ for item in forbidden:
1688
+ md_content.append(f" - `{item}`")
1689
+ md_content.append("")
1690
+ if handoff.sourcehub_projection and handoff.sourcehub_projection.get("status") != "not_available":
1691
+ projection = handoff.sourcehub_projection
1692
+ runtime_web = (
1693
+ projection.get("runtime_web_search")
1694
+ if isinstance(projection.get("runtime_web_search"), dict)
1695
+ else {}
1696
+ )
1697
+ search_tasks = (
1698
+ runtime_web.get("search_tasks")
1699
+ if isinstance(runtime_web.get("search_tasks"), list)
1700
+ else []
1701
+ )
1702
+ md_content.extend([
1703
+ "## SourceHub Lite Projection",
1704
+ "",
1705
+ f"- Status: `{projection.get('status')}`",
1706
+ f"- Source config: `{projection.get('source_config_path') or 'sources.yaml'}`",
1707
+ f"- Boundary: `{projection.get('boundary')}`",
1708
+ f"- Runtime effect: `{projection.get('runtime_effect')}`",
1709
+ f"- SourceHub local files: `{projection.get('sourcehub_file_count', 0)}`",
1710
+ f"- RSS feeds: `{projection.get('rss_feed_count', 0)}`",
1711
+ f"- Runtime web-search mode: `{runtime_web.get('mode') or 'unknown'}`",
1712
+ f"- Runtime web-search tasks: `{runtime_web.get('task_count', 0)}`",
1713
+ "",
1714
+ "Runtime web-search handoff tasks:",
1715
+ ])
1716
+ if search_tasks:
1717
+ for task in search_tasks:
1718
+ if not isinstance(task, dict):
1719
+ continue
1720
+ domains = ", ".join(str(item) for item in (task.get("domains") or [])) or "none"
1721
+ md_content.append(
1722
+ f"- `{task.get('query')}`: max_results=`{task.get('max_results')}`, "
1723
+ f"domains=`{domains}`"
1724
+ )
1725
+ else:
1726
+ md_content.append("- none")
1727
+ md_content.extend([
1728
+ "",
1729
+ "This projection is source setup / handoff only. Runtime web-search",
1730
+ "tasks must be executed by the runtime tool and materialized into",
1731
+ "`input/sources/` before source-discovery completion. Search summaries",
1732
+ "and source candidates are not source evidence.",
1733
+ "",
1734
+ ])
1735
+ if handoff.policy_profile_projection and handoff.policy_profile_projection.get("status") != "not_available":
1736
+ projection = handoff.policy_profile_projection
1737
+ profile = projection.get("profile") if isinstance(projection.get("profile"), dict) else {}
1738
+ md_content.extend([
1739
+ "## Policy Profile Projection",
1740
+ "",
1741
+ f"- Status: `{projection.get('status')}`",
1742
+ f"- Resolved policy profile: `{projection.get('resolved_policy_profile') or projection.get('policy_profile') or 'unknown'}`",
1743
+ f"- Source: `{projection.get('source') or 'unknown'}`",
1744
+ f"- Boundary: `{projection.get('boundary')}`",
1745
+ f"- Runtime effect: `{projection.get('runtime_effect')}`",
1746
+ f"- Industry: `{profile.get('industry') or 'unknown'}`",
1747
+ "",
1748
+ "This projection is product metadata only. It does not adapt gates,",
1749
+ "act as source evidence, judge compliance/truth, authorize release,",
1750
+ "or bypass the Claim Ledger / quality gates / human delivery approval spine.",
1751
+ "",
1752
+ ])
1753
+ if handoff.report_template_projection and handoff.report_template_projection.get("status") != "not_available":
1754
+ projection = handoff.report_template_projection
1755
+ section_order = projection.get("section_order") if isinstance(projection.get("section_order"), list) else []
1756
+ md_content.extend([
1757
+ "## Report Template Projection",
1758
+ "",
1759
+ f"- Status: `{projection.get('status')}`",
1760
+ f"- Template: `{projection.get('template_id') or 'unknown'}`",
1761
+ f"- Report type: `{projection.get('report_type') or 'unknown'}`",
1762
+ f"- Boundary: `{projection.get('boundary')}`",
1763
+ f"- Runtime effect: `{projection.get('runtime_effect')}`",
1764
+ "- Section order:",
1765
+ ])
1766
+ if section_order:
1767
+ for section in section_order:
1768
+ md_content.append(f" - `{section}`")
1769
+ else:
1770
+ md_content.append(" - none")
1771
+ md_content.extend([
1772
+ "",
1773
+ "This projection is product section-order metadata only. It does not render",
1774
+ "templates, rewrite content, bypass gates, deliver reports, or authorize publication.",
1775
+ "",
1776
+ ])
1777
+ if (
1778
+ handoff.report_template_conformance_projection
1779
+ and handoff.report_template_conformance_projection.get("status") not in {"not_available", "no_targets"}
1780
+ ):
1781
+ projection = handoff.report_template_conformance_projection
1782
+ counts = projection.get("summary_counts") if isinstance(projection.get("summary_counts"), dict) else {}
1783
+ targets = projection.get("targets") if isinstance(projection.get("targets"), list) else []
1784
+ md_content.extend([
1785
+ "## Report Template Conformance Projection",
1786
+ "",
1787
+ f"- Status: `{projection.get('status')}`",
1788
+ f"- Boundary: `{projection.get('boundary')}`",
1789
+ f"- Runtime effect: `{projection.get('runtime_effect')}`",
1790
+ f"- Present targets: `{counts.get('present_target_count', 0)}`",
1791
+ f"- Warning targets: `{counts.get('warning_target_count', 0)}`",
1792
+ f"- Missing sections: `{counts.get('missing_section_count', 0)}`",
1793
+ f"- Out-of-order sections: `{counts.get('out_of_order_section_count', 0)}`",
1794
+ f"- Extra section headings: `{counts.get('extra_heading_count', 0)}`",
1795
+ f"- Reader contract warnings: `{counts.get('reader_block_warning_count', 0)}`",
1796
+ "",
1797
+ "Target diagnostics:",
1798
+ ])
1799
+ for target in targets:
1800
+ if not isinstance(target, dict) or target.get("status") == "missing":
1801
+ continue
1802
+ reader_warnings = [
1803
+ str(item.get("type"))
1804
+ for item in (target.get("reader_block_warnings") or [])
1805
+ if isinstance(item, dict) and item.get("type")
1806
+ ]
1807
+ md_content.append(
1808
+ f"- `{target.get('target_artifact')}`: status=`{target.get('status')}`, "
1809
+ f"missing=`{', '.join(str(item) for item in (target.get('missing_sections') or [])) or 'none'}`, "
1810
+ f"out_of_order=`{', '.join(str(item) for item in (target.get('out_of_order_sections') or [])) or 'none'}`, "
1811
+ f"extra=`{', '.join(str(item) for item in (target.get('extra_headings') or [])) or 'none'}`, "
1812
+ f"reader_contract=`{', '.join(reader_warnings) or 'none'}`"
1813
+ )
1814
+ md_content.extend([
1815
+ "",
1816
+ "This projection is structure guidance only. It does not render templates,",
1817
+ "rewrite content, bypass gates, deliver reports, or authorize publication.",
1818
+ "",
1819
+ ])
1820
+ if (
1821
+ handoff.report_template_render_plan_projection
1822
+ and handoff.report_template_render_plan_projection.get("status") != "not_available"
1823
+ ):
1824
+ projection = handoff.report_template_render_plan_projection
1825
+ counts = projection.get("summary_counts") if isinstance(projection.get("summary_counts"), dict) else {}
1826
+ source_candidates = (
1827
+ projection.get("source_artifact_candidates")
1828
+ if isinstance(projection.get("source_artifact_candidates"), list)
1829
+ else []
1830
+ )
1831
+ unresolved_sections = (
1832
+ projection.get("unresolved_sections")
1833
+ if isinstance(projection.get("unresolved_sections"), list)
1834
+ else []
1835
+ )
1836
+ planned_targets = (
1837
+ projection.get("planned_delivery_targets")
1838
+ if isinstance(projection.get("planned_delivery_targets"), list)
1839
+ else []
1840
+ )
1841
+ md_content.extend([
1842
+ "## Report Template Render Plan Projection",
1843
+ "",
1844
+ f"- Status: `{projection.get('status')}`",
1845
+ f"- Template: `{projection.get('template_id') or 'unknown'}`",
1846
+ f"- Selected source artifact: `{projection.get('selected_source_artifact') or 'none'}`",
1847
+ f"- Boundary: `{projection.get('boundary')}`",
1848
+ f"- Runtime effect: `{projection.get('runtime_effect')}`",
1849
+ f"- Sections: `{counts.get('section_count', 0)}`",
1850
+ f"- Unresolved sections: `{counts.get('unresolved_section_count', 0)}`",
1851
+ f"- Planned delivery targets: `{counts.get('planned_delivery_target_count', 0)}`",
1852
+ "",
1853
+ "Source artifact candidates:",
1854
+ ])
1855
+ for item in source_candidates:
1856
+ if not isinstance(item, dict):
1857
+ continue
1858
+ md_content.append(
1859
+ f"- `{item.get('artifact')}`: role=`{item.get('role')}`, "
1860
+ f"status=`{item.get('status')}`, selected=`{item.get('selected')}`"
1861
+ )
1862
+ md_content.append("")
1863
+ md_content.append("Unresolved section diagnostics:")
1864
+ if unresolved_sections:
1865
+ for item in unresolved_sections[:12]:
1866
+ if isinstance(item, dict):
1867
+ md_content.append(f"- `{item.get('section')}`: status=`{item.get('status')}`")
1868
+ else:
1869
+ md_content.append("- none")
1870
+ md_content.append("")
1871
+ md_content.append("Planned targets:")
1872
+ if planned_targets:
1873
+ for item in planned_targets:
1874
+ if isinstance(item, dict):
1875
+ md_content.append(f"- `{item.get('artifact')}` ({item.get('kind')})")
1876
+ else:
1877
+ md_content.append("- none")
1878
+ md_content.extend([
1879
+ "",
1880
+ "This projection is render planning diagnostics only. It does not render",
1881
+ "templates, rewrite content, call finalize, bypass gates, deliver reports,",
1882
+ "or authorize publication.",
1883
+ "",
1884
+ ])
1885
+ md_content.extend([
1886
+ "## Contract References",
1887
+ "",
1888
+ ])
1889
+ for label, rel_path in handoff.contract_references.items():
1890
+ md_content.append(f"- `{label}`: `{rel_path}`")
1891
+ md_content.extend([
1892
+ "",
1893
+ "## Runtime State Files",
1894
+ "",
1895
+ ])
1896
+ for label, rel_path in handoff.runtime_state_files.items():
1897
+ md_content.append(f"- `{label}`: `{rel_path}`")
1898
+ md_content.extend([
1899
+ "",
1900
+ "## Audience Memory Files",
1901
+ "",
1902
+ ])
1903
+ for label, rel_path in handoff.audience_memory_files.items():
1904
+ md_content.append(f"- `{label}`: `{rel_path}`")
1905
+ md_content.extend([
1906
+ "",
1907
+ "## Improvement Memory Files",
1908
+ "",
1909
+ ])
1910
+ for label, rel_path in handoff.improvement_memory_files.items():
1911
+ md_content.append(f"- `{label}`: `{rel_path}`")
1912
+ md_content.extend([
1913
+ "",
1914
+ "## Control Switchboard Files",
1915
+ "",
1916
+ ])
1917
+ for label, rel_path in handoff.control_switchboard_files.items():
1918
+ md_content.append(f"- `{label}`: `{rel_path}`")
1919
+ md_content.extend([
1920
+ "",
1921
+ "## Feedback State Files",
1922
+ "",
1923
+ ])
1924
+ for label, rel_path in handoff.feedback_state_files.items():
1925
+ md_content.append(f"- `{label}`: `{rel_path}`")
1926
+ md_content.extend([
1927
+ "",
1928
+ "## Quality Gate State Files",
1929
+ "",
1930
+ ])
1931
+ for label, rel_path in handoff.quality_gate_state_files.items():
1932
+ md_content.append(f"- `{label}`: `{rel_path}`")
1933
+ md_content.extend([
1934
+ "",
1935
+ "## Provenance State Files",
1936
+ "",
1937
+ ])
1938
+ for label, rel_path in handoff.provenance_state_files.items():
1939
+ md_content.append(f"- `{label}`: `{rel_path}`")
1940
+ md_content.extend([
1941
+ "",
1942
+ "## Stage Completion Protocol",
1943
+ "",
1944
+ f"- Schema: `{handoff.stage_completion_protocol.get('schema_version', '')}`",
1945
+ f"- Status: `{handoff.stage_completion_protocol.get('status', '')}`",
1946
+ "",
1947
+ "### Rules",
1948
+ "",
1949
+ ])
1950
+ for rule in handoff.stage_completion_protocol.get("rules") or []:
1951
+ md_content.append(f"- {rule}")
1952
+ md_content.extend([
1953
+ "",
1954
+ "### Per-Stage Artifact Proof",
1955
+ "",
1956
+ ])
1957
+ for stage in handoff.stage_completion_protocol.get("stages") or []:
1958
+ md_content.extend([
1959
+ f"#### `{stage.get('stage_id')}`",
1960
+ "",
1961
+ f"- Owner: `{stage.get('owner')}`",
1962
+ f"- Context inputs: {', '.join(stage.get('context_inputs') or []) or 'none'}",
1963
+ f"- Completion condition: {stage.get('completion_condition')}",
1964
+ f"- Stage completion transaction: `briefloop state stage-complete --stage {stage.get('stage_id')} --workspace <workspace> --reason \"<reason>\"`",
1965
+ "- Required input artifacts:",
1966
+ ])
1967
+ inputs = stage.get("required_input_artifacts") or []
1968
+ if inputs:
1969
+ for item in inputs:
1970
+ md_content.append(f" - `{item.get('artifact_id')}` at `{item.get('path')}`")
1971
+ else:
1972
+ md_content.append(" - none")
1973
+ freeze_inputs = stage.get("freeze_input_artifacts") or []
1974
+ if freeze_inputs:
1975
+ md_content.append("- Freeze input artifacts:")
1976
+ for item in freeze_inputs:
1977
+ md_content.append(f" - `{item.get('artifact_id')}` at `{item.get('path')}`")
1978
+ pre_transactions = stage.get("pre_completion_transactions") or []
1979
+ if pre_transactions:
1980
+ md_content.append("- Pre-completion transactions:")
1981
+ for transaction in pre_transactions:
1982
+ reads = _protocol_paths(transaction.get("reads") or [])
1983
+ writes = _protocol_paths(transaction.get("writes") or [])
1984
+ md_content.append(f" - `{transaction.get('transaction')}`: `{transaction.get('command')}`")
1985
+ md_content.append(f" - Reads: {reads}")
1986
+ md_content.append(f" - Writes: {writes}")
1987
+ contract_notes = stage.get("contract_notes") or []
1988
+ if contract_notes:
1989
+ md_content.append("- Contract notes:")
1990
+ for note in contract_notes:
1991
+ md_content.append(f" - {note}")
1992
+ chunk_contract = stage.get("parallel_chunk_contract")
1993
+ if isinstance(chunk_contract, dict):
1994
+ md_content.append("- Optional chunk parallelism:")
1995
+ md_content.append(
1996
+ f" - Status: `{chunk_contract.get('status')}`; scratch only: `{chunk_contract.get('scratch_only')}`"
1997
+ )
1998
+ md_content.append(f" - Join: {chunk_contract.get('join_requirement')}")
1999
+ forbidden = chunk_contract.get("forbidden") or []
2000
+ if forbidden:
2001
+ md_content.append(" - Forbidden:")
2002
+ for item in forbidden:
2003
+ md_content.append(f" - {item}")
2004
+ topology_satisfaction = stage.get("topology_satisfaction") or {}
2005
+ if topology_satisfaction:
2006
+ md_content.append("- Topology satisfaction:")
2007
+ for topology in _ordered_role_topologies():
2008
+ rule = topology_satisfaction.get(topology)
2009
+ if not isinstance(rule, dict):
2010
+ continue
2011
+ artifacts = _protocol_paths(rule.get("required_artifacts") or [])
2012
+ md_content.append(
2013
+ f" - `{topology}`: satisfied by `{rule.get('satisfied_by')}` when {artifacts} exist"
2014
+ )
2015
+ forbidden = rule.get("forbidden_replay_actions") or []
2016
+ if forbidden:
2017
+ md_content.append(
2018
+ " - Do not replay: " + ", ".join(str(item) for item in forbidden)
2019
+ )
2020
+ md_content.append(
2021
+ "- Independent completion topologies: "
2022
+ + (", ".join(stage.get("independent_completion_topologies") or []) or "none")
2023
+ )
2024
+ md_content.append("- Required output artifacts for independent completion:")
2025
+ else:
2026
+ md_content.append("- Required output artifacts:")
2027
+ outputs = stage.get("required_output_artifacts") or []
2028
+ if outputs:
2029
+ for item in outputs:
2030
+ md_content.append(f" - `{item.get('artifact_id')}` at `{item.get('path')}`")
2031
+ else:
2032
+ md_content.append(" - none")
2033
+ md_content.append("- Forbidden actions:")
2034
+ for action in stage.get("forbidden_actions") or []:
2035
+ md_content.append(f" - {action}")
2036
+ md_content.append("")
2037
+ md_content.extend([
2038
+ "",
2039
+ "## Prompt",
2040
+ "",
2041
+ "```text",
2042
+ handoff.prompt,
2043
+ "```",
2044
+ "",
2045
+ "## Expected Artifacts",
2046
+ "",
2047
+ ])
2048
+ for a in handoff.expected_artifacts:
2049
+ md_content.append(f"- `{a}`")
2050
+ md_content.append("")
2051
+ if handoff.notes:
2052
+ md_content.append("## Notes")
2053
+ md_content.append("")
2054
+ for n in handoff.notes:
2055
+ md_content.append(f"- {n}")
2056
+ md_content.append("")
2057
+
2058
+ md_path.write_text("\n".join(md_content), encoding="utf-8")
2059
+
2060
+ json_path = intermediate / "agent_handoff.json"
2061
+ json_path.write_text(
2062
+ json.dumps(handoff.to_dict(), ensure_ascii=False, indent=2),
2063
+ encoding="utf-8",
2064
+ )
2065
+
2066
+ return md_path, json_path
2067
+
2068
+
2069
+ def write_handoff_and_state(
2070
+ *,
2071
+ handoff: AgentHandoff,
2072
+ workspace: Path,
2073
+ repo_workdir: Path,
2074
+ prefix: str,
2075
+ ) -> tuple[Path, Path] | None:
2076
+ """Initialize runtime control files and write handoff artifacts."""
2077
+ try:
2078
+ state = initialize_runtime_state(
2079
+ workspace=workspace,
2080
+ runtime=handoff.runtime,
2081
+ repo_workdir=repo_workdir,
2082
+ actor="cli",
2083
+ recipe=handoff.recipe,
2084
+ )
2085
+ snapshot = create_audience_profile_snapshot(
2086
+ workspace=workspace,
2087
+ run_id=str((state.get("manifest") or {}).get("run_id") or ""),
2088
+ profile_data=profile_data_from_workspace_config(workspace),
2089
+ )
2090
+ if snapshot.created:
2091
+ append_event(
2092
+ workspace=workspace,
2093
+ run_id=snapshot.run_id,
2094
+ event_type="audience_profile_snapshot_created",
2095
+ actor="cli",
2096
+ reason="Audience profile snapshot created for the current run.",
2097
+ metadata={
2098
+ "audience_profile": "audience_profile.md",
2099
+ "audience_profile_snapshot": snapshot.relative_path,
2100
+ "profile_created": snapshot.profile_created,
2101
+ "profile_missing": snapshot.profile_missing,
2102
+ "source_sha256": snapshot.source_sha256,
2103
+ "snapshot_sha256": snapshot.snapshot_sha256,
2104
+ },
2105
+ )
2106
+ if snapshot.profile_missing:
2107
+ handoff.notes.append(
2108
+ "Audience profile was missing; a default audience_profile.md and frozen snapshot were created for this run."
2109
+ )
2110
+ if handoff.recipe != RUNTIME_RECIPE_FAST_RERUN:
2111
+ _record_doctor_state(
2112
+ handoff=handoff,
2113
+ workspace=workspace,
2114
+ repo_workdir=repo_workdir,
2115
+ )
2116
+ build_control_switchboard(
2117
+ workspace=workspace,
2118
+ repo_workdir=repo_workdir,
2119
+ actor="cli",
2120
+ )
2121
+ improvement_snapshot = freeze_improvement_memory_for_run(
2122
+ workspace=workspace,
2123
+ run_id=str((state.get("manifest") or {}).get("run_id") or ""),
2124
+ )
2125
+ _apply_improvement_memory_handoff(handoff, improvement_snapshot)
2126
+ if improvement_snapshot.snapshot_created_or_changed:
2127
+ append_event(
2128
+ workspace=workspace,
2129
+ run_id=str((state.get("manifest") or {}).get("run_id") or ""),
2130
+ event_type="improvement_memory_snapshot_created",
2131
+ actor="cli",
2132
+ reason="Improvement memory snapshot created for the current run.",
2133
+ metadata={
2134
+ "ledger_sha256": improvement_snapshot.manifest_improvement["ledger_sha256"],
2135
+ "memory_sha256": improvement_snapshot.manifest_improvement["memory_sha256"],
2136
+ "snapshot_path": improvement_snapshot.snapshot_path,
2137
+ "snapshot_sha256": improvement_snapshot.snapshot_sha256,
2138
+ "materialized_entry_ids": improvement_snapshot.manifest_improvement["materialized_entry_ids"],
2139
+ },
2140
+ )
2141
+ md_path, json_path = write_handoff_artifacts(handoff, workspace)
2142
+ record_handoff_written(
2143
+ workspace=workspace,
2144
+ handoff_markdown=md_path,
2145
+ handoff_json=json_path,
2146
+ actor="cli",
2147
+ )
2148
+ check_runtime_state(
2149
+ workspace=workspace,
2150
+ repo_workdir=repo_workdir,
2151
+ actor="cli",
2152
+ )
2153
+ return md_path, json_path
2154
+ except RuntimeStateError as exc:
2155
+ print(f"{prefix} {exc}")
2156
+ return None
2157
+ except ControlSwitchboardError as exc:
2158
+ print(f"{prefix} {exc}")
2159
+ return None
2160
+ except ImprovementLedgerError as exc:
2161
+ print(f"{prefix} {exc}")
2162
+ return None
2163
+
2164
+
2165
+ def _apply_improvement_memory_handoff(handoff: AgentHandoff, snapshot_result: Any) -> None:
2166
+ if not snapshot_result.snapshot_path:
2167
+ handoff.improvement_memory_files = {}
2168
+ return
2169
+
2170
+ handoff.improvement_memory_files = {
2171
+ "improvement_memory_snapshot": snapshot_result.snapshot_path,
2172
+ }
2173
+ rules = (
2174
+ "Read frozen improvement memory snapshot for this run:\n"
2175
+ f"- {snapshot_result.snapshot_path}\n"
2176
+ "Use it only as taste/audience guidance. It is not evidence, not source material, "
2177
+ "not Claim Ledger input, and not a repair instruction. It must not alter material "
2178
+ "facts, claims, citations, or source support. Mid-run ledger or memory edits apply "
2179
+ "to later runs only."
2180
+ )
2181
+ handoff.prompt = f"{handoff.prompt}\n\n{rules}"
2182
+ handoff.notes.append(
2183
+ "Improvement memory is frozen per run: read the improvement memory snapshot only when present; do not read live improvement memory projections as runtime guidance."
2184
+ )
2185
+
2186
+
2187
+ def _record_doctor_state(
2188
+ *,
2189
+ handoff: AgentHandoff,
2190
+ workspace: Path,
2191
+ repo_workdir: Path,
2192
+ ) -> None:
2193
+ if handoff.doctor_status == "passed":
2194
+ complete_stage_transaction(
2195
+ workspace=workspace,
2196
+ repo_workdir=repo_workdir,
2197
+ stage_id="doctor",
2198
+ reason="Doctor passed during runtime handoff launch.",
2199
+ actor="cli",
2200
+ )
2201
+ elif handoff.doctor_status == "failed":
2202
+ record_decision(
2203
+ workspace=workspace,
2204
+ repo_workdir=repo_workdir,
2205
+ stage_id="doctor",
2206
+ decision="block_run",
2207
+ reason="Doctor failed during runtime handoff launch.",
2208
+ actor="cli",
2209
+ )