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,158 @@
1
+ """Base class for deterministic policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from abc import ABC, abstractmethod
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from forge.guard.types import ActionContext, PolicyDecision, Violation
11
+
12
+
13
+ class DeterministicPolicy(ABC):
14
+ """Base class for deterministic (non-LLM) policies.
15
+
16
+ Subclasses implement:
17
+ - policy_id: Unique identifier (e.g., "tdd.tests-before-impl")
18
+ - description: Human-readable description
19
+ - intent: Why this policy exists (shown to models on deny so they
20
+ understand the goal and surface conflicts instead of working around them)
21
+ - _evaluate: The actual evaluation logic
22
+
23
+ The base class provides:
24
+ - Default applies_to() for Write/Edit filtering
25
+ - Path normalization helpers
26
+ - Common pattern matching utilities
27
+ """
28
+
29
+ @property
30
+ @abstractmethod
31
+ def policy_id(self) -> str:
32
+ """Unique identifier for this policy."""
33
+ ...
34
+
35
+ @property
36
+ @abstractmethod
37
+ def description(self) -> str:
38
+ """Human-readable description."""
39
+ ...
40
+
41
+ @property
42
+ @abstractmethod
43
+ def intent(self) -> str:
44
+ """Why this policy exists.
45
+
46
+ Shown to models on deny so they understand the goal behind the rule.
47
+ This helps models surface conflicts to the user instead of finding
48
+ creative workarounds that satisfy the letter but not the spirit.
49
+ """
50
+ ...
51
+
52
+ def applies_to(self, context: ActionContext) -> bool:
53
+ """Return True if this policy should evaluate the action.
54
+
55
+ Default: applies to Write and Edit tools only.
56
+ Override for more specific filtering.
57
+ """
58
+ return context.tool_name in ("Write", "Edit")
59
+
60
+ def evaluate(self, context: ActionContext) -> PolicyDecision:
61
+ """Evaluate the action and return a decision.
62
+
63
+ Wraps _evaluate() with common setup. Subclasses override _evaluate().
64
+ """
65
+ return self._evaluate(context)
66
+
67
+ @abstractmethod
68
+ def _evaluate(self, context: ActionContext) -> PolicyDecision:
69
+ """Implement policy-specific evaluation logic."""
70
+ ...
71
+
72
+ # --- Helper methods ---
73
+
74
+ def _normalize_path(self, path: str | None, repo_root: str) -> str | None:
75
+ """Normalize a path relative to repo root.
76
+
77
+ Returns the path relative to repo_root, or None if path is None.
78
+ """
79
+ if path is None:
80
+ return None
81
+
82
+ try:
83
+ abs_path = Path(path).resolve()
84
+ root = Path(repo_root).resolve()
85
+ return str(abs_path.relative_to(root))
86
+ except (ValueError, RuntimeError):
87
+ # Path not under repo root, return as-is
88
+ return path
89
+
90
+ def _is_under_directory(self, path: str | None, directory: str) -> bool:
91
+ """Check if path is under a directory (e.g., 'tests/' or 'src/')."""
92
+ if path is None:
93
+ return False
94
+
95
+ # Normalize separators and ensure consistent format
96
+ normalized = path.replace("\\", "/")
97
+
98
+ # Check if path starts with directory or contains /directory/
99
+ return normalized.startswith(f"{directory}/") or f"/{directory}/" in normalized
100
+
101
+ def _matches_any_pattern(self, content: str | None, patterns: list[str]) -> list[str]:
102
+ """Return list of matched patterns from content.
103
+
104
+ Args:
105
+ content: Text to search
106
+ patterns: List of regex patterns
107
+
108
+ Returns:
109
+ List of patterns that matched (empty if none)
110
+ """
111
+ if content is None:
112
+ return []
113
+
114
+ matched = []
115
+ for pattern in patterns:
116
+ if re.search(pattern, content, re.MULTILINE):
117
+ matched.append(pattern)
118
+
119
+ return matched
120
+
121
+ def _allow(self) -> PolicyDecision:
122
+ """Return an allow decision."""
123
+ return PolicyDecision(decision="allow", policy_id=self.policy_id)
124
+
125
+ def _deny(self, violations: list[Violation]) -> PolicyDecision:
126
+ """Return a deny decision with violations and policy intent."""
127
+ return PolicyDecision(
128
+ decision="deny",
129
+ policy_id=self.policy_id,
130
+ violations=violations,
131
+ intent=self.intent,
132
+ )
133
+
134
+ def _warn(self, warnings: list[str]) -> PolicyDecision:
135
+ """Return a warn decision."""
136
+ return PolicyDecision(
137
+ decision="warn",
138
+ policy_id=self.policy_id,
139
+ warnings=warnings,
140
+ )
141
+
142
+
143
+ class StatefulDeterministicPolicy(DeterministicPolicy):
144
+ """Base class for deterministic policies that track state.
145
+
146
+ State is persisted to the session manifest between hook invocations.
147
+ Subclasses implement get_state() and set_state() for their specific state.
148
+ """
149
+
150
+ @abstractmethod
151
+ def get_state(self) -> dict[str, Any]:
152
+ """Return current state for persistence."""
153
+ ...
154
+
155
+ @abstractmethod
156
+ def set_state(self, state: dict[str, Any]) -> None:
157
+ """Restore state from persisted data."""
158
+ ...
@@ -0,0 +1,256 @@
1
+ """Coding standards bundle policies.
2
+
3
+ Enforces coding conventions from docs/developer/coding-standards.md:
4
+ - no-TYPE_CHECKING: Block TYPE_CHECKING import workarounds
5
+ - no-backward-compat: Block backward compatibility hacks
6
+ - no-emoji: Block colorful emoji in code files (monospace matters)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ from forge.guard.deterministic.base import DeterministicPolicy
14
+ from forge.guard.types import ActionContext, PolicyDecision, Violation
15
+
16
+ # Patterns indicating TYPE_CHECKING workarounds
17
+ TYPE_CHECKING_PATTERNS = [
18
+ r"if\s+TYPE_CHECKING\s*:",
19
+ r"from\s+typing\s+import.*TYPE_CHECKING",
20
+ ]
21
+
22
+ # Patterns indicating backward compatibility hacks
23
+ BACKWARD_COMPAT_PATTERNS = [
24
+ r"#\s*backward\s*compat",
25
+ r"#\s*backwards?\s*compat",
26
+ r"#\s*legacy\b",
27
+ r"#\s*deprecated\b",
28
+ r"#\s*TODO.*remove.*later",
29
+ r"#\s*for\s+backward",
30
+ r"#\s*DEPRECATED\b",
31
+ r"#\s*LEGACY\b",
32
+ r"#\s*compat(?:ibility)?\s*(?:layer|shim|wrapper)",
33
+ ]
34
+
35
+
36
+ class NoTypeCheckingPolicy(DeterministicPolicy):
37
+ """Block TYPE_CHECKING import workarounds.
38
+
39
+ From coding-standards.md:
40
+ > No TYPE_CHECKING workarounds: Fix circular imports architecturally
41
+ > instead of using `if TYPE_CHECKING:` blocks
42
+
43
+ TYPE_CHECKING blocks are a symptom of circular imports that should be
44
+ fixed by restructuring the code (e.g., moving types to a separate module).
45
+ """
46
+
47
+ @property
48
+ def policy_id(self) -> str:
49
+ return "coding_standards.no-type-checking"
50
+
51
+ @property
52
+ def description(self) -> str:
53
+ return "Block TYPE_CHECKING workarounds (fix circular imports architecturally)"
54
+
55
+ @property
56
+ def intent(self) -> str:
57
+ return (
58
+ "Circular imports indicate an architectural problem. TYPE_CHECKING blocks "
59
+ "hide the symptom instead of fixing the dependency structure. This policy "
60
+ "ensures clean module boundaries."
61
+ )
62
+
63
+ def applies_to(self, context: ActionContext) -> bool:
64
+ """Apply to Write/Edit on Python files with content."""
65
+ if context.tool_name not in ("Write", "Edit"):
66
+ return False
67
+
68
+ if context.new_content is None:
69
+ return False
70
+
71
+ # Only check Python files
72
+ path = context.target_path
73
+ return path is not None and path.endswith(".py")
74
+
75
+ def _evaluate(self, context: ActionContext) -> PolicyDecision:
76
+ """Check for TYPE_CHECKING patterns."""
77
+ matched = self._matches_any_pattern(context.new_content, TYPE_CHECKING_PATTERNS)
78
+
79
+ if matched:
80
+ violations = [
81
+ Violation(
82
+ rule_id=self.policy_id,
83
+ message="TYPE_CHECKING blocks are not allowed",
84
+ severity="medium",
85
+ evidence=f"Found TYPE_CHECKING pattern(s): {', '.join(matched)}",
86
+ suggested_fix=(
87
+ "Fix circular imports architecturally by:\n"
88
+ "1. Moving shared types to a separate types.py module\n"
89
+ "2. Using dependency injection\n"
90
+ "3. Restructuring the module hierarchy"
91
+ ),
92
+ )
93
+ ]
94
+ return self._deny(violations)
95
+
96
+ return self._allow()
97
+
98
+
99
+ class NoBackwardCompatPolicy(DeterministicPolicy):
100
+ """Block backward compatibility hacks.
101
+
102
+ From coding-standards.md:
103
+ > No Backward Compatibility Wrappers: Update callers directly, don't create adapters
104
+ > Clean Refactoring: Fix underlying issues over compatibility layers
105
+ > No Fallback Logic: When replacing a component, remove the old one completely
106
+
107
+ This policy detects common backward-compat patterns in comments and code.
108
+ """
109
+
110
+ @property
111
+ def policy_id(self) -> str:
112
+ return "coding_standards.no-backward-compat"
113
+
114
+ @property
115
+ def description(self) -> str:
116
+ return "Block backward compatibility hacks (update callers directly)"
117
+
118
+ @property
119
+ def intent(self) -> str:
120
+ return (
121
+ "Compatibility layers accumulate technical debt. This project prefers clean "
122
+ "breaks: update all callers directly and remove old code completely rather "
123
+ "than maintaining shims or fallback logic."
124
+ )
125
+
126
+ def applies_to(self, context: ActionContext) -> bool:
127
+ """Apply to Write/Edit on Python files with content."""
128
+ if context.tool_name not in ("Write", "Edit"):
129
+ return False
130
+
131
+ if context.new_content is None:
132
+ return False
133
+
134
+ # Only check Python files
135
+ path = context.target_path
136
+ return path is not None and path.endswith(".py")
137
+
138
+ def _evaluate(self, context: ActionContext) -> PolicyDecision:
139
+ """Check for backward compatibility patterns."""
140
+ matched = self._matches_any_pattern(context.new_content, BACKWARD_COMPAT_PATTERNS)
141
+
142
+ if matched:
143
+ violations = [
144
+ Violation(
145
+ rule_id=self.policy_id,
146
+ message="Backward compatibility patterns are not allowed",
147
+ severity="medium",
148
+ evidence=f"Found backward-compat pattern(s): {', '.join(matched)}",
149
+ suggested_fix=(
150
+ "Instead of compatibility layers:\n"
151
+ "1. Update all callers directly\n"
152
+ "2. Remove the old implementation completely\n"
153
+ "3. Delete obsolete tests (don't skip them)"
154
+ ),
155
+ )
156
+ ]
157
+ return self._deny(violations)
158
+
159
+ return self._allow()
160
+
161
+
162
+ # Colorful emoji ranges — double-width characters that break monospace rendering.
163
+ # Excludes text-safe dingbats (checkmark, cross, diamond, warning, arrows) that render
164
+ # properly in fixed-width terminals.
165
+ _EMOJI_PATTERN = re.compile(
166
+ "["
167
+ "\U0001f300-\U0001f5ff" # Misc symbols & pictographs
168
+ "\U0001f600-\U0001f64f" # Emoticons (faces)
169
+ "\U0001f680-\U0001f6ff" # Transport & map symbols
170
+ "\U0001f700-\U0001f77f" # Alchemical symbols
171
+ "\U0001f900-\U0001f9ff" # Supplemental symbols & pictographs
172
+ "\U0001fa00-\U0001faff" # Chess, extended-A symbols
173
+ "]"
174
+ )
175
+
176
+ _CODE_EXTENSIONS = frozenset(
177
+ {
178
+ ".py",
179
+ ".js",
180
+ ".ts",
181
+ ".jsx",
182
+ ".tsx",
183
+ ".sh",
184
+ ".bash",
185
+ ".java",
186
+ ".go",
187
+ ".rs",
188
+ ".c",
189
+ ".cpp",
190
+ ".h",
191
+ ".hpp",
192
+ ".rb",
193
+ ".swift",
194
+ ".kt",
195
+ ".scala",
196
+ }
197
+ )
198
+
199
+
200
+ class NoEmojiPolicy(DeterministicPolicy):
201
+ """Block colorful emoji in code files.
202
+
203
+ Monospace rendering matters in code. Double-width emoji characters break
204
+ alignment in terminals, diffs, and code review tools. Text-safe symbols
205
+ (checkmark, cross, arrows, warning) are allowed — only colorful pictographs
206
+ are blocked.
207
+ """
208
+
209
+ @property
210
+ def policy_id(self) -> str:
211
+ return "coding_standards.no-emoji"
212
+
213
+ @property
214
+ def description(self) -> str:
215
+ return "Block colorful emoji in code (monospace matters)"
216
+
217
+ @property
218
+ def intent(self) -> str:
219
+ return (
220
+ "Double-width emoji break alignment in terminals, diffs, and code review "
221
+ "tools. Source code should stay ASCII-clean for consistent monospace "
222
+ "rendering. This includes Unicode escapes that produce emoji at runtime."
223
+ )
224
+
225
+ def applies_to(self, context: ActionContext) -> bool:
226
+ """Apply to Write/Edit on code files with content."""
227
+ if context.tool_name not in ("Write", "Edit"):
228
+ return False
229
+ if context.new_content is None:
230
+ return False
231
+ path = context.target_path
232
+ if path is None:
233
+ return False
234
+ for ext in _CODE_EXTENSIONS:
235
+ if path.endswith(ext):
236
+ return True
237
+ return False
238
+
239
+ def _evaluate(self, context: ActionContext) -> PolicyDecision:
240
+ """Check for colorful emoji in content."""
241
+ assert context.new_content is not None
242
+ found = _EMOJI_PATTERN.findall(context.new_content)
243
+ if found:
244
+ unique = list(dict.fromkeys(found)) # dedupe, preserve order
245
+ sample = " ".join(unique[:5])
246
+ violations = [
247
+ Violation(
248
+ rule_id=self.policy_id,
249
+ message=f"Emoji characters found in code: {sample}",
250
+ severity="low",
251
+ evidence=f"Found {len(found)} emoji character(s): {sample}",
252
+ suggested_fix="Use ASCII equivalents or text-safe symbols instead of colorful emoji.",
253
+ )
254
+ ]
255
+ return self._deny(violations)
256
+ return self._allow()
@@ -0,0 +1,148 @@
1
+ """Bundle registry for deterministic policies.
2
+
3
+ Maps bundle names to policy classes. Bundles are collections of related
4
+ policies that can be enabled together.
5
+
6
+ Available bundles:
7
+ - tdd: Test-driven development workflow enforcement
8
+ - coding_standards: Code style and architecture conventions
9
+ - workflow: Config-driven tagger → branch → stage pipelines
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from forge.guard.deterministic.coding_standards import (
17
+ NoBackwardCompatPolicy,
18
+ NoEmojiPolicy,
19
+ NoTypeCheckingPolicy,
20
+ )
21
+ from forge.guard.deterministic.tdd import (
22
+ NoSkipTestsPolicy,
23
+ TDDEnforcementPolicy,
24
+ )
25
+ from forge.guard.protocols import Policy
26
+
27
+ # Bundle name -> list of policy classes
28
+ # Each class is instantiated fresh when get_bundle_policies() is called
29
+ BUNDLES: dict[str, list[type]] = {
30
+ "tdd": [
31
+ TDDEnforcementPolicy,
32
+ NoSkipTestsPolicy,
33
+ ],
34
+ "coding_standards": [
35
+ NoTypeCheckingPolicy,
36
+ NoBackwardCompatPolicy,
37
+ NoEmojiPolicy,
38
+ ],
39
+ }
40
+
41
+ # Map policy_id to bundle for reverse lookup
42
+ POLICY_TO_BUNDLE: dict[str, str] = {
43
+ "tdd.tests-before-impl": "tdd",
44
+ "tdd.no-skip-tests": "tdd",
45
+ "coding_standards.no-type-checking": "coding_standards",
46
+ "coding_standards.no-backward-compat": "coding_standards",
47
+ "coding_standards.no-emoji": "coding_standards",
48
+ }
49
+
50
+
51
+ def get_bundle_policies(bundle: str, *, config: dict[str, Any] | None = None) -> list[Policy]:
52
+ """Get instantiated policies for a bundle.
53
+
54
+ Args:
55
+ bundle: Bundle name (e.g., "tdd", "coding_standards", "workflow")
56
+ config: Per-bundle configuration dict. For the "tdd" bundle:
57
+ - ``{"strict": False}`` -> TDDEnforcementPolicy warns instead of denying
58
+ - ``{"strict": True}`` or ``{}`` or ``None`` -> strict mode (default)
59
+
60
+ For the "workflow" bundle:
61
+ - ``{"workflows": [{...}, ...]}`` -> one WorkflowPolicy per entry
62
+
63
+ Returns:
64
+ List of policy instances. Empty list if bundle not found.
65
+
66
+ Raises:
67
+ ValueError: If config contains invalid types (e.g., ``strict`` is not bool).
68
+
69
+ Example:
70
+ >>> policies = get_bundle_policies("tdd")
71
+ >>> [p.policy_id for p in policies]
72
+ ['tdd.tests-before-impl', 'tdd.no-skip-tests']
73
+ """
74
+ if bundle == "workflow":
75
+ return _build_workflow_policies(config)
76
+
77
+ policy_classes = BUNDLES.get(bundle, [])
78
+ policies: list[Policy] = []
79
+ for cls in policy_classes:
80
+ if bundle == "tdd" and cls is TDDEnforcementPolicy:
81
+ strict = True # default
82
+ if config and "strict" in config:
83
+ val = config["strict"]
84
+ if not isinstance(val, bool):
85
+ raise ValueError(f"bundle_config.tdd.strict must be bool, got {type(val).__name__}")
86
+ strict = val
87
+ policies.append(cls(strict=strict))
88
+ else:
89
+ policies.append(cls())
90
+ return policies
91
+
92
+
93
+ def _build_workflow_policies(config: dict[str, Any] | None) -> list[Policy]:
94
+ """Instantiate WorkflowPolicy instances from workflow config.
95
+
96
+ Lazy-imports workflow module to avoid pulling LLM dependencies
97
+ unless the workflow bundle is actually used.
98
+ """
99
+ import dacite
100
+
101
+ from forge.guard.workflow.config import WorkflowConfig
102
+ from forge.guard.workflow.policy import WorkflowPolicy
103
+
104
+ if not config:
105
+ return []
106
+ workflows = config.get("workflows", [])
107
+ if not isinstance(workflows, list):
108
+ raise ValueError(f"bundle_config.workflow.workflows must be a list, got {type(workflows).__name__}")
109
+ policies: list[Policy] = []
110
+ for wf_dict in workflows:
111
+ wf_config = dacite.from_dict(WorkflowConfig, wf_dict)
112
+ policies.append(WorkflowPolicy(config=wf_config))
113
+ return policies
114
+
115
+
116
+ def get_all_bundles() -> list[str]:
117
+ """Get list of all available bundle names."""
118
+ return list(BUNDLES.keys()) + ["workflow"]
119
+
120
+
121
+ def get_bundle_for_policy(policy_id: str) -> str | None:
122
+ """Get the bundle name for a policy ID.
123
+
124
+ Args:
125
+ policy_id: Policy identifier (e.g., "tdd.tests-before-impl")
126
+
127
+ Returns:
128
+ Bundle name or None if not found.
129
+ """
130
+ if policy_id.startswith("workflow."):
131
+ return "workflow"
132
+ return POLICY_TO_BUNDLE.get(policy_id)
133
+
134
+
135
+ def get_policy_ids_for_bundle(bundle: str) -> list[str]:
136
+ """Get list of policy IDs in a bundle.
137
+
138
+ For the "workflow" bundle, returns ``[]`` because workflow policy IDs
139
+ are dynamic (``workflow.<name>``) and only known at runtime with config.
140
+
141
+ Args:
142
+ bundle: Bundle name
143
+
144
+ Returns:
145
+ List of policy IDs. Empty list if bundle not found.
146
+ """
147
+ policies = get_bundle_policies(bundle)
148
+ return [p.policy_id for p in policies]