multi-forge 0.5.0__tar.gz → 0.6.0__tar.gz

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 (368) hide show
  1. {multi_forge-0.5.0 → multi_forge-0.6.0}/PKG-INFO +16 -11
  2. {multi_forge-0.5.0 → multi_forge-0.6.0}/README.md +15 -10
  3. {multi_forge-0.5.0 → multi_forge-0.6.0}/pyproject.toml +1 -1
  4. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/activity.py +10 -4
  5. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/extensions.py +69 -8
  6. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/_group.py +5 -4
  7. multi_forge-0.6.0/src/forge/cli/hooks/codex_patch.py +129 -0
  8. multi_forge-0.6.0/src/forge/cli/hooks/codex_policy.py +197 -0
  9. multi_forge-0.6.0/src/forge/cli/hooks/codex_transfer.py +113 -0
  10. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/commands.py +223 -19
  11. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/direct_commands.py +113 -1
  12. multi_forge-0.6.0/src/forge/cli/hooks/policy.py +315 -0
  13. multi_forge-0.6.0/src/forge/cli/hooks/protocols.py +57 -0
  14. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/policy.py +247 -7
  15. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/runtime.py +62 -1
  16. multi_forge-0.6.0/src/forge/cli/session_codex.py +489 -0
  17. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_fork.py +29 -3
  18. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_lifecycle.py +87 -175
  19. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_manage.py +30 -3
  20. multi_forge-0.6.0/src/forge/cli/session_model_pin.py +196 -0
  21. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/codex.py +47 -22
  22. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/codex_stream.py +23 -6
  23. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/types.py +5 -0
  24. multi_forge-0.6.0/src/forge/core/ops/codex_bridge.py +390 -0
  25. multi_forge-0.6.0/src/forge/core/ops/codex_enrollment.py +307 -0
  26. multi_forge-0.6.0/src/forge/core/ops/codex_interactive.py +560 -0
  27. multi_forge-0.6.0/src/forge/core/ops/codex_session.py +579 -0
  28. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/gc.py +18 -0
  29. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/usage_summary.py +45 -11
  30. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/runtime/codex_preflight.py +61 -29
  31. multi_forge-0.6.0/src/forge/core/runtime/codex_rollouts.py +187 -0
  32. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/runtime/registry.py +61 -36
  33. multi_forge-0.6.0/src/forge/install/codex_hooks.py +515 -0
  34. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/installer.py +126 -0
  35. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/models.py +33 -0
  36. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/base.py +15 -8
  37. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/engine.py +115 -63
  38. multi_forge-0.6.0/src/forge/policy/semantic/plan_check.py +561 -0
  39. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/promotion.py +1 -1
  40. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/supervisor.py +4 -4
  41. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/types.py +8 -5
  42. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/extractor.py +1 -1
  43. multi_forge-0.6.0/src/forge/session/codex_handoff.py +253 -0
  44. multi_forge-0.6.0/src/forge/session/codex_invoke.py +81 -0
  45. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/exceptions.py +13 -0
  46. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/manager.py +17 -0
  47. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/models.py +51 -1
  48. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/overrides.py +9 -0
  49. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/transfer.py +1 -1
  50. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/2-extension.md +43 -0
  51. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist.md +13 -11
  52. multi_forge-0.5.0/src/forge/cli/hooks/policy.py +0 -212
  53. multi_forge-0.5.0/src/forge/cli/hooks/protocols.py +0 -53
  54. multi_forge-0.5.0/src/forge/core/ops/codex_bridge.py +0 -244
  55. {multi_forge-0.5.0 → multi_forge-0.6.0}/.gitignore +0 -0
  56. {multi_forge-0.5.0 → multi_forge-0.6.0}/LICENSE +0 -0
  57. {multi_forge-0.5.0 → multi_forge-0.6.0}/NOTICE +0 -0
  58. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/agents/.gitkeep +0 -0
  59. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/commands/.gitkeep +0 -0
  60. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/__init__.py +0 -0
  61. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/__init__.py +0 -0
  62. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/adapters/__init__.py +0 -0
  63. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/adapters/litellm.py +0 -0
  64. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/creation.py +0 -0
  65. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/backend/registry.py +0 -0
  66. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/__init__.py +0 -0
  67. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/auth.py +0 -0
  68. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/backend.py +0 -0
  69. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/claude.py +0 -0
  70. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/config_cmd.py +0 -0
  71. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/editor.py +0 -0
  72. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/gc.py +0 -0
  73. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/guards.py +0 -0
  74. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/__init__.py +0 -0
  75. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/_helpers.py +0 -0
  76. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/install.py +0 -0
  77. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/read_hygiene.py +0 -0
  78. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/hooks/verification.py +0 -0
  79. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/launch_confirmation.py +0 -0
  80. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/logs.py +0 -0
  81. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/main.py +0 -0
  82. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/memory.py +0 -0
  83. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/memory_report.py +0 -0
  84. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/memory_writer.py +0 -0
  85. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/output.py +0 -0
  86. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/proxy.py +0 -0
  87. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/proxy_audit.py +0 -0
  88. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/proxy_costs.py +0 -0
  89. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/search.py +0 -0
  90. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session.py +0 -0
  91. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/session_addendum.py +0 -0
  92. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/status_line.py +0 -0
  93. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/__init__.py +0 -0
  94. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/context.py +0 -0
  95. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/names.py +0 -0
  96. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/palette.py +0 -0
  97. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/registry.py +0 -0
  98. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/statusline/throttle.py +0 -0
  99. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/transfer.py +0 -0
  100. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/cli/workflow.py +0 -0
  101. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/__init__.py +0 -0
  102. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/dataclass_utils.py +0 -0
  103. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/__init__.py +0 -0
  104. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/backends/__init__.py +0 -0
  105. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/backends/litellm.yaml +0 -0
  106. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/__init__.py +0 -0
  107. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/anthropic-passthrough.yaml +0 -0
  108. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-anthropic-local.yaml +0 -0
  109. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-anthropic.yaml +0 -0
  110. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini-flash-local.yaml +0 -0
  111. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini-local.yaml +0 -0
  112. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini-test.yaml +0 -0
  113. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-gemini.yaml +0 -0
  114. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-openai-codex-local.yaml +0 -0
  115. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-openai-local.yaml +0 -0
  116. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/litellm-openai.yaml +0 -0
  117. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-anthropic.yaml +0 -0
  118. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-deepseek.yaml +0 -0
  119. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-gemini-flash.yaml +0 -0
  120. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-gemini.yaml +0 -0
  121. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-glm.yaml +0 -0
  122. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-kimi.yaml +0 -0
  123. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-minimax.yaml +0 -0
  124. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-openai-codex.yaml +0 -0
  125. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-openai.yaml +0 -0
  126. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/defaults/templates/openrouter-qwen.yaml +0 -0
  127. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/loader.py +0 -0
  128. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/config/schema.py +0 -0
  129. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/__init__.py +0 -0
  130. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/__init__.py +0 -0
  131. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/capabilities.py +0 -0
  132. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/credentials_file.py +0 -0
  133. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/protocols.py +0 -0
  134. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/secrets.py +0 -0
  135. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/auth/template_secrets.py +0 -0
  136. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/__init__.py +0 -0
  137. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/model_catalog.yaml +0 -0
  138. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/system_prompt_addendums/__init__.py +0 -0
  139. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/system_prompt_addendums/gemini.md +0 -0
  140. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/data/system_prompt_addendums/openai.md +0 -0
  141. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/__init__.py +0 -0
  142. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/_lifecycle.py +0 -0
  143. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/invoker/claude.py +0 -0
  144. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/__init__.py +0 -0
  145. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/__init__.py +0 -0
  146. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/base.py +0 -0
  147. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/litellm.py +0 -0
  148. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/openai_compat.py +0 -0
  149. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/clients/openrouter.py +0 -0
  150. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/credentials.py +0 -0
  151. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/detection.py +0 -0
  152. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/errors.py +0 -0
  153. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/protocols.py +0 -0
  154. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/llm/types.py +0 -0
  155. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/logging.py +0 -0
  156. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/models/__init__.py +0 -0
  157. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/models/catalog.py +0 -0
  158. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/models/types.py +0 -0
  159. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/naming.py +0 -0
  160. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/__init__.py +0 -0
  161. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/context.py +0 -0
  162. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/proxy.py +0 -0
  163. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/resolution.py +0 -0
  164. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/session.py +0 -0
  165. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/session_context.py +0 -0
  166. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/ops/transfer.py +0 -0
  167. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/paths.py +0 -0
  168. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/process.py +0 -0
  169. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/__init__.py +0 -0
  170. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/cost_tracking.py +0 -0
  171. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/env.py +0 -0
  172. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/headless_json.py +0 -0
  173. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/proxy.py +0 -0
  174. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/routing.py +0 -0
  175. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/session_runner.py +0 -0
  176. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/structured_output.py +0 -0
  177. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/tagger.py +0 -0
  178. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/reactive/throttle.py +0 -0
  179. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/run_id.py +0 -0
  180. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/runtime/__init__.py +0 -0
  181. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/__init__.py +0 -0
  182. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/exceptions.py +0 -0
  183. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/io.py +0 -0
  184. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/lock.py +0 -0
  185. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/state/timestamps.py +0 -0
  186. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/transcript.py +0 -0
  187. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/typing_helpers.py +0 -0
  188. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/__init__.py +0 -0
  189. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/billing.py +0 -0
  190. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/correlation.py +0 -0
  191. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/emit.py +0 -0
  192. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/ledger.py +0 -0
  193. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/usage/vocabulary.py +0 -0
  194. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/workqueue/__init__.py +0 -0
  195. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/workqueue/queue.py +0 -0
  196. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/core/workqueue/types.py +0 -0
  197. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/__init__.py +0 -0
  198. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/cli.py +0 -0
  199. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/exceptions.py +0 -0
  200. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/hooks.py +0 -0
  201. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/preset.py +0 -0
  202. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/settings_merge.py +0 -0
  203. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/tracking.py +0 -0
  204. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/install/version.py +0 -0
  205. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/__init__.py +0 -0
  206. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/__init__.py +0 -0
  207. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/coding_standards.py +0 -0
  208. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/registry.py +0 -0
  209. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/deterministic/tdd.py +0 -0
  210. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/protocols.py +0 -0
  211. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/queries.py +0 -0
  212. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/__init__.py +0 -0
  213. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/semantic/verdict.py +0 -0
  214. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/store.py +0 -0
  215. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/__init__.py +0 -0
  216. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/config.py +0 -0
  217. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/handlers.py +0 -0
  218. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/team/prompts.py +0 -0
  219. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/__init__.py +0 -0
  220. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/branches.py +0 -0
  221. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/config.py +0 -0
  222. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/divergence.py +0 -0
  223. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/policy.py +0 -0
  224. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/policy/workflow/stages.py +0 -0
  225. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/__init__.py +0 -0
  226. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/audit_logger.py +0 -0
  227. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/base_client.py +0 -0
  228. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/client_adapter.py +0 -0
  229. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/client_factory.py +0 -0
  230. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/converters.py +0 -0
  231. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/cost_logger.py +0 -0
  232. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/cost_tracker.py +0 -0
  233. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/data_models.py +0 -0
  234. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/error_hints.py +0 -0
  235. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/intercept.py +0 -0
  236. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/metrics.py +0 -0
  237. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/model_spec.py +0 -0
  238. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/passthrough.py +0 -0
  239. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxies.py +0 -0
  240. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxy_identity.py +0 -0
  241. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxy_orchestrator.py +0 -0
  242. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/proxy_startup.py +0 -0
  243. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/server.py +0 -0
  244. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/proxy/utils.py +0 -0
  245. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/__init__.py +0 -0
  246. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/adversarial.py +0 -0
  247. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/consensus.py +0 -0
  248. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/engine.py +0 -0
  249. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/models.py +0 -0
  250. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/__init__.py +0 -0
  251. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview-performance.md +0 -0
  252. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview-quick.md +0 -0
  253. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview-security.md +0 -0
  254. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/codereview.md +0 -0
  255. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/docreview-quick.md +0 -0
  256. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/docreview.md +0 -0
  257. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/resources/thinkdeep.md +0 -0
  258. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/routing.py +0 -0
  259. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/review/synthesis.py +0 -0
  260. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/runtime_config.py +0 -0
  261. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/__init__.py +0 -0
  262. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/bm25_store.py +0 -0
  263. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/content_store.py +0 -0
  264. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/engine.py +0 -0
  265. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/exceptions.py +0 -0
  266. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/index_state.py +0 -0
  267. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/store.py +0 -0
  268. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/search/tokenizer.py +0 -0
  269. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/__init__.py +0 -0
  270. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/active.py +0 -0
  271. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/artifacts.py +0 -0
  272. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/__init__.py +0 -0
  273. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/cleanup.py +0 -0
  274. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/invoke.py +0 -0
  275. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/paths.py +0 -0
  276. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/claude/relocate.py +0 -0
  277. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/cleanup.py +0 -0
  278. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/config.py +0 -0
  279. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/direct_model.py +0 -0
  280. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/effective.py +0 -0
  281. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/hooks/__init__.py +0 -0
  282. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/hooks/models.py +0 -0
  283. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/hooks/session_start.py +0 -0
  284. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/identity.py +0 -0
  285. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/index.py +0 -0
  286. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/memory_inheritance.py +0 -0
  287. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/memory_writer.py +0 -0
  288. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/passport.py +0 -0
  289. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/plan_resolution.py +0 -0
  290. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/prev_sessions.py +0 -0
  291. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/project_memory.py +0 -0
  292. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/shadow_curation.py +0 -0
  293. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/store.py +0 -0
  294. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/validation.py +0 -0
  295. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/__init__.py +0 -0
  296. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/cleanup.py +0 -0
  297. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/config_copy.py +0 -0
  298. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/session/worktree/create.py +0 -0
  299. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/__init__.py +0 -0
  300. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/container.py +0 -0
  301. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/docker.py +0 -0
  302. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/forge/sidecar/secrets.py +0 -0
  303. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/analyze/SKILL.md +0 -0
  304. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/challenge/SKILL.md +0 -0
  305. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/SKILL.md +0 -0
  306. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/resources/code_consensus_evaluation.md +0 -0
  307. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/resources/consensus_evaluation.md +0 -0
  308. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/consensus/resources/synthesis.md +0 -0
  309. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/debate/SKILL.md +0 -0
  310. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/debate/resources/code_debate_evaluation.md +0 -0
  311. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/debate/resources/debate_evaluation.md +0 -0
  312. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/panel/SKILL.md +0 -0
  313. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/panel/resources/synthesis.md +0 -0
  314. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/SKILL.md +0 -0
  315. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/0-enable.md +0 -0
  316. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/1-preflight.md +0 -0
  317. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/10-resume.md +0 -0
  318. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/11-config.md +0 -0
  319. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/12-search.md +0 -0
  320. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/13-policy.md +0 -0
  321. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/14-workflow.md +0 -0
  322. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/15-skills.md +0 -0
  323. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/16-memory.md +0 -0
  324. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/17-info.md +0 -0
  325. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/18-disable.md +0 -0
  326. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/19-uninstall.md +0 -0
  327. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/20-cleanup.md +0 -0
  328. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/3-authentication.md +0 -0
  329. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/4-proxy.md +0 -0
  330. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/5-session.md +0 -0
  331. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/6-hook.md +0 -0
  332. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/7-costs.md +0 -0
  333. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/8-status-line.md +0 -0
  334. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/checklist/9-direct-commands.md +0 -0
  335. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/resources/report-template.md +0 -0
  336. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/scripts/start-container.sh +0 -0
  337. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/qa/scripts/walkthrough-state.py +0 -0
  338. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/SKILL.md +0 -0
  339. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/claude-4.6.md +0 -0
  340. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/claude-4.8.md +0 -0
  341. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/gemini-3.1.md +0 -0
  342. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/gpt-5.5.md +0 -0
  343. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/references/skills-writing-guide.md +0 -0
  344. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code-anthropic.md +0 -0
  345. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code-gemini.md +0 -0
  346. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code-openai.md +0 -0
  347. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review/resources/code.md +0 -0
  348. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/SKILL.md +0 -0
  349. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs-anthropic.md +0 -0
  350. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs-gemini.md +0 -0
  351. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs-openai.md +0 -0
  352. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/review-docs/resources/docs.md +0 -0
  353. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/smoke-test/SKILL.md +0 -0
  354. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/smoke-test/scripts/smoke-test.sh +0 -0
  355. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/SKILL.md +0 -0
  356. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code-anthropic.md +0 -0
  357. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code-gemini.md +0 -0
  358. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code-openai.md +0 -0
  359. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/code.md +0 -0
  360. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs-anthropic.md +0 -0
  361. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs-gemini.md +0 -0
  362. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs-openai.md +0 -0
  363. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/understand/resources/docs.md +0 -0
  364. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/SKILL.md +0 -0
  365. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/resources/checklist.md +0 -0
  366. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/scripts/run-in-repo.sh +0 -0
  367. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/scripts/setup-test-repo.sh +0 -0
  368. {multi_forge-0.5.0 → multi_forge-0.6.0}/src/skills/walkthrough/scripts/walkthrough-state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-forge
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Multi-runtime agent toolkit: proxy routing, cost control, session management, policy enforcement, and workflow orchestration
5
5
  Project-URL: Homepage, https://github.com/hapa1i/multi-forge
6
6
  Project-URL: Repository, https://github.com/hapa1i/multi-forge
@@ -56,15 +56,18 @@ Description-Content-Type: text/markdown
56
56
  **Multi-runtime agent toolkit: proxy routing, cost control, session management, and policy enforcement for coding
57
57
  agents.**
58
58
 
59
- Forge sits between you and your coding agent (Claude Code today, Codex and Gemini next), adding persistent sessions,
60
- multi-provider model routing, cost visibility with spend caps, and autonomous verification. You run
61
- `forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen model provider, tracking
62
- state across sessions, and enforcing policies.
59
+ Forge sits between you and your coding agent (Claude Code by default, with Codex as an alternate runtime and Gemini
60
+ next), adding persistent sessions, multi-provider model routing, cost visibility with spend caps, and autonomous
61
+ verification. You run `forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen
62
+ model provider, tracking state across sessions, and enforcing policies.
63
63
 
64
64
  ```bash
65
65
  # Use Claude with session tracking (no proxy needed)
66
66
  forge session start
67
67
 
68
+ # Or run a different runtime entirely -- Codex as an alternate frontend
69
+ forge session start --runtime codex # interactive TUI; hooks/policy need a one-time Codex trust enrollment
70
+
68
71
  # Or route through different model providers (after creating proxies -- see Quick Start)
69
72
  forge session start planner --proxy openrouter-openai # GPT for planning
70
73
  forge session start --proxy openrouter-gemini # Gemini for review
@@ -223,12 +226,14 @@ Run `forge <command> --help` for details on any command.
223
226
 
224
227
  ## Documentation
225
228
 
226
- | Audience | Location | Contents |
227
- | ---------------- | ---------------------------------- | ---------------------------------------------------- |
228
- | **Users** | [docs/end-user/](docs/end-user/) | Tour, guides for sessions, proxies, policies, ... |
229
- | **Developers** | [docs/developer/](docs/developer/) | Setup, coding standards, testing guidelines |
230
- | **Architecture** | [docs/design.md](docs/design.md) | System narrative, data flow, invariants |
231
- | **Work Board** | [docs/board/](docs/board/) | Cards, checklists, change log, implementation memory |
229
+ | Audience | Location | Contents |
230
+ | ------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
231
+ | **Users** | [docs/end-user/](docs/end-user/) | Tour, guides for sessions, proxies, policies, ... |
232
+ | **Developers** | [docs/developer/](docs/developer/) | Setup, coding standards, testing guidelines |
233
+ | **Architecture** | [docs/design.md](docs/design.md) | Core system narrative, data flow, invariants |
234
+ | **Workflow design** | [docs/design_workflows.md](docs/design_workflows.md) | Policy, skills, workflow runners, memory architecture |
235
+ | **CLI reference** | [docs/cli_reference.md](docs/cli_reference.md) | Terminal and direct-command inventory |
236
+ | **Work Board** | [docs/board/](docs/board/) | Cards, checklists, change log, implementation memory |
232
237
 
233
238
  ## Contributing
234
239
 
@@ -15,15 +15,18 @@
15
15
  **Multi-runtime agent toolkit: proxy routing, cost control, session management, and policy enforcement for coding
16
16
  agents.**
17
17
 
18
- Forge sits between you and your coding agent (Claude Code today, Codex and Gemini next), adding persistent sessions,
19
- multi-provider model routing, cost visibility with spend caps, and autonomous verification. You run
20
- `forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen model provider, tracking
21
- state across sessions, and enforcing policies.
18
+ Forge sits between you and your coding agent (Claude Code by default, with Codex as an alternate runtime and Gemini
19
+ next), adding persistent sessions, multi-provider model routing, cost visibility with spend caps, and autonomous
20
+ verification. You run `forge session start` instead of `claude`, and Forge handles the rest -- routing to your chosen
21
+ model provider, tracking state across sessions, and enforcing policies.
22
22
 
23
23
  ```bash
24
24
  # Use Claude with session tracking (no proxy needed)
25
25
  forge session start
26
26
 
27
+ # Or run a different runtime entirely -- Codex as an alternate frontend
28
+ forge session start --runtime codex # interactive TUI; hooks/policy need a one-time Codex trust enrollment
29
+
27
30
  # Or route through different model providers (after creating proxies -- see Quick Start)
28
31
  forge session start planner --proxy openrouter-openai # GPT for planning
29
32
  forge session start --proxy openrouter-gemini # Gemini for review
@@ -182,12 +185,14 @@ Run `forge <command> --help` for details on any command.
182
185
 
183
186
  ## Documentation
184
187
 
185
- | Audience | Location | Contents |
186
- | ---------------- | ---------------------------------- | ---------------------------------------------------- |
187
- | **Users** | [docs/end-user/](docs/end-user/) | Tour, guides for sessions, proxies, policies, ... |
188
- | **Developers** | [docs/developer/](docs/developer/) | Setup, coding standards, testing guidelines |
189
- | **Architecture** | [docs/design.md](docs/design.md) | System narrative, data flow, invariants |
190
- | **Work Board** | [docs/board/](docs/board/) | Cards, checklists, change log, implementation memory |
188
+ | Audience | Location | Contents |
189
+ | ------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
190
+ | **Users** | [docs/end-user/](docs/end-user/) | Tour, guides for sessions, proxies, policies, ... |
191
+ | **Developers** | [docs/developer/](docs/developer/) | Setup, coding standards, testing guidelines |
192
+ | **Architecture** | [docs/design.md](docs/design.md) | Core system narrative, data flow, invariants |
193
+ | **Workflow design** | [docs/design_workflows.md](docs/design_workflows.md) | Policy, skills, workflow runners, memory architecture |
194
+ | **CLI reference** | [docs/cli_reference.md](docs/cli_reference.md) | Terminal and direct-command inventory |
195
+ | **Work Board** | [docs/board/](docs/board/) | Cards, checklists, change log, implementation memory |
191
196
 
192
197
  ## Contributing
193
198
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "multi-forge"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Multi-runtime agent toolkit: proxy routing, cost control, session management, policy enforcement, and workflow orchestration"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -113,10 +113,16 @@ def _render(summary: SessionActivitySummary, *, days: int | None) -> None:
113
113
 
114
114
  pol = summary.policy
115
115
  if pol and pol.has_content:
116
- console.print(
117
- f"\n[bold]Supervisor[/bold]: {pol.supervisor_allow} allow · "
118
- f"{pol.supervisor_warn} warn · {pol.supervisor_deny} block"
119
- )
116
+ if pol.plan_check_allow or pol.plan_check_needs_review:
117
+ console.print(
118
+ f"\n[bold]Plan check (tier-1)[/bold]: {pol.plan_check_allow} allow · "
119
+ f"{pol.plan_check_needs_review} needs review"
120
+ )
121
+ if pol.supervisor_allow or pol.supervisor_warn or pol.supervisor_deny or pol.total_warnings:
122
+ console.print(
123
+ f"\n[bold]Supervisor[/bold]: {pol.supervisor_allow} allow · "
124
+ f"{pol.supervisor_warn} warn · {pol.supervisor_deny} block"
125
+ )
120
126
  for warning in pol.recent_warnings:
121
127
  console.print(f" [yellow]•[/yellow] {warning}")
122
128
 
@@ -99,15 +99,18 @@ def _parse_modules(modules_str: str | None) -> set[InstallModule] | None:
99
99
  return {InstallModule(m.strip()) for m in modules_str.split(",")}
100
100
 
101
101
 
102
- def _count_actions(plan: InstallPlan) -> tuple[int, int]:
102
+ def _count_actions(plan: InstallPlan) -> tuple[int, int, int]:
103
103
  """Count non-skip actions in a plan.
104
104
 
105
105
  Returns:
106
- Tuple of (file_actions, settings_actions) that are not skips.
106
+ Tuple of (file_actions, settings_actions, codex_actions) that
107
+ actually change something. A codex install/update counts as an
108
+ action so a codex-only change never renders "Already up to date.".
107
109
  """
108
110
  file_actions = sum(1 for f in plan.files if f.action != "skip")
109
111
  settings_actions = sum(1 for s in plan.settings if s.action != "skip")
110
- return file_actions, settings_actions
112
+ codex_actions = 1 if plan.codex is not None and plan.codex.action in ("install", "update") else 0
113
+ return file_actions, settings_actions, codex_actions
111
114
 
112
115
 
113
116
  # Modules that are intentionally empty in the source tree (only .gitkeep).
@@ -165,8 +168,8 @@ def _print_completion_message(
165
168
  tracking: TrackingStore,
166
169
  ) -> None:
167
170
  """Print appropriate completion message based on what was done."""
168
- file_actions, settings_actions = _count_actions(plan)
169
- total_actions = file_actions + settings_actions
171
+ file_actions, settings_actions, codex_actions = _count_actions(plan)
172
+ total_actions = file_actions + settings_actions + codex_actions
170
173
 
171
174
  _warn_if_modules_have_no_files(plan, scope, project_root, tracking)
172
175
 
@@ -178,6 +181,8 @@ def _print_completion_message(
178
181
  parts.append(f"{file_actions} file{'s' if file_actions != 1 else ''}")
179
182
  if settings_actions > 0:
180
183
  parts.append(f"{settings_actions} setting{'s' if settings_actions != 1 else ''}")
184
+ if codex_actions > 0:
185
+ parts.append("Codex hooks")
181
186
  console.print(f"\n[green]Extensions enabled.[/green] ({', '.join(parts)} updated)")
182
187
 
183
188
  print_tip(
@@ -200,6 +205,30 @@ def _print_completion_message(
200
205
  required = gated[0][1].value
201
206
  print_tip(f"Additional skills available with --profile {required}: {skill_list}", console=console)
202
207
 
208
+ _print_codex_completion(plan, scope)
209
+
210
+
211
+ def _print_codex_completion(plan: InstallPlan, scope: InstallScope) -> None:
212
+ """Print the trust-ceremony guidance (or skip notice) for the codex plan.
213
+
214
+ Registration alone is inert: Codex hooks fire only after the user's
215
+ one-time interactive trust ceremony, which Forge can neither perform nor
216
+ verify -- so a fresh registration always names the ceremony explicitly.
217
+ """
218
+ codex = plan.codex
219
+ if codex is None:
220
+ return
221
+ if codex.action in ("install", "update"):
222
+ where = "in any project" if scope == InstallScope.USER else "in this project"
223
+ config = display_path(codex.config_path) if codex.config_path else "config.toml"
224
+ console.print("\n[dim]Next steps (Codex hooks):[/dim]")
225
+ console.print(f" - Forge hooks are registered in {config} but stay inert until trusted.")
226
+ console.print(f" - Run 'codex' interactively {where} and grant trust when prompted (one-time).")
227
+ elif codex.action == "conflict":
228
+ console.print(f"\n[yellow]Warning:[/yellow] Codex hook registration skipped: {codex.reason}")
229
+ elif codex.action == "unavailable":
230
+ console.print(f"\n[dim]Codex hooks skipped: {codex.reason}.[/dim]")
231
+
203
232
 
204
233
  def _validate_anchor(anchor: Path) -> None:
205
234
  """Reject anchors that point inside a ``.claude/`` directory.
@@ -324,6 +353,23 @@ def _print_plan(plan: InstallPlan, dry_run: bool = False) -> None:
324
353
 
325
354
  console.print(table)
326
355
 
356
+ if plan.codex is not None:
357
+ console.print(f"\n{prefix}[bold]Codex hooks (config.toml):[/bold]")
358
+ table = Table(show_header=True, header_style="bold", box=None)
359
+ table.add_column("ACTION", style="dim")
360
+ table.add_column("TARGET")
361
+ table.add_column("REASON", style="dim")
362
+ style = {
363
+ "install": "green",
364
+ "update": "yellow",
365
+ "skip": "dim",
366
+ "conflict": "yellow", # best-effort: degrades to skip, never blocks
367
+ "unavailable": "dim",
368
+ }.get(plan.codex.action, "")
369
+ target = display_path(plan.codex.config_path) if plan.codex.config_path else ""
370
+ table.add_row(plan.codex.action, target, plan.codex.reason or "", style=style)
371
+ console.print(table)
372
+
327
373
  if plan.has_conflicts:
328
374
  console.print(f"\n{prefix}[bold red]Conflicts detected:[/bold red]")
329
375
  for c in plan.conflicts:
@@ -467,7 +513,7 @@ def extensions() -> None:
467
513
  "--with",
468
514
  "-w",
469
515
  "with_modules",
470
- help="Add modules (comma-separated: commands,agents,skills,hooks,status-line,permissions)",
516
+ help="Add modules (comma-separated: commands,agents,skills,hooks,status-line,permissions,codex-hooks)",
471
517
  )
472
518
  @click.option(
473
519
  "--without",
@@ -672,8 +718,8 @@ def sync_cmd(scope: str | None, force: bool) -> None:
672
718
  console.print("\n[red]Sync failed due to conflicts.[/red]")
673
719
  sys.exit(1)
674
720
  else:
675
- file_actions, settings_actions = _count_actions(plan)
676
- total_actions = file_actions + settings_actions
721
+ file_actions, settings_actions, codex_actions = _count_actions(plan)
722
+ total_actions = file_actions + settings_actions + codex_actions
677
723
  if total_actions == 0:
678
724
  console.print("\n[dim]Already up to date.[/dim]")
679
725
  else:
@@ -682,8 +728,15 @@ def sync_cmd(scope: str | None, force: bool) -> None:
682
728
  parts.append(f"{file_actions} file{'s' if file_actions != 1 else ''}")
683
729
  if settings_actions > 0:
684
730
  parts.append(f"{settings_actions} setting{'s' if settings_actions != 1 else ''}")
731
+ if codex_actions > 0:
732
+ parts.append("Codex hooks")
685
733
  console.print(f"\n[green]Sync complete.[/green] ({', '.join(parts)} updated)")
686
734
 
735
+ # A synced block can carry NEW entries whose trust is not yet
736
+ # granted (per-entry trusted_hash) -- the ceremony guidance
737
+ # matters most exactly here.
738
+ _print_codex_completion(plan, install_scope)
739
+
687
740
  except NoForgeInstallationError as e:
688
741
  console.print(f"[red]Error:[/red] {e}")
689
742
  sys.exit(1)
@@ -788,6 +841,10 @@ def disable_cmd(scope: str | None, uninstall_all: bool, yes: bool) -> None:
788
841
  console.print("[bold]Settings:[/bold]")
789
842
  console.print(table)
790
843
 
844
+ if existing.codex_config_path:
845
+ console.print("\n[bold]Codex hooks:[/bold]")
846
+ console.print(f" [red]remove[/red] managed block in {display_path(existing.codex_config_path)}")
847
+
791
848
  if not yes:
792
849
  if not click.confirm("\nProceed with disable?"):
793
850
  console.print("[dim]Cancelled.[/dim]")
@@ -926,6 +983,8 @@ def status_cmd(scope: str | None, path: str | None, show_all: bool, as_json: boo
926
983
  "modules": list(inst.modules_enabled),
927
984
  "files_count": len(inst.files),
928
985
  "settings_count": len(inst.settings_entries),
986
+ "codex_config_path": inst.codex_config_path,
987
+ "codex_commands": list(inst.codex_commands),
929
988
  "installed_at": inst.installed_at,
930
989
  "updated_at": inst.updated_at,
931
990
  }
@@ -966,6 +1025,8 @@ def status_cmd(scope: str | None, path: str | None, show_all: bool, as_json: boo
966
1025
  console.print(f" Modules: {', '.join(installation.modules_enabled)}")
967
1026
  console.print(f" Files: {len(installation.files)}")
968
1027
  console.print(f" Settings: {len(installation.settings_entries)} entries")
1028
+ if installation.codex_config_path:
1029
+ console.print(f" Codex: hooks registered in {display_path(installation.codex_config_path)}")
969
1030
  console.print(f" Installed: {installation.installed_at}")
970
1031
  console.print(f" Updated: {installation.updated_at}")
971
1032
 
@@ -8,11 +8,12 @@ import click
8
8
  @click.group(name="hook", hidden=True)
9
9
  @click.pass_context
10
10
  def hooks(ctx: click.Context) -> None:
11
- """Hook handlers invoked by Claude Code.
11
+ """Hook handlers invoked by agent runtimes.
12
12
 
13
- Most subcommands are invoked automatically by Claude Code hooks
14
- configured in .claude/settings.local.json. The 'enable' and
15
- 'disable' subcommands are user-facing.
13
+ Most subcommands are invoked automatically by runtime hooks: Claude Code's
14
+ are configured in .claude/settings.local.json; Codex's (codex-policy-check)
15
+ are registered in a Codex config and require trust enrollment. The 'enable'
16
+ and 'disable' subcommands are user-facing.
16
17
  """
17
18
  from forge.core.logging import configure_debug_logging
18
19
 
@@ -0,0 +1,129 @@
1
+ """Parser for Codex's apply_patch envelope (PreToolUse ``tool_input.command``).
2
+
3
+ System boundary: the patch text arrives in a Codex hook payload (external data),
4
+ so the parser is strict but the caller fails open -- ``None`` means "not a patch
5
+ Forge can reason about", and the hook allows the action rather than guessing.
6
+ Codex's own apply_patch rejects input outside this grammar, so failing open on
7
+ malformed text converges with native behavior.
8
+
9
+ Grammar (codex-cli 0.138.0; witness fixture in ``tests/fixtures/codex/hooks/``)::
10
+
11
+ *** Begin Patch
12
+ *** Add File: <path> | *** Update File: <path> | *** Delete File: <path>
13
+ *** Move to: <path> (only immediately after an Update header)
14
+ <body lines prefixed +, -, space, or @@; blank lines are context>
15
+ *** End of File (tolerated inside add/update sections)
16
+ *** End Patch
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass, field
22
+ from typing import Literal
23
+
24
+ from forge.policy.types import extract_added_lines
25
+
26
+ PatchOpKind = Literal["add", "update", "delete"]
27
+
28
+ _BEGIN = "*** Begin Patch"
29
+ _END = "*** End Patch"
30
+ _EOF_MARKER = "*** End of File"
31
+ _MOVE_TO = "*** Move to: "
32
+ _HEADERS: tuple[tuple[str, PatchOpKind], ...] = (
33
+ ("*** Add File: ", "add"),
34
+ ("*** Update File: ", "update"),
35
+ ("*** Delete File: ", "delete"),
36
+ )
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class PatchFileOp:
41
+ """One file operation parsed from an apply_patch envelope.
42
+
43
+ ``path`` is the post-op path (the "Move to" target when present) -- policies
44
+ judge where content lands, not where it came from. The pre-move path is
45
+ recoverable from ``raw_section``.
46
+ """
47
+
48
+ kind: PatchOpKind
49
+ path: str
50
+ move_to: str | None
51
+ added_content: str # introduced lines ("" for delete)
52
+ raw_section: str # verbatim header + body (raw_diff source)
53
+
54
+
55
+ @dataclass
56
+ class _Section:
57
+ kind: PatchOpKind
58
+ path: str
59
+ move_to: str | None = None
60
+ header_lines: list[str] = field(default_factory=list)
61
+ body: list[str] = field(default_factory=list)
62
+
63
+ def finalize(self) -> PatchFileOp:
64
+ body = "\n".join(self.body)
65
+ return PatchFileOp(
66
+ kind=self.kind,
67
+ path=self.move_to or self.path,
68
+ move_to=self.move_to,
69
+ added_content="" if self.kind == "delete" else extract_added_lines(body),
70
+ raw_section="\n".join(self.header_lines + self.body),
71
+ )
72
+
73
+
74
+ def parse_apply_patch(command: str) -> list[PatchFileOp] | None:
75
+ """Parse an apply_patch envelope into per-file operations.
76
+
77
+ Returns None for anything outside the known grammar (caller fails open);
78
+ an empty envelope (Begin + End only) returns [].
79
+ """
80
+ lines = [line.rstrip("\r") for line in command.split("\n")]
81
+ while lines and not lines[0].strip():
82
+ lines.pop(0)
83
+ while lines and not lines[-1].strip():
84
+ lines.pop()
85
+ if not lines or lines[0] != _BEGIN or lines[-1] != _END or len(lines) < 2:
86
+ return None
87
+
88
+ ops: list[PatchFileOp] = []
89
+ current: _Section | None = None
90
+
91
+ for line in lines[1:-1]:
92
+ header = _match_header(line)
93
+ if header is not None:
94
+ kind, path = header
95
+ if not path:
96
+ return None
97
+ if current is not None:
98
+ ops.append(current.finalize())
99
+ current = _Section(kind=kind, path=path, header_lines=[line])
100
+ elif line.startswith(_MOVE_TO):
101
+ # Only valid immediately after an Update header (no body yet, one move max).
102
+ target = line[len(_MOVE_TO) :].strip()
103
+ if current is None or current.kind != "update" or current.body or current.move_to or not target:
104
+ return None
105
+ current.move_to = target
106
+ current.header_lines.append(line)
107
+ elif line == _EOF_MARKER:
108
+ if current is None or current.kind == "delete":
109
+ return None
110
+ current.body.append(line) # kept verbatim in raw_section; extract_added_lines ignores it
111
+ elif current is None:
112
+ return None # body line before any section header
113
+ elif current.kind == "delete":
114
+ return None # Delete sections are bodyless in the grammar
115
+ elif line == "" or line.startswith(("+", "-", " ", "@@")):
116
+ current.body.append(line)
117
+ else:
118
+ return None
119
+
120
+ if current is not None:
121
+ ops.append(current.finalize())
122
+ return ops
123
+
124
+
125
+ def _match_header(line: str) -> tuple[PatchOpKind, str] | None:
126
+ for prefix, kind in _HEADERS:
127
+ if line.startswith(prefix):
128
+ return kind, line[len(prefix) :].strip()
129
+ return None
@@ -0,0 +1,197 @@
1
+ """Codex halves of the hook seam: payload -> ActionContexts, decision -> stdout JSON.
2
+
3
+ The Codex counterparts of ``ClaudeHookAdapter``/``ClaudeHookResponder`` (policy.py),
4
+ filling the runtime-neutral protocols in ``protocols.py``. Probe-pinned facts this
5
+ module encodes (codex_frontend Phase 1, codex-cli 0.138.0):
6
+
7
+ - Codex file writes arrive as ``tool_name="apply_patch"`` with the patch envelope in
8
+ ``tool_input.command``; shell commands arrive as ``tool_name="Bash"``.
9
+ - Every Forge policy's ``applies_to`` gates on ``tool_name in ("Write", "Edit")``, so
10
+ the adapter normalizes patch operations to those names (Add File -> Write,
11
+ Update File -> Edit). The runtime truth stays in ``origin="codex"`` + ``tool_args``.
12
+ - Codex FAILS OPEN on malformed hook output, so the responder emits only
13
+ ``json.dumps`` of literal dicts -- never hand-assembled wire strings.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ from collections.abc import Callable
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from forge.cli.hooks.codex_patch import PatchFileOp, parse_apply_patch
25
+ from forge.cli.hooks.policy import format_deny_text, format_needs_review_text
26
+ from forge.policy.deterministic.base import is_under_directory
27
+ from forge.policy.types import ActionContext, CompositeDecision
28
+
29
+ _MAX_CONTENT_CHARS = 5000 # same truncation convention as ClaudeHookAdapter
30
+
31
+
32
+ class CodexHookAdapter:
33
+ """Normalize a Codex PreToolUse payload into ``ActionContext``s (origin="codex").
34
+
35
+ Only ``apply_patch`` actions are evaluable (Codex's file-write tool); anything
36
+ else -- including ``Bash`` -- yields ``[]`` so the hook command fails open, the
37
+ same posture as Claude's hook skipping non-Write/Edit tools. A multi-file patch
38
+ yields one context per non-delete file operation, in patch order; deletions are
39
+ skipped (no policy evaluates them -- there is no introduced content).
40
+ """
41
+
42
+ ORIGIN = "codex"
43
+
44
+ def build_contexts(self, payload: dict[str, Any], tool_name: str, manifest: Any) -> list[ActionContext]:
45
+ """Build per-file ``ActionContext``s from a Codex PreToolUse payload ([] if unbuildable)."""
46
+ if tool_name != "apply_patch":
47
+ return []
48
+ tool_input = payload.get("tool_input", {})
49
+ if not isinstance(tool_input, dict):
50
+ return []
51
+ command = tool_input.get("command")
52
+ if not isinstance(command, str):
53
+ return []
54
+
55
+ ops = parse_apply_patch(command)
56
+ if ops is None:
57
+ return [] # malformed patch: fail open, like Claude's unbuildable payload
58
+
59
+ # Payload cwd is where Codex resolves apply_patch paths; the hook process's
60
+ # own CWD is unpinned, so prefer the payload's.
61
+ payload_cwd = payload.get("cwd")
62
+ cwd = Path(payload_cwd) if isinstance(payload_cwd, str) and payload_cwd else Path(os.getcwd())
63
+ cwd = cwd.resolve()
64
+
65
+ return [self._context_for_op(op, cwd, manifest) for op in ops if op.kind != "delete"]
66
+
67
+ def _context_for_op(self, op: PatchFileOp, cwd: Path, manifest: Any) -> ActionContext:
68
+ # Normalize to the tool names every policy's applies_to expects.
69
+ normalized_tool = "Write" if op.kind == "add" else "Edit"
70
+
71
+ target_path = op.path
72
+ try:
73
+ p = Path(target_path)
74
+ if p.is_absolute():
75
+ target_path = str(p.relative_to(cwd))
76
+ except (ValueError, RuntimeError):
77
+ pass # Keep as-is if can't make relative
78
+
79
+ new_content: str | None = op.added_content
80
+ if new_content and len(new_content) > _MAX_CONTENT_CHARS:
81
+ new_content = new_content[:_MAX_CONTENT_CHARS] + "\n... (truncated)"
82
+
83
+ # raw_diff only for updates: an Add's full content already is new_content,
84
+ # while an update section genuinely is a diff (richer LLM-policy context).
85
+ raw_diff = op.raw_section[:_MAX_CONTENT_CHARS] if op.kind == "update" else None
86
+
87
+ return ActionContext(
88
+ origin=self.ORIGIN,
89
+ event=f"PreToolUse.{normalized_tool}",
90
+ tool_name=normalized_tool,
91
+ tool_args={
92
+ "codex_tool_name": "apply_patch",
93
+ "path": op.path,
94
+ "move_to": op.move_to,
95
+ "kind": op.kind,
96
+ },
97
+ repo_root=str(cwd),
98
+ session_name=manifest.name,
99
+ target_path=target_path,
100
+ new_content=new_content or None,
101
+ raw_diff=raw_diff,
102
+ )
103
+
104
+
105
+ class CodexHookResponder:
106
+ """Serialize a composed policy decision into Codex's PreToolUse wire contract.
107
+
108
+ Codex blocks on a strict stdout JSON ``hookSpecificOutput`` with
109
+ ``permissionDecision: "deny"`` and exits 0 (probe-pinned; the exit-2 form also
110
+ blocks but the JSON form carries the reason in-band). Codex fails OPEN on
111
+ malformed output, so every wire string is ``json.dumps`` of a literal dict.
112
+ """
113
+
114
+ BLOCK_EXIT = 0 # deny is stdout JSON, not an exit code (contrast Claude's 2)
115
+ ALLOW_EXIT = 0
116
+
117
+ def format_deny(self, result: CompositeDecision) -> str:
118
+ """Render the deny wire JSON for a single composed decision."""
119
+ return self.format_deny_multi([(None, result)])
120
+
121
+ def format_needs_review(self, result: CompositeDecision) -> str:
122
+ """Render the deny wire JSON for a single unresolved ``needs_review``."""
123
+ return self.format_needs_review_multi([(None, result)])
124
+
125
+ def format_deny_multi(self, file_results: list[tuple[str | None, CompositeDecision]]) -> str:
126
+ """Render one deny wire JSON covering every denying file of a patch."""
127
+ reason = self._join_sections(file_results, format_deny_text)
128
+ return self._deny_wire(reason)
129
+
130
+ def format_needs_review_multi(self, file_results: list[tuple[str | None, CompositeDecision]]) -> str:
131
+ """Render one deny wire JSON for the unresolved-review files of a patch."""
132
+ reason = self._join_sections(file_results, format_needs_review_text)
133
+ return self._deny_wire(reason)
134
+
135
+ def format_error_deny(self, reason: str) -> str:
136
+ """Render the deny wire JSON for a fail-closed evaluation error (no decision)."""
137
+ return self._deny_wire(reason)
138
+
139
+ def allow_feedback(self, additional_context: str) -> dict[str, Any]:
140
+ """Build the allow JSON (protocol conformance only).
141
+
142
+ Not emitted in Phase 3: PreToolUse ``additionalContext`` delivery on allow is
143
+ unprobed (only SessionStart's is confirmed), so the command allows silently.
144
+ """
145
+ return {
146
+ "hookSpecificOutput": {
147
+ "hookEventName": "PreToolUse",
148
+ "permissionDecision": "allow",
149
+ "additionalContext": additional_context,
150
+ }
151
+ }
152
+
153
+ @staticmethod
154
+ def _deny_wire(reason: str) -> str:
155
+ return json.dumps(
156
+ {
157
+ "hookSpecificOutput": {
158
+ "hookEventName": "PreToolUse",
159
+ "permissionDecision": "deny",
160
+ "permissionDecisionReason": reason,
161
+ }
162
+ }
163
+ )
164
+
165
+ @staticmethod
166
+ def _join_sections(
167
+ file_results: list[tuple[str | None, CompositeDecision]],
168
+ format_text: Callable[[CompositeDecision], str],
169
+ ) -> str:
170
+ sections = []
171
+ for path, result in file_results:
172
+ text = format_text(result)
173
+ sections.append(f"{path}:\n{text}" if path else text)
174
+ return "\n\n".join(sections)
175
+
176
+
177
+ def sort_contexts_tests_first(contexts: list[ActionContext]) -> list[ActionContext]:
178
+ """Order contexts so tests/ paths evaluate before src/ paths.
179
+
180
+ Optimistic ordering for TDD stateful evaluation: an atomic patch adding test +
181
+ implementation together passes tests-before-impl (the test file populates
182
+ ``tests_touched`` first). Uses ``is_under_directory`` -- the SAME nested-aware rule
183
+ the TDD policy's ``applies_to`` gates on -- so a nested ``pkg/tests`` / ``pkg/src``
184
+ layout is reordered too. A top-level-only prefix match would leave both nested files
185
+ in one bucket and false-deny an impl-first atomic patch. ``sorted`` is stable, so
186
+ patch order is preserved within each bucket.
187
+ """
188
+
189
+ def _key(ctx: ActionContext) -> int:
190
+ path = ctx.target_path or ""
191
+ if is_under_directory(path, "tests"):
192
+ return 0
193
+ if is_under_directory(path, "src"):
194
+ return 2
195
+ return 1
196
+
197
+ return sorted(contexts, key=_key)