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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- 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]
|