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,572 @@
1
+ """Dataclasses for Forge Session module.
2
+
3
+ All timestamps are stored as ISO8601 strings for trivial JSON roundtripping.
4
+ Use forge.core.state.now_iso() to generate timestamps and parse_iso()
5
+ for runtime conversion.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ from forge.core.state import now_iso
14
+ from forge.guard.team.config import TeamSupervisorConfig
15
+ from forge.guard.types import FailMode
16
+
17
+ from .config import LAUNCH_MODE_HOST, LAUNCH_MODE_SIDECAR
18
+
19
+ # Schema version for session state files.
20
+ SCHEMA_VERSION = 1
21
+ INDEX_VERSION = 1
22
+
23
+
24
+ # --- Worktree metadata (embedded in SessionState) ---
25
+
26
+
27
+ @dataclass
28
+ class Worktree:
29
+ """Git worktree metadata.
30
+
31
+ ``path`` is the checkout root (git ``--show-toplevel``), not the Forge
32
+ project root. The Forge project root is stored separately as
33
+ ``SessionState.forge_root`` and ``SessionIndexEntry.forge_root``.
34
+ """
35
+
36
+ path: str # Absolute path to checkout root (git --show-toplevel)
37
+ branch: str # Git branch name (may contain slashes)
38
+ is_worktree: bool = False # True only if this is a git worktree (not main repo)
39
+ owns_worktree: bool = True # False for --into (session is a guest, not the creator)
40
+
41
+
42
+ # --- Intent section - what Forge requested ---
43
+
44
+
45
+ @dataclass
46
+ class ProxyIntent:
47
+ """Proxy configuration intent. Both fields are required."""
48
+
49
+ template: str # e.g., "litellm-gemini"
50
+ base_url: str # e.g., "http://localhost:8084"
51
+
52
+
53
+ @dataclass
54
+ class SystemPromptIntent:
55
+ """System prompt configuration intent."""
56
+
57
+ mode: str = "append" # "append" or "replace"
58
+ file: str | None = None # Path to custom system prompt file
59
+
60
+
61
+ @dataclass
62
+ class SidecarLaunchIntent:
63
+ """Persisted sidecar launch preferences for reproducible relaunches."""
64
+
65
+ mounts: list[str] = field(default_factory=list) # Raw CLI mount specs: host:container[:ro|rw]
66
+ image: str | None = None # Optional sidecar image override
67
+
68
+
69
+ @dataclass
70
+ class LaunchIntent:
71
+ """How Forge should relaunch this session."""
72
+
73
+ mode: str = LAUNCH_MODE_HOST # "host" or "sidecar"
74
+ sidecar: SidecarLaunchIntent | None = None
75
+ direct_model: str | None = None # Claude Code env-ready direct model pin (e.g. claude-opus-4-7[1m])
76
+
77
+
78
+ @dataclass
79
+ class HandoffConfig:
80
+ """Handoff agent configuration for automatic memory doc updates.
81
+
82
+ The handoff agent runs after session stop to update designated
83
+ project memory documents (e.g., project-state.md) using ``claude -p``.
84
+
85
+ Fields:
86
+ enabled: Whether the handoff agent should run on session stop.
87
+ mode: "augment" (add missing info) or "review-only" (report only, no edits).
88
+ proxy: Optional proxy (proxy_id or template name) to route the agent's
89
+ LLM calls through. If None, inherits the session's confirmed proxy.
90
+ direct: When True, force direct Anthropic routing regardless of session proxy.
91
+ min_turns: Minimum conversation turns before triggering handoff.
92
+ Sessions below this threshold are skipped (too short to be useful).
93
+ """
94
+
95
+ enabled: bool = False
96
+ mode: str = "augment" # "augment" | "review-only"
97
+ proxy: str | None = None
98
+ direct: bool = False
99
+ min_turns: int = 5
100
+
101
+
102
+ @dataclass
103
+ class DesignatedDoc:
104
+ """A document the handoff agent should update after session stop.
105
+
106
+ Fields:
107
+ path: Worktree-relative path (e.g., "docs/checklist.md").
108
+ Must NOT be absolute. Resolved against worktree_path at runtime.
109
+ strategy: Built-in augmentation strategy:
110
+ "project-state" — handoff notes (skip if missing)
111
+ "checklist" — mark completed tasks, add discovered tasks
112
+ "changelog" — add accomplishments not already recorded
113
+ "debugging" — record error causes, solutions, workarounds
114
+ "patterns" — record architecture patterns and conventions
115
+ "suggested" — propose additions as checkboxes (requires shadows)
116
+ "generic" — read and add missing information (default)
117
+ Unknown values fall back to "generic" behavior.
118
+ shadows: When set, switches to shadow/propose mode (Mode 2).
119
+ Path to the official document this doc proposes changes for.
120
+ The agent reads the official doc first, then writes suggestions
121
+ to this doc's path. Only valid with strategy="suggested".
122
+ """
123
+
124
+ path: str
125
+ strategy: str = "generic"
126
+ shadows: str | None = None
127
+
128
+
129
+ @dataclass
130
+ class MemoryIntent:
131
+ """Memory/context injection intent."""
132
+
133
+ auto_recall: bool = False
134
+ tags: list[str] = field(default_factory=list)
135
+ strategy: str = "summary" # "summary", "full", or "off"
136
+ max_chars: int = 6000
137
+ generated_file: str | None = None # e.g., ".claude/forge.context.generated.md"
138
+ designated_docs: list[DesignatedDoc] = field(default_factory=list)
139
+ auto_update: HandoffConfig | None = None
140
+
141
+
142
+ @dataclass
143
+ class SupervisorConfig:
144
+ """Semantic supervisor configuration.
145
+
146
+ The supervisor is an LLM session (typically forked from the planner) that
147
+ validates executor actions against the approved plan.
148
+ """
149
+
150
+ resume_id: str | None = None # Claude session UUID, or a Forge session name resolved to a UUID at runtime
151
+ proxy: str | None = None # Optional: proxy_id or template name for base_url lookup
152
+ direct: bool = False # When True, force direct Anthropic routing
153
+ base_url: str | None = None # Optional: explicit base_url override
154
+ forge_root: str | None = None # Scope for name-based lookups (set at wiring time)
155
+ timeout_seconds: int = 45 # Max time to wait for supervisor response (15s margin within 60s hook timeout)
156
+ throttle_seconds: int = 30 # Min time between supervisor calls (for caching)
157
+ fork_session: bool = True # Fork supervisor session to avoid polluting planner context
158
+ suspended: bool = False # True = supervision paused, config preserved
159
+ plan_override_path: str | None = None # Absolute path to plan file that supersedes session context
160
+
161
+
162
+ @dataclass
163
+ class PolicyIntent:
164
+ """Policy configuration for the session.
165
+
166
+ Policies are enforced at PreToolUse:Write/Edit boundaries. They can be
167
+ deterministic (fast local checks) or semantic (LLM-based supervisor).
168
+ """
169
+
170
+ enabled: bool = False
171
+ fail_mode: FailMode = "open" # "open" = allow on error, "closed" = deny on error
172
+ bundles: list[str] = field(default_factory=list) # e.g., ["tdd", "coding_standards"]
173
+ bundle_config: dict[str, dict[str, Any]] = field(default_factory=dict) # per-bundle options
174
+ supervisor: SupervisorConfig | None = None
175
+ team_supervisor: TeamSupervisorConfig | None = None
176
+
177
+
178
+ @dataclass
179
+ class VerificationConfig:
180
+ """Verification policy configuration (Ralph-Wiggum pattern).
181
+
182
+ Verification runs at the Stop boundary and can block exit until
183
+ the assistant produces a completion signal.
184
+
185
+ Fields:
186
+ type: Verification type.
187
+ - "completion_promise": Check for promise string in last assistant message.
188
+ - "test_suite": Run `uv run pytest` and check exit code.
189
+ promise: (completion_promise only) The exact string that must appear on a
190
+ standalone line in the last assistant message.
191
+ max_iterations: Maximum number of blocked Stop attempts before auto-bypass.
192
+ max_minutes: Maximum minutes from first block before auto-bypass (None = no limit).
193
+ bypass: If True, skip verification entirely (escape hatch).
194
+ on_incomplete: What to do when verification fails:
195
+ - "block": sys.exit(2) with stderr guidance
196
+ - "warn": print warning, allow Stop
197
+ - "allow": skip verification entirely
198
+ re_inject_prompt: Custom message to print to stderr when blocking.
199
+ If None, a default message is used.
200
+ test_timeout_seconds: (test_suite only) Timeout for pytest command in seconds.
201
+ """
202
+
203
+ type: str = "completion_promise" # "completion_promise" | "test_suite"
204
+ promise: str | None = None
205
+ max_iterations: int = 50
206
+ max_minutes: int | None = None
207
+ bypass: bool = False
208
+ on_incomplete: str = "block" # "block", "warn", "allow"
209
+ re_inject_prompt: str | None = None
210
+ test_timeout_seconds: int = 300 # 5 minutes default (test_suite only)
211
+
212
+
213
+ @dataclass
214
+ class SessionIntent:
215
+ """What Forge intends for this session.
216
+
217
+ NOTE: Proxy-owned routing and LLM hyperparameters are intentionally excluded
218
+ from the session schema. Sessions may express only session-owned intent.
219
+ """
220
+
221
+ agent: str = "claude-code"
222
+ proxy: ProxyIntent | None = None
223
+ subprocess_proxy: str | None = None # proxy_id for routing subprocesses (supervisor, panel, etc.)
224
+ launch: LaunchIntent | None = None
225
+ system_prompt: SystemPromptIntent | None = None
226
+ memory: MemoryIntent | None = None
227
+ policy: PolicyIntent | None = None
228
+ verification: VerificationConfig | None = None
229
+
230
+
231
+ # --- Confirmed section - what Claude Code actually did (filled by hooks) ---
232
+
233
+
234
+ @dataclass
235
+ class PolicyConfirmed:
236
+ """Hook-owned policy state persisted across hook invocations.
237
+
238
+ Since hooks are short-lived processes, stateful policy data must be
239
+ persisted to the session manifest between invocations.
240
+
241
+ Fields:
242
+ forge_version: Version for provenance tracking
243
+ bundles: Active bundle names at last evaluation
244
+ rules_active: Active rule IDs at last evaluation
245
+ decisions: Log of recent policy decisions (bounded to MAX_DECISION_LOG)
246
+ policy_states: Generic per-policy state dict keyed by policy_id
247
+ """
248
+
249
+ forge_version: str | None = None
250
+ bundles: list[str] = field(default_factory=list)
251
+ rules_active: list[str] = field(default_factory=list)
252
+ decisions: list[dict[str, Any]] = field(default_factory=list)
253
+ policy_states: dict[str, dict[str, Any]] = field(default_factory=dict)
254
+
255
+
256
+ @dataclass
257
+ class VerificationConfirmed:
258
+ """Hook-owned verification state persisted across Stop invocations.
259
+
260
+ Tracks runtime verification state for the Ralph-Wiggum feedback loop.
261
+
262
+ Fields:
263
+ started_at: ISO8601 timestamp of first blocked Stop (for max_minutes).
264
+ iterations: Number of times Stop was blocked (not total Stop invocations).
265
+ last_result: Outcome of last verification check:
266
+ - "passed": promise found, Stop allowed
267
+ - "failed": promise not found, Stop blocked
268
+ - "warned": promise not found, Stop allowed (on_incomplete=warn)
269
+ - "max_iterations": limit exceeded, auto-bypassed
270
+ - "max_minutes": time limit exceeded, auto-bypassed
271
+ - "bypassed": manually bypassed via %cancel-verification
272
+ - "error": verification check failed due to internal error
273
+ last_error: Short description of last failure (for debugging).
274
+ """
275
+
276
+ started_at: str | None = None
277
+ iterations: int = 0
278
+ last_result: str | None = None
279
+ last_error: str | None = None
280
+
281
+
282
+ @dataclass
283
+ class CompactionConfirmed:
284
+ """Compaction tracking state persisted across hook invocations.
285
+
286
+ Records compaction events and pre-compact transcript snapshots for
287
+ session metadata, search indexing, and transcript lineage.
288
+ PreCompact captures the full transcript before compaction; PostCompact
289
+ records the completion timestamp.
290
+ """
291
+
292
+ compact_count: int = 0
293
+ last_compact_at: str | None = None # ISO8601, set by PostCompact
294
+ last_compact_type: str | None = None # "auto" | "manual" | "unknown"
295
+ transcript_snapshots: list[dict[str, Any]] = field(default_factory=list)
296
+ # Each entry: {captured_at, reason, source_path, snapshot_path, copied}
297
+
298
+
299
+ @dataclass
300
+ class SubagentConfirmed:
301
+ """Subagent activity tracking persisted across hook invocations.
302
+
303
+ Records subagent stop events for session observability and future
304
+ policy enforcement. Currently observe-only (no blocking).
305
+ """
306
+
307
+ total_count: int = 0
308
+ by_type: dict[str, int] = field(default_factory=dict) # {"Explore": 2, "Bash": 1}
309
+ last_agent_id: str | None = None
310
+ last_agent_type: str | None = None
311
+ last_stop_at: str | None = None # ISO8601
312
+ last_transcript_path: str | None = None # Agent-specific transcript
313
+ last_message_preview: str | None = None # Truncated last_assistant_message (~200 chars)
314
+
315
+
316
+ @dataclass
317
+ class StartedWithProxy:
318
+ """Proxy identity snapshot captured at session start.
319
+
320
+ This is hook-owned runtime truth for UX/traceability only.
321
+ The proxy remains the authoritative source of routing behavior.
322
+ """
323
+
324
+ base_url: str
325
+ proxy_id: str | None = None
326
+ template: str | None = None
327
+ port: int | None = None
328
+
329
+
330
+ @dataclass
331
+ class Derivation:
332
+ """Context derivation tracking for resumed or forked sessions.
333
+
334
+ Records how this session was derived from its parent(s), enabling
335
+ audit trails and context reconstruction. This is CLI-owned (written
336
+ by `forge session resume` and `forge session fork`), not hook-owned.
337
+
338
+ Fields:
339
+ parent_session: Parent session name (same as SessionState.parent_session).
340
+ parent_transcript: Repo-relative path to parent's transcript artifact.
341
+ inherited_proxy: Template from parent's started_with_proxy (if any).
342
+ resume_mode: "native" (--resume --fork-session) or "handoff" (assembled context).
343
+ None = legacy (handoff). Authoritative field for how context was transferred.
344
+ strategy: Context assembly strategy (minimal|structured|full|ai-curated).
345
+ Only set when resume_mode is "handoff" (or legacy None). Null for native resumes.
346
+ depth: How many ancestors were traversed (1 = parent only).
347
+ resumed_at: ISO8601 timestamp when resume was executed.
348
+ lineage: Ancestry chain from parent to oldest ancestor traversed.
349
+ context_file: Repo-relative path to generated context file.
350
+ """
351
+
352
+ parent_session: str
353
+ parent_transcript: str | None = None
354
+ inherited_proxy: str | None = None
355
+ resume_mode: str | None = None
356
+ strategy: str | None = "structured"
357
+ depth: int = 1
358
+ resumed_at: str | None = None
359
+ lineage: list[str] = field(default_factory=list)
360
+ context_file: str | None = None
361
+ # Project identity fields for cross-project resume (see design.md §3)
362
+ parent_forge_root: str | None = None # Where to find parent artifacts
363
+ parent_project_root: str | None = None # Must match child's project_root
364
+
365
+
366
+ @dataclass
367
+ class SessionConfirmed:
368
+ """What Claude Code actually reported via hooks.
369
+
370
+ Ownership: hook-owned runtime facts only (see docs/design.md ownership boundaries).
371
+
372
+ Notes:
373
+ - Paths recorded in `artifacts` are repo-root-relative (e.g., `.forge/artifacts/...`) unless
374
+ otherwise specified.
375
+ """
376
+
377
+ claude_session_id: str | None = None # Pre-seeded at launch when possible; validated by SessionStart hook
378
+ transcript_path: str | None = None
379
+
380
+ # Proxy identity snapshot (optional; only set in proxy mode)
381
+ started_with_proxy: StartedWithProxy | None = None
382
+
383
+ # Plan tracking
384
+ latest_plan_path: str | None = None # Worktree-relative path to the latest plan file (e.g., ".claude/plans/x.md")
385
+
386
+ # Session artifacts captured by hooks (repo-root-relative paths)
387
+ artifacts: dict[str, Any] = field(default_factory=dict)
388
+
389
+ # Policy enforcement state (decisions log, per-policy states)
390
+ policy: PolicyConfirmed | None = None
391
+
392
+ # Verification state (iterations, timing for Ralph-Wiggum feedback loop)
393
+ verification: VerificationConfirmed | None = None
394
+
395
+ # Compaction tracking (transcript snapshots, event count)
396
+ compaction: CompactionConfirmed | None = None
397
+
398
+ # Subagent activity tracking (counts, last agent info)
399
+ subagents: SubagentConfirmed | None = None
400
+
401
+ # Sidecar execution mode (proxy bundled in Docker container)
402
+ is_sandboxed: bool = False
403
+
404
+ # Context derivation tracking (for resumed or forked sessions)
405
+ derivation: Derivation | None = None
406
+
407
+ # The exact CWD Claude Code was launched from. Set at launch time by the
408
+ # CLI (not the hook) because the hook runs inside the Claude process which
409
+ # already inherited the CWD. Used by resume to match Claude's project
410
+ # namespace (~/.claude/projects/<encoded-cwd>/).
411
+ claude_project_root: str | None = None
412
+
413
+ confirmed_at: str | None = None # ISO8601 string
414
+ confirmed_by: str | None = None # e.g., "hook:SessionStart"
415
+
416
+
417
+ # --- Main session state structure ---
418
+
419
+
420
+ @dataclass
421
+ class SessionState:
422
+ """Complete session state stored in .forge/sessions/<name>/forge.session.json.
423
+
424
+ Schema is intentionally strict:
425
+ - No unknown top-level fields
426
+ - No unknown nested fields
427
+ - No unknown override keys
428
+
429
+ This keeps the file a clear contract rather than an unbounded blob.
430
+ """
431
+
432
+ schema_version: int
433
+ name: str
434
+ created_at: str # ISO8601 string
435
+ last_accessed_at: str # ISO8601 string
436
+ parent_session: str | None = None
437
+ is_fork: bool = False
438
+ is_incognito: bool = False
439
+ worktree: Worktree | None = None
440
+ intent: SessionIntent = field(default_factory=SessionIntent)
441
+ # Sparse overrides - same shape as intent, only changed fields present
442
+ overrides: dict[str, Any] = field(default_factory=dict)
443
+ confirmed: SessionConfirmed = field(default_factory=SessionConfirmed)
444
+ # Project identity (see design.md §3). Optional for backward compat with existing manifests.
445
+ forge_root: str | None = None # Forge project root (where .forge/ lives)
446
+
447
+
448
+ # --- Index structures (for ~/.forge/sessions/index.json) ---
449
+
450
+
451
+ @dataclass
452
+ class SessionIndexEntry:
453
+ """A single entry in the session index.
454
+
455
+ UUID fields enable fast reverse lookup (find session by UUID) without
456
+ scanning all manifests. These are lazily synced by CLI commands.
457
+ """
458
+
459
+ worktree_path: str # Absolute path to worktree (legacy; prefer forge_root)
460
+ project_root: str # Absolute path to main repo (logical repo identity)
461
+ last_accessed_at: str # ISO8601 string
462
+ is_fork: bool = False
463
+ is_incognito: bool = False
464
+ parent_session: str | None = None
465
+ # UUID field for reverse lookup (set by SessionStart hook)
466
+ claude_session_id: str | None = None
467
+ # Empty string (not None) because strict dacite requires str type match;
468
+ # use entry.root for the resolved path (prefers forge_root, falls back to worktree_path).
469
+ forge_root: str = "" # Forge project root (where .forge/ lives)
470
+ checkout_root: str = "" # Git checkout root (--show-toplevel)
471
+ relative_path: str = "." # forge_root relative to checkout_root
472
+
473
+ @property
474
+ def root(self) -> str:
475
+ """Resolved project root: forge_root if set, else worktree_path (pre-identity-model fallback)."""
476
+ return self.forge_root or self.worktree_path
477
+
478
+
479
+ @dataclass
480
+ class SessionIndex:
481
+ """Global session index for fast listing."""
482
+
483
+ version: int = INDEX_VERSION
484
+ sessions: dict[str, SessionIndexEntry] = field(default_factory=dict)
485
+
486
+
487
+ # --- Factory functions ---
488
+
489
+
490
+ def create_session_state(
491
+ name: str,
492
+ *,
493
+ proxy_template: str | None = None,
494
+ proxy_base_url: str | None = None,
495
+ parent_session: str | None = None,
496
+ is_fork: bool = False,
497
+ is_incognito: bool = False,
498
+ worktree_path: str | None = None,
499
+ worktree_branch: str | None = None,
500
+ launch_mode: str = LAUNCH_MODE_HOST,
501
+ sidecar_mounts: list[str] | None = None,
502
+ sidecar_image: str | None = None,
503
+ direct_model: str | None = None,
504
+ ) -> SessionState:
505
+ """Create a new session state with defaults.
506
+
507
+ Args:
508
+ name: Session name (must be validated separately).
509
+ proxy_template: Proxy template (e.g., "litellm-gemini"). Optional in direct mode.
510
+ proxy_base_url: Proxy base URL (e.g., "http://localhost:8084"). Optional in direct mode.
511
+ parent_session: Parent session name (for forks).
512
+ is_fork: Whether this is a forked session.
513
+ is_incognito: Whether this is an incognito session.
514
+ worktree_path: Absolute path to git worktree (if any).
515
+ worktree_branch: Git branch name (defaults to session name).
516
+ launch_mode: How Forge should relaunch this session ("host" or "sidecar").
517
+ sidecar_mounts: Raw sidecar mount specs to persist for relaunch.
518
+ sidecar_image: Optional sidecar image override to persist for relaunch.
519
+ direct_model: Optional Claude Code env-ready direct model pin.
520
+
521
+ Returns:
522
+ A new SessionState with timestamps set to now.
523
+ """
524
+ now = now_iso()
525
+
526
+ if (proxy_template is None) != (proxy_base_url is None):
527
+ raise ValueError("proxy_template and proxy_base_url must be provided together")
528
+
529
+ proxy = None
530
+ if proxy_template is not None and proxy_base_url is not None:
531
+ proxy = ProxyIntent(template=proxy_template, base_url=proxy_base_url)
532
+
533
+ launch = LaunchIntent(mode=launch_mode, direct_model=direct_model)
534
+ if launch_mode == LAUNCH_MODE_SIDECAR or sidecar_mounts or sidecar_image is not None:
535
+ launch.sidecar = SidecarLaunchIntent(
536
+ mounts=list(sidecar_mounts or []),
537
+ image=sidecar_image,
538
+ )
539
+
540
+ worktree = None
541
+ if worktree_path:
542
+ worktree = Worktree(
543
+ path=worktree_path,
544
+ branch=worktree_branch or name,
545
+ )
546
+
547
+ return SessionState(
548
+ schema_version=SCHEMA_VERSION,
549
+ name=name,
550
+ created_at=now,
551
+ last_accessed_at=now,
552
+ parent_session=parent_session,
553
+ is_fork=is_fork,
554
+ is_incognito=is_incognito,
555
+ worktree=worktree,
556
+ intent=SessionIntent(
557
+ proxy=proxy,
558
+ launch=launch,
559
+ ),
560
+ confirmed=SessionConfirmed(),
561
+ )
562
+
563
+
564
+ def session_state_to_dict(state: SessionState) -> dict[str, Any]:
565
+ """Convert SessionState to dict.
566
+
567
+ This should be used instead of dataclasses.asdict() when serializing
568
+ session state to JSON.
569
+ """
570
+ from dataclasses import asdict
571
+
572
+ return asdict(state)