multi-forge 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. forge/__init__.py +3 -0
  2. forge/_extensions/agents/.gitkeep +0 -0
  3. forge/_extensions/commands/.gitkeep +0 -0
  4. forge/_extensions/skills/analyze/SKILL.md +87 -0
  5. forge/_extensions/skills/challenge/SKILL.md +91 -0
  6. forge/_extensions/skills/consensus/SKILL.md +120 -0
  7. forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
  8. forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
  9. forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
  10. forge/_extensions/skills/debate/SKILL.md +116 -0
  11. forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
  12. forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
  13. forge/_extensions/skills/panel/SKILL.md +141 -0
  14. forge/_extensions/skills/panel/resources/synthesis.md +103 -0
  15. forge/_extensions/skills/qa/SKILL.md +704 -0
  16. forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
  17. forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
  18. forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
  19. forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
  20. forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
  21. forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
  22. forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
  23. forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
  24. forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
  25. forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
  26. forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
  27. forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
  28. forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
  29. forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
  30. forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
  31. forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
  32. forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
  33. forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
  34. forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
  35. forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
  36. forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
  37. forge/_extensions/skills/qa/resources/checklist.md +103 -0
  38. forge/_extensions/skills/qa/resources/report-template.md +62 -0
  39. forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
  40. forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
  41. forge/_extensions/skills/review/SKILL.md +125 -0
  42. forge/_extensions/skills/review/references/claude-4.6.md +474 -0
  43. forge/_extensions/skills/review/references/claude-4.7.md +710 -0
  44. forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
  45. forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
  46. forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
  47. forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
  48. forge/_extensions/skills/review/resources/code-gemini.md +184 -0
  49. forge/_extensions/skills/review/resources/code-openai.md +203 -0
  50. forge/_extensions/skills/review/resources/code.md +160 -0
  51. forge/_extensions/skills/review-docs/SKILL.md +121 -0
  52. forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
  53. forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
  54. forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
  55. forge/_extensions/skills/review-docs/resources/docs.md +170 -0
  56. forge/_extensions/skills/smoke-test/SKILL.md +27 -0
  57. forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
  58. forge/_extensions/skills/understand/SKILL.md +148 -0
  59. forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
  60. forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
  61. forge/_extensions/skills/understand/resources/code-openai.md +181 -0
  62. forge/_extensions/skills/understand/resources/code.md +163 -0
  63. forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
  64. forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
  65. forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
  66. forge/_extensions/skills/understand/resources/docs.md +177 -0
  67. forge/_extensions/skills/walkthrough/SKILL.md +599 -0
  68. forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
  69. forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
  70. forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
  71. forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
  72. forge/backend/__init__.py +174 -0
  73. forge/backend/adapters/__init__.py +38 -0
  74. forge/backend/adapters/litellm.py +158 -0
  75. forge/backend/creation.py +89 -0
  76. forge/backend/registry.py +178 -0
  77. forge/cli/__init__.py +16 -0
  78. forge/cli/auth.py +483 -0
  79. forge/cli/backend.py +298 -0
  80. forge/cli/claude.py +411 -0
  81. forge/cli/config_cmd.py +303 -0
  82. forge/cli/extensions.py +1001 -0
  83. forge/cli/gc.py +165 -0
  84. forge/cli/guard.py +1018 -0
  85. forge/cli/guards.py +106 -0
  86. forge/cli/handoff.py +110 -0
  87. forge/cli/hooks/__init__.py +36 -0
  88. forge/cli/hooks/_group.py +20 -0
  89. forge/cli/hooks/_helpers.py +149 -0
  90. forge/cli/hooks/commands.py +1677 -0
  91. forge/cli/hooks/direct_commands.py +1304 -0
  92. forge/cli/hooks/install.py +232 -0
  93. forge/cli/hooks/policy.py +151 -0
  94. forge/cli/hooks/read_hygiene.py +74 -0
  95. forge/cli/hooks/verification.py +370 -0
  96. forge/cli/logs.py +406 -0
  97. forge/cli/main.py +292 -0
  98. forge/cli/proxy.py +1821 -0
  99. forge/cli/proxy_costs.py +313 -0
  100. forge/cli/search.py +416 -0
  101. forge/cli/session.py +892 -0
  102. forge/cli/session_addendum.py +81 -0
  103. forge/cli/session_fork.py +750 -0
  104. forge/cli/session_handoff.py +141 -0
  105. forge/cli/session_lifecycle.py +2053 -0
  106. forge/cli/session_manage.py +1336 -0
  107. forge/cli/session_memory.py +201 -0
  108. forge/cli/status_line.py +1398 -0
  109. forge/cli/workflow.py +1964 -0
  110. forge/config/__init__.py +110 -0
  111. forge/config/dataclass_utils.py +88 -0
  112. forge/config/defaults/__init__.py +0 -0
  113. forge/config/defaults/backends/__init__.py +0 -0
  114. forge/config/defaults/backends/litellm.yaml +196 -0
  115. forge/config/defaults/templates/__init__.py +0 -0
  116. forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
  117. forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
  118. forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
  119. forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
  120. forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
  121. forge/config/defaults/templates/litellm-gemini.yaml +21 -0
  122. forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
  123. forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
  124. forge/config/defaults/templates/litellm-openai.yaml +28 -0
  125. forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
  126. forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
  127. forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
  128. forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
  129. forge/config/defaults/templates/openrouter-glm.yaml +23 -0
  130. forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
  131. forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
  132. forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
  133. forge/config/defaults/templates/openrouter-openai.yaml +28 -0
  134. forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
  135. forge/config/loader.py +675 -0
  136. forge/config/schema.py +448 -0
  137. forge/core/__init__.py +5 -0
  138. forge/core/auth/__init__.py +67 -0
  139. forge/core/auth/capabilities.py +219 -0
  140. forge/core/auth/credentials_file.py +244 -0
  141. forge/core/auth/protocols.py +18 -0
  142. forge/core/auth/secrets.py +243 -0
  143. forge/core/auth/template_secrets.py +112 -0
  144. forge/core/data/__init__.py +5 -0
  145. forge/core/data/model_catalog.yaml +1522 -0
  146. forge/core/data/pricing.yaml +140 -0
  147. forge/core/data/system_prompt_addendums/__init__.py +0 -0
  148. forge/core/data/system_prompt_addendums/gemini.md +330 -0
  149. forge/core/data/system_prompt_addendums/openai.md +328 -0
  150. forge/core/llm/__init__.py +231 -0
  151. forge/core/llm/clients/__init__.py +14 -0
  152. forge/core/llm/clients/base.py +115 -0
  153. forge/core/llm/clients/litellm.py +619 -0
  154. forge/core/llm/clients/openai_compat.py +244 -0
  155. forge/core/llm/clients/openrouter.py +234 -0
  156. forge/core/llm/credentials.py +439 -0
  157. forge/core/llm/detection.py +86 -0
  158. forge/core/llm/errors.py +44 -0
  159. forge/core/llm/protocols.py +80 -0
  160. forge/core/llm/types.py +176 -0
  161. forge/core/logging.py +146 -0
  162. forge/core/models/__init__.py +91 -0
  163. forge/core/models/catalog.py +467 -0
  164. forge/core/models/pricing.py +165 -0
  165. forge/core/models/types.py +167 -0
  166. forge/core/naming.py +212 -0
  167. forge/core/ops/__init__.py +73 -0
  168. forge/core/ops/context.py +141 -0
  169. forge/core/ops/gc.py +802 -0
  170. forge/core/ops/proxy.py +146 -0
  171. forge/core/ops/resolution.py +135 -0
  172. forge/core/ops/session.py +344 -0
  173. forge/core/ops/session_context.py +548 -0
  174. forge/core/paths.py +38 -0
  175. forge/core/process.py +54 -0
  176. forge/core/reactive/__init__.py +38 -0
  177. forge/core/reactive/cost_tracking.py +300 -0
  178. forge/core/reactive/env.py +180 -0
  179. forge/core/reactive/proxy.py +78 -0
  180. forge/core/reactive/routing.py +622 -0
  181. forge/core/reactive/session_runner.py +185 -0
  182. forge/core/reactive/structured_output.py +62 -0
  183. forge/core/reactive/tagger.py +94 -0
  184. forge/core/reactive/throttle.py +132 -0
  185. forge/core/state/__init__.py +59 -0
  186. forge/core/state/exceptions.py +59 -0
  187. forge/core/state/io.py +140 -0
  188. forge/core/state/lock.py +99 -0
  189. forge/core/state/timestamps.py +60 -0
  190. forge/core/transcript.py +78 -0
  191. forge/core/typing_helpers.py +24 -0
  192. forge/core/workqueue/__init__.py +67 -0
  193. forge/core/workqueue/queue.py +552 -0
  194. forge/core/workqueue/types.py +63 -0
  195. forge/guard/__init__.py +26 -0
  196. forge/guard/deterministic/__init__.py +26 -0
  197. forge/guard/deterministic/base.py +158 -0
  198. forge/guard/deterministic/coding_standards.py +256 -0
  199. forge/guard/deterministic/registry.py +148 -0
  200. forge/guard/deterministic/tdd.py +171 -0
  201. forge/guard/engine.py +216 -0
  202. forge/guard/protocols.py +91 -0
  203. forge/guard/queries.py +96 -0
  204. forge/guard/semantic/__init__.py +34 -0
  205. forge/guard/semantic/promotion.py +18 -0
  206. forge/guard/semantic/supervisor.py +813 -0
  207. forge/guard/semantic/verdict.py +183 -0
  208. forge/guard/store.py +124 -0
  209. forge/guard/team/__init__.py +6 -0
  210. forge/guard/team/config.py +24 -0
  211. forge/guard/team/handlers.py +209 -0
  212. forge/guard/team/prompts.py +41 -0
  213. forge/guard/types.py +125 -0
  214. forge/guard/workflow/__init__.py +17 -0
  215. forge/guard/workflow/branches.py +67 -0
  216. forge/guard/workflow/config.py +63 -0
  217. forge/guard/workflow/divergence.py +113 -0
  218. forge/guard/workflow/policy.py +87 -0
  219. forge/guard/workflow/stages.py +205 -0
  220. forge/install/__init__.py +55 -0
  221. forge/install/cli.py +281 -0
  222. forge/install/exceptions.py +163 -0
  223. forge/install/hooks.py +109 -0
  224. forge/install/installer.py +1037 -0
  225. forge/install/models.py +321 -0
  226. forge/install/preset.py +272 -0
  227. forge/install/settings_merge.py +831 -0
  228. forge/install/tracking.py +238 -0
  229. forge/install/version.py +141 -0
  230. forge/proxy/__init__.py +0 -0
  231. forge/proxy/base_client.py +181 -0
  232. forge/proxy/client_adapter.py +476 -0
  233. forge/proxy/client_factory.py +531 -0
  234. forge/proxy/converters.py +1206 -0
  235. forge/proxy/cost_logger.py +132 -0
  236. forge/proxy/cost_tracker.py +242 -0
  237. forge/proxy/data_models.py +338 -0
  238. forge/proxy/error_hints.py +92 -0
  239. forge/proxy/metrics.py +222 -0
  240. forge/proxy/model_spec.py +158 -0
  241. forge/proxy/proxies.py +333 -0
  242. forge/proxy/proxy_identity.py +134 -0
  243. forge/proxy/proxy_orchestrator.py +1018 -0
  244. forge/proxy/proxy_startup.py +54 -0
  245. forge/proxy/server.py +1561 -0
  246. forge/proxy/utils.py +537 -0
  247. forge/review/__init__.py +6 -0
  248. forge/review/adversarial.py +111 -0
  249. forge/review/consensus.py +236 -0
  250. forge/review/engine.py +356 -0
  251. forge/review/models.py +437 -0
  252. forge/review/resources/__init__.py +5 -0
  253. forge/review/resources/codereview-performance.md +85 -0
  254. forge/review/resources/codereview-quick.md +75 -0
  255. forge/review/resources/codereview-security.md +92 -0
  256. forge/review/resources/codereview.md +85 -0
  257. forge/review/resources/docreview-quick.md +75 -0
  258. forge/review/resources/docreview.md +86 -0
  259. forge/review/resources/thinkdeep.md +89 -0
  260. forge/review/routing.py +368 -0
  261. forge/review/synthesis.py +73 -0
  262. forge/runtime_config.py +438 -0
  263. forge/search/__init__.py +55 -0
  264. forge/search/bm25_store.py +264 -0
  265. forge/search/content_store.py +197 -0
  266. forge/search/engine.py +352 -0
  267. forge/search/exceptions.py +51 -0
  268. forge/search/extractor.py +234 -0
  269. forge/search/index_state.py +295 -0
  270. forge/search/store.py +215 -0
  271. forge/search/tokenizer.py +24 -0
  272. forge/session/__init__.py +130 -0
  273. forge/session/active.py +339 -0
  274. forge/session/artifacts.py +202 -0
  275. forge/session/claude/__init__.py +50 -0
  276. forge/session/claude/cleanup.py +105 -0
  277. forge/session/claude/invoke.py +236 -0
  278. forge/session/claude/paths.py +200 -0
  279. forge/session/cleanup.py +216 -0
  280. forge/session/config.py +34 -0
  281. forge/session/direct_model.py +107 -0
  282. forge/session/effective.py +169 -0
  283. forge/session/exceptions.py +255 -0
  284. forge/session/handoff.py +881 -0
  285. forge/session/handoff_agent.py +544 -0
  286. forge/session/hooks/__init__.py +35 -0
  287. forge/session/hooks/models.py +73 -0
  288. forge/session/hooks/session_start.py +507 -0
  289. forge/session/identity.py +84 -0
  290. forge/session/index.py +553 -0
  291. forge/session/manager.py +1506 -0
  292. forge/session/models.py +572 -0
  293. forge/session/overrides.py +344 -0
  294. forge/session/plan_resolution.py +286 -0
  295. forge/session/prev_sessions.py +128 -0
  296. forge/session/store.py +431 -0
  297. forge/session/validation.py +47 -0
  298. forge/session/worktree/__init__.py +65 -0
  299. forge/session/worktree/cleanup.py +262 -0
  300. forge/session/worktree/config_copy.py +203 -0
  301. forge/session/worktree/create.py +332 -0
  302. forge/sidecar/__init__.py +29 -0
  303. forge/sidecar/container.py +161 -0
  304. forge/sidecar/docker.py +86 -0
  305. forge/sidecar/secrets.py +19 -0
  306. multi_forge-0.2.0.dist-info/METADATA +242 -0
  307. multi_forge-0.2.0.dist-info/RECORD +311 -0
  308. multi_forge-0.2.0.dist-info/WHEEL +4 -0
  309. multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
  310. multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
  311. multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,750 @@
1
+ """Session fork command.
2
+
3
+ Extracted from session_lifecycle.py for file-size compliance.
4
+ Re-exported via session.py so patch("forge.cli.session.fork") works.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ import uuid as _uuid
11
+ from pathlib import Path
12
+
13
+ import click
14
+
15
+ from forge.cli.session_addendum import (
16
+ resolve_addendum_content_for_proxy,
17
+ write_managed_addendum,
18
+ )
19
+ from forge.core.paths import display_path
20
+ from forge.session import (
21
+ LAUNCH_MODE_HOST,
22
+ ForgeSessionError,
23
+ SessionState,
24
+ )
25
+ from forge.session.direct_model import (
26
+ apply_direct_model_env,
27
+ )
28
+ from forge.session.exceptions import (
29
+ BranchExistsError,
30
+ BranchInUseError,
31
+ BranchNotMergedError,
32
+ CannotForkIncognitoError,
33
+ InvalidBranchNameError,
34
+ SessionNotFoundError,
35
+ WorktreePathExistsError,
36
+ )
37
+
38
+
39
+ def _sess(): # type: ignore[return]
40
+ return sys.modules["forge.cli.session"]
41
+
42
+
43
+ from forge.cli.session import ( # noqa: E402
44
+ ResolvedRouting,
45
+ _apply_routing_override_to_state,
46
+ _combine_prompt_files,
47
+ _get_effective_proxy_for_session,
48
+ _get_launch_preferences,
49
+ _get_runtime_base_url,
50
+ _handle_error,
51
+ _hint_cross_project_session,
52
+ _persist_routing_override,
53
+ _print_routing_summary,
54
+ _resolve_session_artifact_root,
55
+ _resolve_worktree_extension_root,
56
+ console,
57
+ logger,
58
+ )
59
+ from forge.cli.session_lifecycle import ( # noqa: E402
60
+ _launch_claude_for_session,
61
+ _persist_fork_handoff_derivation,
62
+ _print_branch_exists_tip,
63
+ _print_post_exit_tip,
64
+ _resolve_manifest_prompt_file,
65
+ _resume_tip_command,
66
+ session,
67
+ )
68
+
69
+ __all__ = ["fork"]
70
+
71
+
72
+ @session.command()
73
+ @click.argument("parent")
74
+ @click.option(
75
+ "--name",
76
+ "-n",
77
+ default=None,
78
+ help="Name for the fork (auto-generated if not provided)",
79
+ )
80
+ @click.option(
81
+ "--proxy",
82
+ "proxy_name",
83
+ type=str,
84
+ default=None,
85
+ help="Proxy to use (proxy_id or template name)",
86
+ )
87
+ @click.option(
88
+ "--no-proxy",
89
+ "direct",
90
+ is_flag=True,
91
+ help="Bypass the proxy and talk to Anthropic directly",
92
+ )
93
+ @click.option("--incognito", "-i", is_flag=True, help="Auto-delete fork on exit")
94
+ @click.option("--worktree", "-w", is_flag=True, help="Create git worktree for fork isolation")
95
+ @click.option("--branch", "-b", help="Override branch name (implies --worktree)")
96
+ @click.option("--no-launch", is_flag=True, help="Create fork without launching Claude")
97
+ @click.option(
98
+ "--extensions/--no-extensions",
99
+ default=None,
100
+ help="Auto-install extensions in worktree (default: inherit from parent)",
101
+ )
102
+ @click.option(
103
+ "--strategy",
104
+ type=click.Choice(["minimal", "structured", "full", "ai-curated"]),
105
+ default="structured",
106
+ help="Context assembly strategy for worktree forks (default: structured)",
107
+ )
108
+ @click.option(
109
+ "--inline-plan",
110
+ is_flag=True,
111
+ default=False,
112
+ help="Inline the approved plan content in handoff context",
113
+ )
114
+ @click.option(
115
+ "--into",
116
+ "into_path",
117
+ type=click.Path(exists=True),
118
+ default=None,
119
+ help="Fork into an existing non-main worktree directory",
120
+ )
121
+ @click.option(
122
+ "--supervise",
123
+ "supervise_target",
124
+ is_flag=True,
125
+ default=False,
126
+ help="Set parent as plan supervisor for the fork (enables policy enforcement)",
127
+ )
128
+ @click.option(
129
+ "--supervisor-proxy",
130
+ type=str,
131
+ default=None,
132
+ help="Proxy for supervisor routing (requires --supervise)",
133
+ )
134
+ @click.option(
135
+ "--no-supervisor-proxy",
136
+ "supervisor_direct",
137
+ is_flag=True,
138
+ default=False,
139
+ help="Force supervisor to use direct Anthropic routing (requires --supervise)",
140
+ )
141
+ @click.option(
142
+ "--force",
143
+ "-f",
144
+ is_flag=True,
145
+ help="Replace existing branch/worktree and skip budget preflight",
146
+ )
147
+ def fork(
148
+ parent: str,
149
+ name: str | None,
150
+ proxy_name: str | None,
151
+ direct: bool,
152
+ incognito: bool,
153
+ worktree: bool,
154
+ branch: str | None,
155
+ no_launch: bool,
156
+ extensions: bool | None,
157
+ strategy: str,
158
+ inline_plan: bool,
159
+ into_path: str | None,
160
+ supervise_target: bool,
161
+ supervisor_proxy: str | None,
162
+ supervisor_direct: bool,
163
+ force: bool,
164
+ ) -> None:
165
+ """Fork an existing session.
166
+
167
+ By default the fork shares the parent's directory so Claude's
168
+ conversation carries over via --fork-session. Use --worktree for
169
+ code isolation in a separate git worktree, or --into for an existing
170
+ non-main worktree.
171
+
172
+ Use --no-proxy to bypass the proxy, or --proxy to route through
173
+ a specific proxy instead of the parent's.
174
+
175
+ \b
176
+ Examples:
177
+ forge session fork parent-session # Fork, same directory
178
+ forge session fork parent-session --worktree # Fork with worktree
179
+ forge session fork parent-session -n child-session # Custom fork name
180
+ forge session fork parent-session --no-proxy # Fork, bypass proxy
181
+ """
182
+ if direct and proxy_name:
183
+ console.print("[red]Error:[/red] --no-proxy and --proxy are mutually exclusive")
184
+ sys.exit(1)
185
+ if supervisor_proxy and supervisor_direct:
186
+ console.print("[red]Error:[/red] --supervisor-proxy and --no-supervisor-proxy are mutually exclusive")
187
+ sys.exit(1)
188
+ if (supervisor_proxy or supervisor_direct) and not supervise_target:
189
+ console.print("[red]Error:[/red] --supervisor-proxy/--no-supervisor-proxy require --supervise")
190
+ sys.exit(1)
191
+
192
+ if branch:
193
+ worktree = True
194
+
195
+ # --into validation
196
+ into_resolved: str | None = None
197
+ into_branch: str | None = None
198
+ into_target_common: str | None = None
199
+ if into_path is not None:
200
+ if worktree:
201
+ console.print("[red]Error:[/red] --into and --worktree are mutually exclusive")
202
+ sys.exit(1)
203
+ if branch:
204
+ console.print("[red]Error:[/red] --into and --branch are mutually exclusive")
205
+ sys.exit(1)
206
+
207
+ import subprocess as _sp
208
+
209
+ try:
210
+ into_resolved = _sp.run(
211
+ ["git", "-C", into_path, "rev-parse", "--show-toplevel"],
212
+ capture_output=True,
213
+ text=True,
214
+ check=True,
215
+ ).stdout.strip()
216
+ except _sp.CalledProcessError:
217
+ console.print(f"[red]Error:[/red] '{display_path(into_path)}' is not inside a git repository")
218
+ sys.exit(1)
219
+
220
+ # Resolve git-common-dir for the target (absolute, to avoid .git relative path bug)
221
+ try:
222
+ target_common_raw = _sp.run(
223
+ ["git", "-C", into_resolved, "rev-parse", "--git-common-dir"],
224
+ capture_output=True,
225
+ text=True,
226
+ check=True,
227
+ ).stdout.strip()
228
+ # git returns relative paths from the checkout root; resolve against it
229
+ target_common = str((Path(into_resolved) / target_common_raw).resolve())
230
+ except _sp.CalledProcessError:
231
+ console.print("[red]Error:[/red] Failed to resolve git repository for --into target")
232
+ sys.exit(1)
233
+
234
+ # Store for deferred comparison after parent session is loaded
235
+ into_target_common = target_common
236
+
237
+ # Reject main checkout: the main checkout's --show-toplevel == its own path
238
+ # A real worktree has a different toplevel than the main repo
239
+ try:
240
+ # Use git-common-dir to find the main repo's toplevel
241
+ main_git_dir = _sp.run(
242
+ ["git", "-C", into_resolved, "rev-parse", "--git-common-dir"],
243
+ capture_output=True,
244
+ text=True,
245
+ check=True,
246
+ ).stdout.strip()
247
+ main_git_dir_abs = (Path(into_resolved) / main_git_dir).resolve()
248
+ # Main repo root is the parent of the .git directory
249
+ main_repo_root = main_git_dir_abs.parent if main_git_dir_abs.name == ".git" else main_git_dir_abs
250
+ if Path(into_resolved).resolve() == main_repo_root:
251
+ console.print(
252
+ "[red]Error:[/red] --into targets existing worktrees, not the main checkout. "
253
+ "Use a same-directory fork instead."
254
+ )
255
+ sys.exit(1)
256
+ except _sp.CalledProcessError:
257
+ pass # Can't determine; allow
258
+
259
+ try:
260
+ into_branch = _sp.run(
261
+ ["git", "-C", into_resolved, "rev-parse", "--abbrev-ref", "HEAD"],
262
+ capture_output=True,
263
+ text=True,
264
+ check=True,
265
+ ).stdout.strip()
266
+ except _sp.CalledProcessError:
267
+ into_branch = None
268
+
269
+ # CWD validation (skip for --into, which has its own path resolution)
270
+ if into_path is None:
271
+ from forge.cli.guards import require_main_repo_root, require_repo_root
272
+
273
+ if worktree:
274
+ require_main_repo_root()
275
+ else:
276
+ require_repo_root()
277
+
278
+ ctx = click.get_current_context()
279
+ _strategy_explicit = ctx.get_parameter_source("strategy") == click.core.ParameterSource.COMMANDLINE
280
+ _inline_plan_explicit = ctx.get_parameter_source("inline_plan") == click.core.ParameterSource.COMMANDLINE
281
+
282
+ manager = _sess().SessionManager()
283
+ _fr = _sess()._cwd_forge_root()
284
+
285
+ # --into cross-repo preflight: reject before fork_session() to avoid orphaned sessions
286
+ if into_resolved is not None and into_target_common is not None:
287
+ import subprocess as _sp2
288
+
289
+ try:
290
+ parent_state_pre = manager.get_session(parent, forge_root=_fr)
291
+ parent_wt_pre = parent_state_pre.worktree.path if parent_state_pre.worktree else None
292
+ if parent_wt_pre:
293
+ parent_common_raw = _sp2.run(
294
+ ["git", "-C", parent_wt_pre, "rev-parse", "--git-common-dir"],
295
+ capture_output=True,
296
+ text=True,
297
+ check=True,
298
+ ).stdout.strip()
299
+ parent_common = str((Path(parent_wt_pre) / parent_common_raw).resolve())
300
+ if into_target_common != parent_common:
301
+ console.print(
302
+ "[red]Error:[/red] --into target is not part of the same repository as the parent session"
303
+ )
304
+ sys.exit(1)
305
+ except _sp2.CalledProcessError:
306
+ pass # Can't resolve parent repo; allow
307
+ except ForgeSessionError:
308
+ pass # Parent not found; fork_session() will raise the right error
309
+
310
+ # Budget preflight for --strategy full (before fork_session to avoid orphaned sessions/worktrees)
311
+ # Use the child's effective routing: --no-proxy means no proxy, --proxy overrides parent
312
+ is_cross_dir = worktree or into_resolved is not None
313
+ # Resolve --proxy early for preflight (reuses routing resolved later for launch)
314
+ _preflight_routing: ResolvedRouting | None = None
315
+ if proxy_name:
316
+ _preflight_routing = _sess()._resolve_routing_from_cli(proxy_name=proxy_name, direct=False)
317
+ if is_cross_dir and strategy == "full" and not direct:
318
+ try:
319
+ from forge.session.artifacts import resolve_artifact_path
320
+
321
+ parent_state = manager.get_session(parent, forge_root=_fr)
322
+ # --proxy override > parent's proxy for budget check
323
+ if _preflight_routing:
324
+ preflight_ref = _preflight_routing.proxy_id
325
+ else:
326
+ child_template = parent_state.intent.proxy.template if parent_state.intent.proxy else None
327
+ preflight_ref = child_template
328
+ context_limit_preflight = _sess()._resolve_context_limit(preflight_ref)
329
+ if context_limit_preflight is not None:
330
+ from forge.session.handoff import estimate_transcript_tokens
331
+
332
+ artifact_root = _resolve_session_artifact_root(manager=manager, state=parent_state)
333
+ transcripts = parent_state.confirmed.artifacts.get("transcripts", [])
334
+ if transcripts and isinstance(transcripts, list):
335
+ latest = transcripts[-1]
336
+ if isinstance(latest, dict):
337
+ copied_path = latest.get("copied_path")
338
+ if isinstance(copied_path, str):
339
+ transcript_path = resolve_artifact_path(artifact_root, copied_path)
340
+ if transcript_path is not None and transcript_path.is_file():
341
+ token_est = estimate_transcript_tokens(transcript_path)
342
+ if token_est > context_limit_preflight:
343
+ if force:
344
+ console.print(
345
+ f"[yellow]Warning:[/yellow] Parent transcript ({token_est:,} tokens) "
346
+ f"exceeds context limit ({context_limit_preflight:,}). "
347
+ "Proceeding anyway (--force)."
348
+ )
349
+ else:
350
+ console.print(
351
+ f"[red]Error:[/red] Parent transcript ({token_est:,} tokens) exceeds "
352
+ f"context limit ({context_limit_preflight:,})."
353
+ )
354
+ console.print(
355
+ "[dim]Tip: Use --strategy structured or --strategy ai-curated instead.[/dim]"
356
+ )
357
+ sys.exit(1)
358
+ except ForgeSessionError:
359
+ pass # Parent not found; fork_session() will raise the right error
360
+
361
+ # Preflight supervisor proxy BEFORE fork_session() to avoid half-created state
362
+ if supervisor_proxy:
363
+ from forge.guard.semantic.supervisor import preflight_supervisor_proxy
364
+
365
+ try:
366
+ supervisor_proxy = preflight_supervisor_proxy(supervisor_proxy)
367
+ except ValueError as e:
368
+ console.print(f"[red]Error:[/red] {e}")
369
+ sys.exit(1)
370
+
371
+ try:
372
+ parent_manifest, fork_manifest = manager.fork_session(
373
+ parent_name=parent,
374
+ fork_name=name,
375
+ direct=direct,
376
+ is_incognito=incognito,
377
+ create_worktree=worktree,
378
+ branch=into_branch if into_resolved else branch,
379
+ into_path=into_resolved,
380
+ forge_root=_fr,
381
+ force=force,
382
+ )
383
+ except CannotForkIncognitoError as e:
384
+ console.print(f"[red]Error:[/red] {e}")
385
+ console.print("\n[dim]Tip: Incognito sessions cannot be forked.[/dim]")
386
+ sys.exit(1)
387
+ except BranchExistsError as e:
388
+ _print_branch_exists_tip(e)
389
+ sys.exit(1)
390
+ except BranchInUseError as e:
391
+ console.print(f"[red]Error:[/red] {e}")
392
+ console.print("\n[dim]Tip: The branch is checked out in another worktree. Remove that worktree first.[/dim]")
393
+ sys.exit(1)
394
+ except BranchNotMergedError as e:
395
+ console.print(f"[red]Error:[/red] {e}")
396
+ console.print("\n[dim]Tip: Merge or delete the branch manually before using --force.[/dim]")
397
+ sys.exit(1)
398
+ except WorktreePathExistsError as e:
399
+ console.print(f"[red]Error:[/red] {e}")
400
+ console.print("\n[dim]Tip: Remove the directory or use a different fork name.[/dim]")
401
+ sys.exit(1)
402
+ except InvalidBranchNameError as e:
403
+ console.print(f"[red]Error:[/red] {e}")
404
+ sys.exit(1)
405
+ except SessionNotFoundError:
406
+ if not _hint_cross_project_session(parent, _fr):
407
+ console.print(f"[red]Error:[/red] session '{parent}' not found")
408
+ sys.exit(1)
409
+ except ForgeSessionError as e:
410
+ _handle_error(e)
411
+ return
412
+
413
+ # Persist routing override to manifest (ensures --no-launch retains proxy choice)
414
+ fork_worktree_path = Path(fork_manifest.worktree.path) if fork_manifest.worktree else Path.cwd()
415
+ _persist_routing_override(
416
+ forge_root=Path(fork_manifest.forge_root) if fork_manifest.forge_root else fork_worktree_path,
417
+ session_name=fork_manifest.name,
418
+ routing=_preflight_routing,
419
+ direct=direct,
420
+ )
421
+ _apply_routing_override_to_state(state=fork_manifest, routing=_preflight_routing, direct=direct)
422
+
423
+ # --- wire supervisor (if --supervise flag set) ---
424
+ if supervise_target:
425
+ from forge.guard.semantic.supervisor import (
426
+ apply_supervisor_routing,
427
+ apply_supervisor_to_intent,
428
+ )
429
+ from forge.session.models import SupervisorConfig
430
+ from forge.session.store import SessionStore
431
+
432
+ fork_forge_root = fork_manifest.forge_root or str(fork_worktree_path)
433
+ sup_config = SupervisorConfig(
434
+ resume_id=parent,
435
+ forge_root=parent_manifest.forge_root or fork_forge_root,
436
+ )
437
+ apply_supervisor_routing(
438
+ sup_config,
439
+ parent_manifest,
440
+ supervisor_proxy=supervisor_proxy,
441
+ supervisor_direct=supervisor_direct,
442
+ current_proxy_id=_preflight_routing.proxy_id if _preflight_routing else None,
443
+ current_template=_preflight_routing.template if _preflight_routing else None,
444
+ current_direct=direct,
445
+ )
446
+ fork_store = SessionStore(fork_forge_root, fork_manifest.name)
447
+ fork_store.update(timeout_s=5.0, mutate=lambda m: apply_supervisor_to_intent(m, sup_config))
448
+ fork_manifest = fork_store.read()
449
+
450
+ if _preflight_routing:
451
+ effective_template = _preflight_routing.template
452
+ effective_url = _preflight_routing.base_url
453
+ effective_proxy_id = _preflight_routing.proxy_id
454
+ elif proxy_name:
455
+ routing = _sess()._resolve_routing_from_cli(proxy_name=proxy_name, direct=False)
456
+ effective_template = routing.template
457
+ effective_url = routing.base_url
458
+ effective_proxy_id = routing.proxy_id
459
+ else:
460
+ effective_template, effective_url, effective_proxy_id = _get_effective_proxy_for_session(fork_manifest)
461
+
462
+ # Compute context limit (uses exact proxy_id when available for deterministic result)
463
+ context_limit = _sess()._resolve_context_limit(effective_proxy_id or effective_template)
464
+
465
+ console.print(f"Forked [cyan]{parent}[/cyan] -> [green]{fork_manifest.name}[/green]")
466
+ _print_routing_summary(template=effective_template, base_url=effective_url)
467
+ if fork_manifest.worktree and fork_manifest.worktree.is_worktree:
468
+ console.print(f" Worktree: {display_path(fork_manifest.worktree.path)}")
469
+ console.print(f" Branch: {fork_manifest.worktree.branch}")
470
+ if supervise_target:
471
+ console.print(f" Supervisor: {parent}")
472
+ if incognito:
473
+ console.print("[yellow] (will auto-delete on exit)[/yellow]")
474
+ console.print()
475
+
476
+ parent_session_id = parent_manifest.confirmed.claude_session_id
477
+ if not parent_session_id:
478
+ console.print("[red]Error:[/red] Parent session has no UUID")
479
+ console.print("The parent session may not have been started yet.")
480
+ sys.exit(1)
481
+
482
+ use_sidecar, mounts, image = _get_launch_preferences(fork_manifest)
483
+
484
+ # Set env vars for fork registration (hook uses FORGE_FORK_NAME for fork detection)
485
+ env_vars, unset_env_vars = _sess()._build_session_env(
486
+ session_name=fork_manifest.name,
487
+ context_limit=context_limit,
488
+ template=effective_template,
489
+ base_url=effective_url,
490
+ fork_name=fork_manifest.name,
491
+ parent_session=parent,
492
+ forge_root=fork_manifest.forge_root,
493
+ subprocess_proxy=fork_manifest.intent.subprocess_proxy,
494
+ sidecar=use_sidecar,
495
+ )
496
+ fork_name = fork_manifest.name # Capture for cleanup
497
+ is_worktree_fork = bool(fork_manifest.worktree and fork_manifest.worktree.is_worktree)
498
+ if effective_url is None:
499
+ from forge.runtime_config import get_default_direct_model
500
+
501
+ fork_direct_model = fork_manifest.intent.launch.direct_model if fork_manifest.intent.launch else None
502
+ fork_direct_model = fork_direct_model or get_default_direct_model()
503
+ error = apply_direct_model_env(env_vars, fork_direct_model)
504
+ if error:
505
+ console.print(f"[red]Error:[/red] {error}")
506
+ sys.exit(1)
507
+
508
+ # Warn about --strategy/--inline-plan on same-directory forks (only if user explicitly set them)
509
+ if not is_worktree_fork and (_strategy_explicit or _inline_plan_explicit):
510
+ console.print(
511
+ "[dim]Tip: --strategy/--inline-plan only apply to worktree forks "
512
+ "(ignored for same-directory forks).[/dim]"
513
+ )
514
+
515
+ # Worktree forks: Claude Code stores sessions at ~/.claude/projects/<encoded-cwd>/,
516
+ # so --resume --fork-session cannot find the parent's conversation from a different
517
+ # directory. Tested 2026-04-02 with Claude Code 2.1.90: all cross-CWD scenarios fail
518
+ # with "No conversation found." See scripts/experiments/native-resume/.
519
+ # Use handoff (assembled context via --append-system-prompt-file) instead.
520
+ if is_worktree_fork:
521
+ worktree_path = Path(fork_manifest.worktree.path) # type: ignore[union-attr]
522
+ fork_context, prompt_warnings = _sess()._generate_parent_handoff_context(
523
+ manager=manager,
524
+ manifest=fork_manifest,
525
+ parent_state=parent_manifest,
526
+ strategy=strategy,
527
+ inline_plan=inline_plan,
528
+ )
529
+ prompt_files: list[Path] = []
530
+ if fork_context is not None:
531
+ prompt_files.append(fork_context)
532
+ configured_prompt = _resolve_manifest_prompt_file(fork_manifest)
533
+ if configured_prompt is not None:
534
+ prompt_files.append(configured_prompt)
535
+ prompt_file = _combine_prompt_files(
536
+ worktree_path=worktree_path,
537
+ session_name=fork_manifest.name,
538
+ prompt_files=prompt_files,
539
+ )
540
+ if prompt_file:
541
+ prompt_path = Path(prompt_file)
542
+ try:
543
+ console.print(f" Context: {prompt_path.relative_to(worktree_path)}")
544
+ except ValueError:
545
+ console.print(f" Context: {display_path(prompt_path)}")
546
+ for warning in prompt_warnings:
547
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
548
+
549
+ try:
550
+ fork_manifest = _persist_fork_handoff_derivation(
551
+ manifest=fork_manifest,
552
+ strategy=strategy,
553
+ context_path=fork_context,
554
+ )
555
+ except Exception:
556
+ logger.warning("Failed to persist fork derivation handoff details", exc_info=True)
557
+
558
+ _fork_uuid = str(_uuid.uuid4())
559
+ try:
560
+ from forge.session import SessionStore as _ForkStore
561
+
562
+ _fork_wt = Path(fork_manifest.worktree.path) if fork_manifest.worktree else Path.cwd()
563
+ _fork_store_root = Path(fork_manifest.forge_root) if fork_manifest.forge_root else _fork_wt
564
+ _fork_store = _ForkStore(str(_fork_store_root), fork_manifest.name)
565
+ from forge.session.claude.paths import (
566
+ resolve_claude_project_root as _resolve_fork_root_preseed,
567
+ )
568
+
569
+ _fork_cwd_preseed = _resolve_fork_root_preseed(fork_manifest)
570
+
571
+ def _preseed_mutate(m: SessionState) -> None:
572
+ m.confirmed.claude_session_id = _fork_uuid
573
+ m.confirmed.claude_project_root = _fork_cwd_preseed
574
+
575
+ _fork_store.update(timeout_s=5.0, mutate=_preseed_mutate)
576
+ manager.index_store.sync_uuid_from_state(fork_manifest.name, _fork_store.read())
577
+ except Exception:
578
+ logger.debug("Pre-seed UUID write failed (hook will reconcile)", exc_info=True)
579
+
580
+ from forge.session.claude.paths import (
581
+ resolve_claude_project_root as _resolve_fork_root,
582
+ )
583
+
584
+ _fork_cwd = _resolve_fork_root(fork_manifest)
585
+
586
+ _wt_addendum = resolve_addendum_content_for_proxy(effective_proxy_id)
587
+ _wt_prompt = prompt_file
588
+ if _wt_addendum:
589
+ _wt_forge_root = Path(fork_manifest.forge_root) if fork_manifest.forge_root else Path.cwd()
590
+ _wt_addendum_path = write_managed_addendum(_wt_forge_root, fork_manifest.name, _wt_addendum)
591
+ _wt_files: list[Path] = [_wt_addendum_path]
592
+ if _wt_prompt:
593
+ _wt_files.append(Path(_wt_prompt))
594
+ _wt_prompt = _combine_prompt_files(
595
+ worktree_path=worktree_path,
596
+ session_name=fork_manifest.name,
597
+ prompt_files=_wt_files,
598
+ )
599
+
600
+ def _invoke_fork() -> int:
601
+ return _sess().invoke_claude(
602
+ session_id=_fork_uuid,
603
+ name=fork_manifest.name,
604
+ model=None,
605
+ system_prompt_file=_wt_prompt,
606
+ env_vars=env_vars,
607
+ unset_env_vars=unset_env_vars,
608
+ cwd=_fork_cwd,
609
+ )
610
+
611
+ # Same-directory forks: --resume --fork-session works natively.
612
+ else:
613
+ from forge.session.claude.paths import (
614
+ resolve_claude_project_root as _resolve_fork_root,
615
+ )
616
+
617
+ _fork_cwd = _resolve_fork_root(fork_manifest)
618
+ _samedir_addendum = resolve_addendum_content_for_proxy(effective_proxy_id)
619
+ _samedir_prompt: str | None = None
620
+ if _samedir_addendum:
621
+ _samedir_forge_root = Path(fork_manifest.forge_root) if fork_manifest.forge_root else Path.cwd()
622
+ _samedir_prompt = str(write_managed_addendum(_samedir_forge_root, fork_manifest.name, _samedir_addendum))
623
+
624
+ def _invoke_fork() -> int:
625
+ return _sess().invoke_claude(
626
+ resume_id=parent_session_id,
627
+ fork_session=True,
628
+ name=fork_manifest.name,
629
+ model=None,
630
+ system_prompt_file=_samedir_prompt,
631
+ env_vars=env_vars,
632
+ unset_env_vars=unset_env_vars,
633
+ cwd=_fork_cwd,
634
+ )
635
+
636
+ # Auto-install extensions in worktree forks (before no_launch check so --no-launch still prepares the worktree)
637
+ if is_worktree_fork:
638
+ extension_root = _resolve_worktree_extension_root(fork_manifest)
639
+ # For --into, skip if the target already has a local Forge install
640
+ _skip_extensions = False
641
+ if into_resolved is not None and extension_root is not None:
642
+ try:
643
+ from forge.install.tracking import TrackingStore as _TSCheck
644
+
645
+ if _TSCheck().get_installation("local", str(extension_root)) is not None:
646
+ _skip_extensions = True
647
+ logger.debug("Skipping auto-install: target worktree has existing local install")
648
+ except Exception:
649
+ pass
650
+
651
+ if not _skip_extensions and extension_root is not None:
652
+ # Use forge_root (where .claude/ and .forge/ live), not checkout_root.
653
+ # The tracking store keys by forge_root, so get_repo_root() misses when
654
+ # forge_root != checkout_root (e.g., nested .claude/ in a subdirectory).
655
+ _parent_forge_root = Path(
656
+ parent_manifest.forge_root
657
+ or (parent_manifest.worktree.path if parent_manifest.worktree else str(Path.cwd()))
658
+ )
659
+ _sess()._auto_install_extensions(
660
+ install_root=extension_root,
661
+ parent_project_root=_parent_forge_root,
662
+ force_extensions=extensions,
663
+ )
664
+ elif extensions is True:
665
+ console.print("[dim]Tip: --extensions only applies with --worktree.[/dim]")
666
+
667
+ if no_launch:
668
+ console.print("[dim]Fork created (--no-launch: Claude not started)[/dim]")
669
+ if is_worktree_fork:
670
+ console.print(f"\n[dim]Tip: {_resume_tip_command(fork_manifest)}[/dim]")
671
+ sys.exit(0)
672
+
673
+ runtime_base_url = _get_runtime_base_url(use_sidecar=use_sidecar, effective_url=effective_url)
674
+
675
+ if use_sidecar:
676
+ exit_code = 0
677
+ try:
678
+ exit_code = _launch_claude_for_session(
679
+ manifest=fork_manifest,
680
+ session_id=_fork_uuid if is_worktree_fork else None,
681
+ resume_id=None if is_worktree_fork else parent_session_id,
682
+ effective_template=effective_template,
683
+ runtime_base_url=runtime_base_url,
684
+ context_limit=context_limit,
685
+ use_sidecar=True,
686
+ mounts=mounts,
687
+ image=image,
688
+ fork_session=not is_worktree_fork,
689
+ register_fork=is_worktree_fork,
690
+ system_prompt_file=prompt_file if is_worktree_fork else None,
691
+ name=fork_manifest.name,
692
+ proxy_id=effective_proxy_id,
693
+ )
694
+ finally:
695
+ if incognito:
696
+ console.print(f"\n[dim]Cleaning up incognito fork '{fork_name}'...[/dim]")
697
+ try:
698
+ manager.delete_session(
699
+ fork_name,
700
+ delete_transcripts=True,
701
+ force=True,
702
+ forge_root=fork_manifest.forge_root,
703
+ )
704
+ console.print("[green]Cleanup complete.[/green]")
705
+ except ForgeSessionError as e:
706
+ console.print(f"[yellow]Cleanup warning:[/yellow] {e}")
707
+ sys.exit(exit_code)
708
+
709
+ fork_worktree = Path(fork_manifest.worktree.path) if fork_manifest.worktree else Path.cwd()
710
+ # Check hooks from forge_root (where .claude/ lives), not checkout root
711
+ _fork_forge_root = Path(fork_manifest.forge_root) if fork_manifest.forge_root else fork_worktree
712
+ _sess()._warn_if_hooks_missing(_fork_forge_root)
713
+ _sess()._warn_if_version_outdated()
714
+ active_claude_session_id = _fork_uuid if is_worktree_fork else None
715
+
716
+ if incognito:
717
+ exit_code = 0
718
+ try:
719
+ exit_code = _sess().run_with_active_session(
720
+ session_name=fork_name,
721
+ worktree_path=fork_worktree,
722
+ launch_mode=LAUNCH_MODE_HOST,
723
+ forge_root=fork_manifest.forge_root,
724
+ claude_session_id=active_claude_session_id,
725
+ runner=_invoke_fork,
726
+ )
727
+ finally:
728
+ console.print(f"\n[dim]Cleaning up incognito fork '{fork_name}'...[/dim]")
729
+ try:
730
+ manager.delete_session(
731
+ fork_name,
732
+ delete_transcripts=True,
733
+ force=True,
734
+ forge_root=fork_manifest.forge_root,
735
+ )
736
+ console.print("[green]Cleanup complete.[/green]")
737
+ except ForgeSessionError as e:
738
+ console.print(f"[yellow]Cleanup warning:[/yellow] {e}")
739
+ sys.exit(exit_code)
740
+ else:
741
+ exit_code = _sess().run_with_active_session(
742
+ session_name=fork_name,
743
+ worktree_path=fork_worktree,
744
+ launch_mode=LAUNCH_MODE_HOST,
745
+ forge_root=fork_manifest.forge_root,
746
+ claude_session_id=active_claude_session_id,
747
+ runner=_invoke_fork,
748
+ )
749
+ _print_post_exit_tip(fork_manifest)
750
+ sys.exit(exit_code)