multi-forge 0.3.0__tar.gz → 0.4.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 (355) hide show
  1. {multi_forge-0.3.0 → multi_forge-0.4.0}/PKG-INFO +4 -3
  2. {multi_forge-0.3.0 → multi_forge-0.4.0}/README.md +3 -2
  3. {multi_forge-0.3.0 → multi_forge-0.4.0}/pyproject.toml +4 -2
  4. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/__init__.py +1 -1
  5. multi_forge-0.4.0/src/forge/cli/activity.py +151 -0
  6. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/auth.py +1 -3
  7. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/backend.py +1 -3
  8. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/claude.py +1 -3
  9. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/config_cmd.py +114 -10
  10. multi_forge-0.4.0/src/forge/cli/editor.py +44 -0
  11. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/extensions.py +2 -5
  12. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/gc.py +2 -2
  13. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/commands.py +16 -44
  14. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/direct_commands.py +71 -3
  15. multi_forge-0.4.0/src/forge/cli/hooks/policy.py +212 -0
  16. multi_forge-0.4.0/src/forge/cli/hooks/protocols.py +53 -0
  17. multi_forge-0.4.0/src/forge/cli/launch_confirmation.py +134 -0
  18. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/main.py +70 -7
  19. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/memory.py +20 -42
  20. multi_forge-0.3.0/src/forge/cli/session_handoff.py → multi_forge-0.4.0/src/forge/cli/memory_report.py +24 -22
  21. multi_forge-0.3.0/src/forge/cli/handoff.py → multi_forge-0.4.0/src/forge/cli/memory_writer.py +12 -12
  22. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/policy.py +2 -0
  23. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/proxy.py +28 -16
  24. multi_forge-0.4.0/src/forge/cli/proxy_audit.py +253 -0
  25. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/proxy_costs.py +203 -55
  26. multi_forge-0.4.0/src/forge/cli/runtime.py +105 -0
  27. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/search.py +2 -26
  28. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/session.py +8 -23
  29. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/session_fork.py +226 -14
  30. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/session_lifecycle.py +194 -130
  31. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/status_line.py +490 -161
  32. multi_forge-0.4.0/src/forge/cli/statusline/__init__.py +9 -0
  33. multi_forge-0.4.0/src/forge/cli/statusline/context.py +144 -0
  34. multi_forge-0.4.0/src/forge/cli/statusline/names.py +61 -0
  35. multi_forge-0.4.0/src/forge/cli/statusline/palette.py +149 -0
  36. multi_forge-0.4.0/src/forge/cli/statusline/registry.py +379 -0
  37. multi_forge-0.4.0/src/forge/cli/statusline/throttle.py +172 -0
  38. multi_forge-0.4.0/src/forge/cli/transfer.py +186 -0
  39. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/workflow.py +57 -4
  40. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/backends/litellm.yaml +12 -0
  41. multi_forge-0.4.0/src/forge/config/defaults/templates/anthropic-passthrough.yaml +30 -0
  42. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-anthropic-local.yaml +1 -1
  43. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-anthropic.yaml +3 -3
  44. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-gemini.yaml +1 -1
  45. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-anthropic.yaml +3 -3
  46. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-gemini-flash.yaml +4 -4
  47. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-gemini.yaml +1 -1
  48. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-minimax.yaml +6 -4
  49. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/loader.py +6 -0
  50. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/schema.py +243 -9
  51. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/auth/capabilities.py +2 -1
  52. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/auth/template_secrets.py +23 -8
  53. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/data/model_catalog.yaml +95 -3
  54. multi_forge-0.4.0/src/forge/core/invoker/__init__.py +23 -0
  55. multi_forge-0.4.0/src/forge/core/invoker/claude.py +457 -0
  56. multi_forge-0.4.0/src/forge/core/invoker/types.py +111 -0
  57. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/clients/litellm.py +58 -7
  58. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/clients/openai_compat.py +26 -0
  59. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/clients/openrouter.py +8 -1
  60. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/credentials.py +20 -0
  61. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/types.py +6 -0
  62. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/models/__init__.py +0 -11
  63. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/ops/gc.py +23 -12
  64. multi_forge-0.4.0/src/forge/core/ops/transfer.py +265 -0
  65. multi_forge-0.4.0/src/forge/core/ops/usage_summary.py +364 -0
  66. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/reactive/__init__.py +17 -1
  67. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/reactive/cost_tracking.py +72 -54
  68. multi_forge-0.4.0/src/forge/core/reactive/env.py +363 -0
  69. multi_forge-0.4.0/src/forge/core/reactive/headless_json.py +133 -0
  70. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/reactive/proxy.py +1 -1
  71. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/reactive/routing.py +2 -2
  72. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/reactive/session_runner.py +122 -25
  73. multi_forge-0.4.0/src/forge/core/reactive/structured_output.py +153 -0
  74. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/reactive/tagger.py +34 -3
  75. multi_forge-0.4.0/src/forge/core/runtime/__init__.py +27 -0
  76. multi_forge-0.4.0/src/forge/core/runtime/registry.py +211 -0
  77. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/state/__init__.py +8 -1
  78. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/state/io.py +18 -0
  79. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/transcript.py +1 -1
  80. multi_forge-0.4.0/src/forge/core/usage/__init__.py +56 -0
  81. multi_forge-0.4.0/src/forge/core/usage/billing.py +28 -0
  82. multi_forge-0.4.0/src/forge/core/usage/correlation.py +91 -0
  83. multi_forge-0.4.0/src/forge/core/usage/emit.py +440 -0
  84. multi_forge-0.4.0/src/forge/core/usage/ledger.py +306 -0
  85. multi_forge-0.4.0/src/forge/core/usage/vocabulary.py +70 -0
  86. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/workqueue/queue.py +12 -0
  87. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/__init__.py +3 -2
  88. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/exceptions.py +3 -2
  89. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/installer.py +6 -6
  90. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/preset.py +2 -2
  91. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/tracking.py +2 -2
  92. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/deterministic/coding_standards.py +24 -12
  93. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/semantic/supervisor.py +14 -2
  94. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/team/handlers.py +20 -4
  95. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/types.py +9 -3
  96. multi_forge-0.4.0/src/forge/proxy/audit_logger.py +464 -0
  97. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/client_adapter.py +17 -7
  98. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/converters.py +7 -0
  99. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/cost_logger.py +35 -10
  100. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/cost_tracker.py +31 -28
  101. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/data_models.py +6 -1
  102. multi_forge-0.4.0/src/forge/proxy/intercept.py +314 -0
  103. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/metrics.py +38 -8
  104. multi_forge-0.4.0/src/forge/proxy/passthrough.py +281 -0
  105. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/proxies.py +1 -1
  106. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/proxy_orchestrator.py +58 -7
  107. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/server.py +586 -67
  108. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/utils.py +27 -0
  109. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/adversarial.py +4 -0
  110. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/consensus.py +4 -0
  111. multi_forge-0.4.0/src/forge/review/engine.py +357 -0
  112. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/models.py +14 -9
  113. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/runtime_config.py +141 -15
  114. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/claude/__init__.py +14 -0
  115. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/claude/invoke.py +21 -4
  116. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/claude/paths.py +14 -4
  117. multi_forge-0.4.0/src/forge/session/claude/relocate.py +171 -0
  118. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/manager.py +344 -50
  119. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/memory_inheritance.py +3 -3
  120. multi_forge-0.3.0/src/forge/session/handoff_agent.py → multi_forge-0.4.0/src/forge/session/memory_writer.py +48 -39
  121. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/models.py +46 -12
  122. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/passport.py +1 -28
  123. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/plan_resolution.py +1 -1
  124. multi_forge-0.4.0/src/forge/session/prev_sessions.py +236 -0
  125. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/project_memory.py +1 -34
  126. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/shadow_curation.py +13 -2
  127. multi_forge-0.3.0/src/forge/session/handoff.py → multi_forge-0.4.0/src/forge/session/transfer.py +309 -87
  128. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/sidecar/container.py +84 -0
  129. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/panel/SKILL.md +4 -4
  130. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/SKILL.md +44 -44
  131. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/10-resume.md +66 -5
  132. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/11-config.md +4 -4
  133. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/13-policy.md +18 -12
  134. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/15-skills.md +6 -6
  135. multi_forge-0.3.0/src/skills/qa/resources/checklist/16-handoff.md → multi_forge-0.4.0/src/skills/qa/resources/checklist/16-memory.md +19 -14
  136. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/17-info.md +16 -0
  137. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/18-disable.md +10 -10
  138. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/20-cleanup.md +5 -0
  139. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/3-authentication.md +9 -22
  140. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/4-proxy.md +89 -21
  141. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/5-session.md +172 -54
  142. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/6-hook.md +1 -1
  143. multi_forge-0.4.0/src/skills/qa/resources/checklist/7-costs.md +464 -0
  144. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/8-status-line.md +110 -2
  145. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/9-direct-commands.md +19 -0
  146. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist.md +13 -6
  147. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/report-template.md +1 -1
  148. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/scripts/start-container.sh +37 -18
  149. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/SKILL.md +5 -5
  150. multi_forge-0.3.0/src/skills/review/references/claude-4.7.md → multi_forge-0.4.0/src/skills/review/references/claude-4.8.md +85 -69
  151. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/references/gemini-3.1.md +2 -2
  152. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review-docs/SKILL.md +2 -2
  153. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/SKILL.md +2 -2
  154. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/walkthrough/resources/checklist.md +1 -1
  155. multi_forge-0.3.0/src/forge/cli/hooks/policy.py +0 -151
  156. multi_forge-0.3.0/src/forge/cli/session_memory.py +0 -36
  157. multi_forge-0.3.0/src/forge/core/data/pricing.yaml +0 -140
  158. multi_forge-0.3.0/src/forge/core/models/pricing.py +0 -165
  159. multi_forge-0.3.0/src/forge/core/reactive/env.py +0 -180
  160. multi_forge-0.3.0/src/forge/core/reactive/structured_output.py +0 -62
  161. multi_forge-0.3.0/src/forge/review/engine.py +0 -358
  162. multi_forge-0.3.0/src/forge/session/prev_sessions.py +0 -128
  163. multi_forge-0.3.0/src/skills/qa/resources/checklist/7-costs.md +0 -309
  164. {multi_forge-0.3.0 → multi_forge-0.4.0}/.gitignore +0 -0
  165. {multi_forge-0.3.0 → multi_forge-0.4.0}/LICENSE +0 -0
  166. {multi_forge-0.3.0 → multi_forge-0.4.0}/NOTICE +0 -0
  167. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/agents/.gitkeep +0 -0
  168. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/commands/.gitkeep +0 -0
  169. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/backend/__init__.py +0 -0
  170. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/backend/adapters/__init__.py +0 -0
  171. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/backend/adapters/litellm.py +0 -0
  172. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/backend/creation.py +0 -0
  173. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/backend/registry.py +0 -0
  174. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/__init__.py +0 -0
  175. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/guards.py +0 -0
  176. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/__init__.py +0 -0
  177. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/_group.py +0 -0
  178. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/_helpers.py +0 -0
  179. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/install.py +0 -0
  180. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/read_hygiene.py +0 -0
  181. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/hooks/verification.py +0 -0
  182. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/logs.py +0 -0
  183. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/output.py +0 -0
  184. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/session_addendum.py +0 -0
  185. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/cli/session_manage.py +0 -0
  186. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/__init__.py +0 -0
  187. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/dataclass_utils.py +0 -0
  188. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/__init__.py +0 -0
  189. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/backends/__init__.py +0 -0
  190. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/__init__.py +0 -0
  191. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-gemini-flash-local.yaml +0 -0
  192. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-gemini-local.yaml +0 -0
  193. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-gemini-test.yaml +0 -0
  194. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-openai-codex-local.yaml +0 -0
  195. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-openai-local.yaml +0 -0
  196. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/litellm-openai.yaml +0 -0
  197. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-deepseek.yaml +0 -0
  198. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-glm.yaml +0 -0
  199. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-kimi.yaml +0 -0
  200. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-openai-codex.yaml +0 -0
  201. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-openai.yaml +0 -0
  202. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/config/defaults/templates/openrouter-qwen.yaml +0 -0
  203. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/__init__.py +0 -0
  204. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/auth/__init__.py +0 -0
  205. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/auth/credentials_file.py +0 -0
  206. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/auth/protocols.py +0 -0
  207. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/auth/secrets.py +0 -0
  208. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/data/__init__.py +0 -0
  209. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/data/system_prompt_addendums/__init__.py +0 -0
  210. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/data/system_prompt_addendums/gemini.md +0 -0
  211. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/data/system_prompt_addendums/openai.md +0 -0
  212. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/__init__.py +0 -0
  213. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/clients/__init__.py +0 -0
  214. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/clients/base.py +0 -0
  215. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/detection.py +0 -0
  216. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/errors.py +0 -0
  217. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/llm/protocols.py +0 -0
  218. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/logging.py +0 -0
  219. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/models/catalog.py +0 -0
  220. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/models/types.py +0 -0
  221. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/naming.py +0 -0
  222. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/ops/__init__.py +0 -0
  223. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/ops/context.py +0 -0
  224. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/ops/proxy.py +0 -0
  225. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/ops/resolution.py +0 -0
  226. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/ops/session.py +0 -0
  227. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/ops/session_context.py +0 -0
  228. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/paths.py +0 -0
  229. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/process.py +0 -0
  230. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/reactive/throttle.py +0 -0
  231. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/state/exceptions.py +0 -0
  232. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/state/lock.py +0 -0
  233. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/state/timestamps.py +0 -0
  234. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/typing_helpers.py +0 -0
  235. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/workqueue/__init__.py +0 -0
  236. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/core/workqueue/types.py +0 -0
  237. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/cli.py +0 -0
  238. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/hooks.py +0 -0
  239. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/models.py +0 -0
  240. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/settings_merge.py +0 -0
  241. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/install/version.py +0 -0
  242. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/__init__.py +0 -0
  243. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/deterministic/__init__.py +0 -0
  244. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/deterministic/base.py +0 -0
  245. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/deterministic/registry.py +0 -0
  246. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/deterministic/tdd.py +0 -0
  247. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/engine.py +0 -0
  248. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/protocols.py +0 -0
  249. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/queries.py +0 -0
  250. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/semantic/__init__.py +0 -0
  251. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/semantic/promotion.py +0 -0
  252. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/semantic/verdict.py +0 -0
  253. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/store.py +0 -0
  254. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/team/__init__.py +0 -0
  255. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/team/config.py +0 -0
  256. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/team/prompts.py +0 -0
  257. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/workflow/__init__.py +0 -0
  258. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/workflow/branches.py +0 -0
  259. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/workflow/config.py +0 -0
  260. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/workflow/divergence.py +0 -0
  261. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/workflow/policy.py +0 -0
  262. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/policy/workflow/stages.py +0 -0
  263. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/__init__.py +0 -0
  264. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/base_client.py +0 -0
  265. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/client_factory.py +0 -0
  266. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/error_hints.py +0 -0
  267. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/model_spec.py +0 -0
  268. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/proxy_identity.py +0 -0
  269. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/proxy/proxy_startup.py +0 -0
  270. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/__init__.py +0 -0
  271. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/__init__.py +0 -0
  272. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/codereview-performance.md +0 -0
  273. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/codereview-quick.md +0 -0
  274. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/codereview-security.md +0 -0
  275. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/codereview.md +0 -0
  276. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/docreview-quick.md +0 -0
  277. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/docreview.md +0 -0
  278. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/resources/thinkdeep.md +0 -0
  279. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/routing.py +0 -0
  280. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/review/synthesis.py +0 -0
  281. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/__init__.py +0 -0
  282. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/bm25_store.py +0 -0
  283. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/content_store.py +0 -0
  284. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/engine.py +0 -0
  285. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/exceptions.py +0 -0
  286. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/extractor.py +0 -0
  287. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/index_state.py +0 -0
  288. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/store.py +0 -0
  289. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/search/tokenizer.py +0 -0
  290. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/__init__.py +0 -0
  291. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/active.py +0 -0
  292. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/artifacts.py +0 -0
  293. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/claude/cleanup.py +0 -0
  294. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/cleanup.py +0 -0
  295. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/config.py +0 -0
  296. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/direct_model.py +0 -0
  297. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/effective.py +0 -0
  298. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/exceptions.py +0 -0
  299. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/hooks/__init__.py +0 -0
  300. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/hooks/models.py +0 -0
  301. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/hooks/session_start.py +0 -0
  302. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/identity.py +0 -0
  303. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/index.py +0 -0
  304. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/overrides.py +0 -0
  305. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/store.py +0 -0
  306. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/validation.py +0 -0
  307. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/worktree/__init__.py +0 -0
  308. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/worktree/cleanup.py +0 -0
  309. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/worktree/config_copy.py +0 -0
  310. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/session/worktree/create.py +0 -0
  311. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/sidecar/__init__.py +0 -0
  312. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/sidecar/docker.py +0 -0
  313. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/forge/sidecar/secrets.py +0 -0
  314. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/analyze/SKILL.md +0 -0
  315. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/challenge/SKILL.md +0 -0
  316. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/consensus/SKILL.md +0 -0
  317. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/consensus/resources/code_consensus_evaluation.md +0 -0
  318. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/consensus/resources/consensus_evaluation.md +0 -0
  319. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/consensus/resources/synthesis.md +0 -0
  320. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/debate/SKILL.md +0 -0
  321. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/debate/resources/code_debate_evaluation.md +0 -0
  322. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/debate/resources/debate_evaluation.md +0 -0
  323. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/panel/resources/synthesis.md +0 -0
  324. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/0-enable.md +0 -0
  325. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/1-preflight.md +0 -0
  326. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/12-search.md +0 -0
  327. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/14-workflow.md +0 -0
  328. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/19-uninstall.md +0 -0
  329. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/resources/checklist/2-extension.md +0 -0
  330. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/qa/scripts/walkthrough-state.py +0 -0
  331. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/references/claude-4.6.md +0 -0
  332. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/references/gpt-5.5.md +0 -0
  333. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/references/skills-writing-guide.md +0 -0
  334. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/resources/code-anthropic.md +0 -0
  335. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/resources/code-gemini.md +0 -0
  336. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/resources/code-openai.md +0 -0
  337. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review/resources/code.md +0 -0
  338. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review-docs/resources/docs-anthropic.md +0 -0
  339. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review-docs/resources/docs-gemini.md +0 -0
  340. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review-docs/resources/docs-openai.md +0 -0
  341. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/review-docs/resources/docs.md +0 -0
  342. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/smoke-test/SKILL.md +0 -0
  343. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/smoke-test/scripts/smoke-test.sh +0 -0
  344. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/code-anthropic.md +0 -0
  345. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/code-gemini.md +0 -0
  346. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/code-openai.md +0 -0
  347. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/code.md +0 -0
  348. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/docs-anthropic.md +0 -0
  349. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/docs-gemini.md +0 -0
  350. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/docs-openai.md +0 -0
  351. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/understand/resources/docs.md +0 -0
  352. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/walkthrough/SKILL.md +0 -0
  353. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/walkthrough/scripts/run-in-repo.sh +0 -0
  354. {multi_forge-0.3.0 → multi_forge-0.4.0}/src/skills/walkthrough/scripts/setup-test-repo.sh +0 -0
  355. {multi_forge-0.3.0 → multi_forge-0.4.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.3.0
3
+ Version: 0.4.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
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
  # Multi-Forge
43
43
 
44
44
  <p align="left">
45
- <img src="assets/logo.jpg" alt="Dusk" width=320">
45
+ <img src="assets/logo.jpg" alt="Dusk" width=240">
46
46
  </p>
47
47
 
48
48
  [![PyPI](https://img.shields.io/pypi/v/multi-forge)](https://pypi.org/project/multi-forge/)
@@ -96,7 +96,7 @@ Running `claude` directly bypasses session tracking. When you launch through For
96
96
  | Hook-driven artifacts | No | Yes -- plan snapshots, transcript capture |
97
97
  | Policy enforcement | No | Yes -- TDD, coding standards, supervisor |
98
98
  | Search across sessions | No | Yes -- `forge search` indexes transcripts |
99
- | Handoff agent | No | Yes -- auto-updates project docs on exit |
99
+ | Project memory | No | Yes -- passported docs auto-updated on exit |
100
100
 
101
101
  Even without a proxy, `forge session start` gives you session tracking, hooks, and the status line (direct mode is the
102
102
  default). The proxy adds multi-model routing on top. (`forge claude start` is also available as a bare launcher with
@@ -209,6 +209,7 @@ inside Claude Code for an interactive walkthrough.
209
209
  | ---------------------- | -------------------------------------------- |
210
210
  | `forge claude` | Bare launch, settings preset management |
211
211
  | `forge session` | Named sessions, worktrees, resume, fork |
212
+ | `forge memory` | Project memory passports, shadow proposals |
212
213
  | `forge proxy` | Model routing, templates, tier mappings |
213
214
  | `forge authentication` | Credential management (`credentials.yaml`) |
214
215
  | `forge policy` | Policy enforcement, plan supervision |
@@ -1,7 +1,7 @@
1
1
  # Multi-Forge
2
2
 
3
3
  <p align="left">
4
- <img src="assets/logo.jpg" alt="Dusk" width=320">
4
+ <img src="assets/logo.jpg" alt="Dusk" width=240">
5
5
  </p>
6
6
 
7
7
  [![PyPI](https://img.shields.io/pypi/v/multi-forge)](https://pypi.org/project/multi-forge/)
@@ -55,7 +55,7 @@ Running `claude` directly bypasses session tracking. When you launch through For
55
55
  | Hook-driven artifacts | No | Yes -- plan snapshots, transcript capture |
56
56
  | Policy enforcement | No | Yes -- TDD, coding standards, supervisor |
57
57
  | Search across sessions | No | Yes -- `forge search` indexes transcripts |
58
- | Handoff agent | No | Yes -- auto-updates project docs on exit |
58
+ | Project memory | No | Yes -- passported docs auto-updated on exit |
59
59
 
60
60
  Even without a proxy, `forge session start` gives you session tracking, hooks, and the status line (direct mode is the
61
61
  default). The proxy adds multi-model routing on top. (`forge claude start` is also available as a bare launcher with
@@ -168,6 +168,7 @@ inside Claude Code for an interactive walkthrough.
168
168
  | ---------------------- | -------------------------------------------- |
169
169
  | `forge claude` | Bare launch, settings preset management |
170
170
  | `forge session` | Named sessions, worktrees, resume, fork |
171
+ | `forge memory` | Project memory passports, shadow proposals |
171
172
  | `forge proxy` | Model routing, templates, tier mappings |
172
173
  | `forge authentication` | Credential management (`credentials.yaml`) |
173
174
  | `forge policy` | Policy enforcement, plan supervision |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "multi-forge"
3
- version = "0.3.0"
3
+ version = "0.4.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"
@@ -122,10 +122,11 @@ disallow_untyped_calls = false
122
122
  constraint-dependencies = [
123
123
  "filelock>=3.20.3",
124
124
  "virtualenv>=20.36.1",
125
- "aiohttp>=3.13.4",
125
+ "aiohttp>=3.14.0",
126
126
  "cryptography>=46.0.7",
127
127
  "python-multipart>=0.0.26",
128
128
  "Pygments>=2.20.0",
129
+ "starlette>=1.0.1",
129
130
  ]
130
131
  # litellm pins fastapi-sso<0.17.0 but 0.16.x has a CSRF vulnerability.
131
132
  # Forge doesn't use fastapi-sso directly; override to force the patched version.
@@ -148,6 +149,7 @@ dev = [
148
149
  "types-PyYAML>=6.0.0",
149
150
  "types-requests>=2.31.0",
150
151
  "python-dotenv>=1.2.1",
152
+ "httpx2>=2.3.0",
151
153
  ]
152
154
  provider-check = [
153
155
  "anthropic>=0.100.0",
@@ -1,3 +1,3 @@
1
1
  """Multi-Forge - Multi-runtime agent toolkit."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
@@ -0,0 +1,151 @@
1
+ """``forge activity`` — per-session Forge *automation* activity (supervisor, memory
2
+ writer, workflow verbs) + policy decisions. NOT your full interactive Claude usage.
3
+
4
+ Reads the two already-captured planes (usage ledger + ``confirmed.policy.decisions``)
5
+ via :func:`forge.core.ops.usage_summary.build_session_activity_summary` and renders a
6
+ table. Cost is reported-or-estimated (best-effort attribution) — ``forge proxy costs show``
7
+ stays the authoritative spend view.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import sys
14
+ from dataclasses import asdict
15
+ from datetime import datetime, timedelta, timezone
16
+
17
+ import click
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+
21
+ from forge.cli.output import print_error_with_tip
22
+ from forge.core.ops.session_context import (
23
+ SessionContextError,
24
+ resolve_session_identifier,
25
+ )
26
+ from forge.core.ops.usage_summary import (
27
+ SessionActivitySummary,
28
+ build_session_activity_summary,
29
+ )
30
+
31
+ console = Console()
32
+
33
+
34
+ @click.command("activity")
35
+ @click.argument("session", required=False)
36
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
37
+ @click.option("--days", "-d", type=int, default=30, help="Look back this many days (default: 30)")
38
+ @click.option("--all", "all_time", is_flag=True, help="Report all time (ignore --days)")
39
+ def activity_cmd(session: str | None, as_json: bool, days: int, all_time: bool) -> None:
40
+ """Show Forge's automation activity for a session: supervisor checks, cost, tokens.
41
+
42
+ This is what Forge did *on top of* your session — the supervisor, memory writer,
43
+ and workflow verbs (panel/debate/...) — plus policy decisions. It is **not** your
44
+ full interactive Claude usage. Reads the usage ledger and the session's
45
+ policy-decision log. Cost is reported-or-estimated (best-effort attribution);
46
+ 'forge proxy costs show' is the authoritative spend view.
47
+
48
+ \b
49
+ Examples:
50
+ forge activity # current session ($FORGE_SESSION)
51
+ forge activity planner # a named session (or Claude UUID)
52
+ forge activity --all --json # full history, JSON
53
+ """
54
+ try:
55
+ session_name, forge_root = resolve_session_identifier(session)
56
+ except SessionContextError as e:
57
+ if as_json:
58
+ click.echo(json.dumps({"error": str(e)}))
59
+ else:
60
+ print_error_with_tip(
61
+ str(e),
62
+ "Run 'forge session list' to see sessions.",
63
+ console=console,
64
+ )
65
+ sys.exit(1)
66
+
67
+ since = None if all_time else datetime.now(timezone.utc) - timedelta(days=days)
68
+ summary = build_session_activity_summary(session_name, forge_root, since=since)
69
+
70
+ if as_json:
71
+ console.print_json(data=asdict(summary))
72
+ return
73
+ _render(summary, days=None if all_time else days)
74
+
75
+
76
+ def _render(summary: SessionActivitySummary, *, days: int | None) -> None:
77
+ scope = "all time" if days is None else f"last {days}d"
78
+ if summary.is_empty:
79
+ console.print(f"[dim]No Forge activity for session '{summary.session}' ({scope}).[/dim]")
80
+ return
81
+
82
+ console.print(f"\n[bold]Forge activity — {summary.session}[/bold] [dim]({scope})[/dim]")
83
+ console.print(
84
+ "[dim]Forge automation (supervisor, memory writer, workflow verbs) — "
85
+ "not your full interactive session.[/dim]"
86
+ )
87
+
88
+ if summary.commands:
89
+ # The Workers column only earns its width when a fan-out (panel/debate/...) ran;
90
+ # supervisor/memory-writer have no workers, so most sessions skip it.
91
+ show_workers = any(c.workers for c in summary.commands)
92
+ table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
93
+ table.add_column("Command", style="cyan")
94
+ table.add_column("Calls", justify="right")
95
+ if show_workers:
96
+ table.add_column("Workers", justify="right", style="dim")
97
+ table.add_column("Errors", justify="right")
98
+ table.add_column("Tokens in/out", justify="right", style="dim")
99
+ table.add_column("Cost", justify="right", style="dim")
100
+ for c in summary.commands:
101
+ errors = f"[red]{c.errors}[/red]" if c.errors else "0"
102
+ tokens = f"{c.input_tokens}/{c.output_tokens}" if (c.input_tokens or c.output_tokens) else "-"
103
+ cost = f"~{_fmt_usd(c.cost_micro_usd)}" if c.cost_micro_usd is not None else "-"
104
+ row = [c.command, str(c.calls)]
105
+ if show_workers:
106
+ row.append(str(c.workers) if c.workers else "-")
107
+ row += [errors, tokens, cost]
108
+ table.add_row(*row)
109
+ console.print(table)
110
+
111
+ pol = summary.policy
112
+ if pol and pol.has_content:
113
+ console.print(
114
+ f"\n[bold]Supervisor[/bold]: {pol.supervisor_allow} allow · "
115
+ f"{pol.supervisor_warn} warn · {pol.supervisor_deny} block"
116
+ )
117
+ for warning in pol.recent_warnings:
118
+ console.print(f" [yellow]•[/yellow] {warning}")
119
+
120
+ if summary.subagents:
121
+ console.print(f"\n[bold]Subagents[/bold]: {summary.subagents}")
122
+
123
+ total_cost = f"~{_fmt_usd(summary.total_cost_micro_usd)}" if summary.total_cost_micro_usd is not None else "n/a"
124
+ console.print(
125
+ f"\n[dim]Total:[/dim] {summary.total_events} events · "
126
+ f"{summary.total_input_tokens}/{summary.total_output_tokens} tok · {total_cost}"
127
+ )
128
+
129
+ for note in _footnotes(summary):
130
+ console.print(f"[dim]{note}[/dim]")
131
+
132
+
133
+ def _footnotes(summary: SessionActivitySummary) -> list[str]:
134
+ notes: list[str] = []
135
+ if summary.cost_partial:
136
+ notes.append("cost is best-effort and partial (some calls report no cost)")
137
+ if summary.policy is not None and summary.policy.log_capped:
138
+ notes.append("policy decision log is at capacity — older decisions may not be shown")
139
+ if summary.session_tagging_partial:
140
+ notes.append("some calls (e.g. the action tagger) are not session-attributed")
141
+ notes.append("cost is reported-or-estimated, best-effort; 'forge proxy costs show' is the authoritative spend view")
142
+ return notes
143
+
144
+
145
+ def _fmt_usd(micros: int | None) -> str:
146
+ if micros is None:
147
+ return "-"
148
+ dollars = micros / 1_000_000
149
+ if dollars and abs(dollars) < 0.01:
150
+ return f"${dollars:.4f}"
151
+ return f"${dollars:.2f}"
@@ -408,8 +408,7 @@ def status(profile: str | None) -> None:
408
408
  help="Profile to remove credentials from (default: 'default' or FORGE_PROFILE)",
409
409
  )
410
410
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
411
- @click.option("--force", "-f", is_flag=True, hidden=True, help="Deprecated alias for --yes")
412
- def logout(profile: str | None, yes: bool, force: bool) -> None:
411
+ def logout(profile: str | None, yes: bool) -> None:
413
412
  """Remove stored credentials for a profile.
414
413
 
415
414
  Deletes the profile from ~/.forge/credentials.yaml.
@@ -421,7 +420,6 @@ def logout(profile: str | None, yes: bool, force: bool) -> None:
421
420
  forge authentication logout --profile work
422
421
  forge authentication logout -y # Skip confirmation
423
422
  """
424
- yes = yes or force
425
423
  profile_name = resolve_profile(profile)
426
424
 
427
425
  if not yes:
@@ -251,8 +251,7 @@ def stop_cmd(adapter: str, port: int) -> None:
251
251
  help="Delete specific instance (if not specified, deletes config)",
252
252
  )
253
253
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
254
- @click.option("--force", "-f", is_flag=True, hidden=True, help="Deprecated alias for --yes")
255
- def delete_cmd(adapter: str, port: int | None, yes: bool, force: bool) -> None:
254
+ def delete_cmd(adapter: str, port: int | None, yes: bool) -> None:
256
255
  """Delete a backend instance or config.
257
256
 
258
257
  Without --port: Deletes the backend config (stops all instances first).
@@ -260,7 +259,6 @@ def delete_cmd(adapter: str, port: int | None, yes: bool, force: bool) -> None:
260
259
  """
261
260
  import shutil
262
261
 
263
- yes = yes or force
264
262
  console = Console(width=200)
265
263
 
266
264
  if port is not None:
@@ -392,13 +392,11 @@ def preset_edit() -> None:
392
392
 
393
393
  @preset.command("reset")
394
394
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
395
- @click.option("--force", "-f", is_flag=True, hidden=True, help="Deprecated alias for --yes")
396
- def preset_reset(yes: bool, force: bool) -> None:
395
+ def preset_reset(yes: bool) -> None:
397
396
  """Reset settings preset to built-in defaults."""
398
397
  from forge.core.state import atomic_write_text
399
398
  from forge.install.preset import get_builtin_preset_json, get_preset_path
400
399
 
401
- yes = yes or force
402
400
  preset_path = get_preset_path()
403
401
 
404
402
  if not yes:
@@ -16,7 +16,8 @@ import shutil
16
16
  import subprocess
17
17
  import sys
18
18
  import tempfile
19
- from dataclasses import fields
19
+ from collections.abc import MutableMapping
20
+ from dataclasses import asdict, fields, is_dataclass
20
21
  from pathlib import Path
21
22
  from typing import Any
22
23
 
@@ -71,7 +72,12 @@ def show_cmd(raw: bool = False) -> None:
71
72
 
72
73
  effective: dict[str, Any] = {}
73
74
  for f in fields(RuntimeConfig):
74
- effective[f.name] = getattr(rc, f.name)
75
+ val = getattr(rc, f.name)
76
+ # Nested config (e.g. statusline) must render as a plain mapping — yaml
77
+ # can't dump a dataclass instance.
78
+ if is_dataclass(val) and not isinstance(val, type):
79
+ val = asdict(val)
80
+ effective[f.name] = val
75
81
 
76
82
  content = yaml.dump(effective, default_flow_style=False, sort_keys=False)
77
83
 
@@ -107,6 +113,11 @@ def set_cmd(key_value: str) -> None:
107
113
 
108
114
  key, value = key_value.split("=", 1)
109
115
 
116
+ # Nested section keys (e.g. statusline.cost_mode) take the dotted path.
117
+ if "." in key:
118
+ _set_nested_key(key, value, console)
119
+ return
120
+
110
121
  known_fields = {f.name: f for f in fields(RuntimeConfig)}
111
122
  if key not in known_fields:
112
123
  console.print(f"[red]Error:[/red] Unknown config key: '{key}'")
@@ -198,6 +209,20 @@ def edit_cmd() -> None:
198
209
  console.print(f"Your changes are saved at: {display_path(tmp_path)}")
199
210
  sys.exit(1)
200
211
 
212
+ # Segment names aren't validated by StatusLineConfig (the renderer and
213
+ # the set/edit CLI own that), so the edit path must enforce the allowlist
214
+ # too — otherwise statusline.segments: [path, bogus] would be accepted.
215
+ sl_section = edited_data.get("statusline")
216
+ if isinstance(sl_section, dict) and isinstance(sl_section.get("segments"), list):
217
+ unknown_segs = _unknown_segments(sl_section["segments"])
218
+ if unknown_segs:
219
+ from forge.cli.statusline.names import SEGMENT_NAMES
220
+
221
+ console.print(f"[red]Error:[/red] Unknown statusline segment(s): {', '.join(map(str, unknown_segs))}")
222
+ console.print(f"[dim]Valid segments: {', '.join(SEGMENT_NAMES)}[/dim]")
223
+ console.print(f"Your changes are saved at: {display_path(tmp_path)}")
224
+ sys.exit(1)
225
+
201
226
  write_runtime_config(dict(edited_data))
202
227
  success = True
203
228
  console.print("[green]Updated[/green] runtime configuration")
@@ -213,14 +238,12 @@ def edit_cmd() -> None:
213
238
  @config.command("reset")
214
239
  @click.argument("key", required=False)
215
240
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
216
- @click.option("--force", "-f", is_flag=True, hidden=True, help="Deprecated alias for --yes")
217
- def reset_cmd(key: str | None = None, yes: bool = False, force: bool = False) -> None:
241
+ def reset_cmd(key: str | None = None, yes: bool = False) -> None:
218
242
  """Reset configuration to defaults.
219
243
 
220
244
  With KEY: removes that key (reverts to built-in default).
221
245
  Without KEY: deletes the entire config file.
222
246
  """
223
- yes = yes or force
224
247
  console = Console(width=200)
225
248
  config_path = get_config_path()
226
249
 
@@ -257,18 +280,23 @@ def reset_cmd(key: str | None = None, yes: bool = False, force: bool = False) ->
257
280
  return
258
281
 
259
282
  del data[key]
283
+ _persist_or_clear(data, config_path)
284
+
285
+ default_val = getattr(RuntimeConfig(), key)
286
+ console.print(f"[green]Reset[/green] {key} (default: {default_val})")
260
287
 
288
+
289
+ # --- Helpers ---
290
+
291
+
292
+ def _persist_or_clear(data: MutableMapping[str, Any], config_path: Path) -> None:
293
+ """Write ``data`` back, or remove the config file when nothing remains."""
261
294
  if data:
262
295
  write_runtime_config(dict(data))
263
296
  else:
264
297
  config_path.unlink()
265
298
  reset_runtime_config()
266
299
 
267
- default_val = getattr(RuntimeConfig(), key)
268
- console.print(f"[green]Reset[/green] {key} (default: {default_val})")
269
-
270
-
271
- # --- Helpers ---
272
300
 
273
301
  _COERCE_ERROR = object()
274
302
 
@@ -301,3 +329,79 @@ def _coerce_value(key: str, value: str, field_info: Any) -> Any:
301
329
 
302
330
  # String fields: pass through
303
331
  return value
332
+
333
+
334
+ def _unknown_segments(segments: list[Any]) -> list[Any]:
335
+ """Return segment names not in the allowlist (the set/edit strict gate).
336
+
337
+ Segment names are intentionally NOT validated by ``StatusLineConfig`` (the
338
+ renderer drops unknown names on load); the write paths reject them instead.
339
+ """
340
+ from forge.cli.statusline.names import SEGMENT_NAMES
341
+
342
+ return [s for s in segments if s not in SEGMENT_NAMES]
343
+
344
+
345
+ def _set_nested_key(key: str, value: str, console: Console) -> None:
346
+ """Set a dotted nested config key. Only ``statusline.<subkey>`` is supported.
347
+
348
+ Strict (fail-closed): unknown section/subkey, invalid enum values, and
349
+ unknown segment names all error and exit non-zero, naming valid options.
350
+ """
351
+ from forge.cli.statusline.names import SEGMENT_NAMES
352
+ from forge.runtime_config import StatusLineConfig
353
+
354
+ section, _, subkey = key.partition(".")
355
+ if section != "statusline":
356
+ console.print(f"[red]Error:[/red] Unknown config section: '{section}'")
357
+ console.print("\n[dim]Only 'statusline.*' nested keys are supported.[/dim]")
358
+ sys.exit(1)
359
+
360
+ sl_fields = {f.name: f for f in fields(StatusLineConfig)}
361
+ if subkey not in sl_fields:
362
+ console.print(f"[red]Error:[/red] Unknown statusline key: '{subkey}'")
363
+ console.print(f"\n[dim]Available: {', '.join(sorted(sl_fields))}[/dim]")
364
+ sys.exit(1)
365
+
366
+ coerced_sub: Any
367
+ if subkey == "segments":
368
+ coerced_sub = [s.strip() for s in value.split(",") if s.strip()]
369
+ unknown = _unknown_segments(coerced_sub)
370
+ if unknown:
371
+ console.print(f"[red]Error:[/red] Unknown segment(s): {', '.join(unknown)}")
372
+ console.print(f"\n[dim]Valid segments: {', '.join(SEGMENT_NAMES)}[/dim]")
373
+ sys.exit(1)
374
+ else:
375
+ coerced_sub = _coerce_value(subkey, value, sl_fields[subkey])
376
+ if coerced_sub is _COERCE_ERROR:
377
+ console.print(f"[red]Error:[/red] Invalid value for 'statusline.{subkey}': {value}")
378
+ sys.exit(1)
379
+
380
+ config_path = get_config_path()
381
+ if config_path.is_file():
382
+ from ruamel.yaml import YAML
383
+
384
+ ruamel = YAML()
385
+ ruamel.preserve_quotes = True
386
+ with open(config_path) as f:
387
+ data = ruamel.load(f) or {}
388
+ else:
389
+ data = {}
390
+
391
+ section_data = data.get("statusline")
392
+ if not isinstance(section_data, dict):
393
+ section_data = {}
394
+ section_data[subkey] = coerced_sub
395
+ data["statusline"] = section_data
396
+
397
+ # Validate via construction — StatusLineConfig.__post_init__ rejects bad enums
398
+ # (fail-closed); segment names were already checked above.
399
+ known_fields = {f.name for f in fields(RuntimeConfig)}
400
+ try:
401
+ RuntimeConfig(**{k: v for k, v in dict(data).items() if k in known_fields})
402
+ except (ValueError, TypeError) as e:
403
+ console.print(f"[red]Error:[/red] Invalid configuration: {e}")
404
+ sys.exit(1)
405
+
406
+ write_runtime_config(data)
407
+ console.print(f"[green]Set[/green] {key}={coerced_sub}")
@@ -0,0 +1,44 @@
1
+ """Shared $EDITOR launcher for editable transfer/context files.
2
+
3
+ Extracted from ``session_lifecycle`` so both ``forge session resume --review``
4
+ and ``forge transfer edit`` use one editor-launch path with the same
5
+ git-commit-style abort behavior: a non-zero editor exit aborts and leaves the
6
+ file untouched.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import shlex
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from rich.console import Console
19
+
20
+ from forge.cli.output import print_tip
21
+
22
+
23
+ def open_in_editor(file_path: Path, *, console: Console, abort_tip: str | None = None) -> None:
24
+ """Open ``file_path`` in $EDITOR, aborting on a non-zero editor exit.
25
+
26
+ Git-commit-style: a non-zero editor exit prints the optional ``abort_tip``
27
+ and exits with the editor's return code, leaving the file as the user left
28
+ it. Exits 1 when $EDITOR is empty or its program is not on PATH.
29
+ """
30
+ editor = os.environ.get("EDITOR", "vim")
31
+ editor_argv = shlex.split(editor)
32
+ if not editor_argv:
33
+ console.print("[red]Error:[/red] $EDITOR is empty. Set $EDITOR to an available editor.")
34
+ sys.exit(1)
35
+ if not shutil.which(editor_argv[0]):
36
+ console.print(f"[red]Error:[/red] Editor '{editor}' not found. Set $EDITOR to an available editor.")
37
+ sys.exit(1)
38
+
39
+ result = subprocess.run([*editor_argv, str(file_path)])
40
+ if result.returncode != 0:
41
+ console.print(f"[red]Aborted:[/red] editor exited with code {result.returncode}.")
42
+ if abort_tip:
43
+ print_tip(abort_tip, blank_before=False, console=console)
44
+ sys.exit(result.returncode)
@@ -712,8 +712,7 @@ def sync_cmd(scope: str | None, force: bool) -> None:
712
712
  help="Disable ALL tracked installations",
713
713
  )
714
714
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
715
- @click.option("--force", "-f", is_flag=True, hidden=True, help="Deprecated alias for --yes")
716
- def disable_cmd(scope: str | None, uninstall_all: bool, yes: bool, force: bool) -> None:
715
+ def disable_cmd(scope: str | None, uninstall_all: bool, yes: bool) -> None:
717
716
  """Disable Forge extensions.
718
717
 
719
718
  Removes only files and settings entries that were added by Forge.
@@ -737,8 +736,6 @@ def disable_cmd(scope: str | None, uninstall_all: bool, yes: bool, force: bool)
737
736
  forge extension disable --scope local # Disable local scope
738
737
  forge extension disable --all --yes # Disable everything
739
738
  """
740
- yes = yes or force
741
-
742
739
  if uninstall_all and scope is not None:
743
740
  raise click.UsageError("--all and --scope are mutually exclusive.")
744
741
  try:
@@ -791,7 +788,7 @@ def disable_cmd(scope: str | None, uninstall_all: bool, yes: bool, force: bool)
791
788
  console.print("[bold]Settings:[/bold]")
792
789
  console.print(table)
793
790
 
794
- if not (force or yes):
791
+ if not yes:
795
792
  if not click.confirm("\nProceed with disable?"):
796
793
  console.print("[dim]Cancelled.[/dim]")
797
794
  return
@@ -24,7 +24,7 @@ from forge.core.ops.gc import CleanError, CleanReport, collect_clean_report, run
24
24
  @click.option("--verbose", "-v", is_flag=True, help="Show individual items")
25
25
  @click.option("--json", "as_json", is_flag=True, help="JSON output")
26
26
  def clean_cmd(scope: str, yes: bool, verbose: bool, as_json: bool) -> None:
27
- """Remove orphaned Forge state (sessions, handoff files, stale entries).
27
+ """Remove orphaned Forge state (sessions, transfer files, stale entries).
28
28
 
29
29
  By default, shows what would be cleaned (dry-run). Pass --yes to actually delete.
30
30
 
@@ -156,7 +156,7 @@ def _category_label(category: str) -> str:
156
156
  """Human-readable label for a category."""
157
157
  labels = {
158
158
  "session_dirs": "Orphan session dirs:",
159
- "handoff_files": "Orphan handoff files:",
159
+ "transfer_files": "Orphan transfer files:",
160
160
  "active_entries": "Stale active entries:",
161
161
  "work_queue": "Stale work queue:",
162
162
  "proxies": "Stale proxy entries:",