@jaguilar87/gaia 5.0.4 → 5.0.5
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +56 -0
- package/INSTALL.md +0 -2
- package/README.md +1 -6
- package/bin/README.md +0 -1
- package/bin/cli/_install_helpers.py +1 -1
- package/bin/cli/cleanup.py +0 -1
- package/bin/cli/doctor.py +1 -1
- package/bin/cli/memory.py +2 -0
- package/bin/cli/update.py +1 -1
- package/bin/pre-publish-validate.js +48 -5
- package/config/README.md +22 -44
- package/config/surface-routing.json +0 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/config/README.md +22 -44
- package/dist/gaia-ops/config/surface-routing.json +0 -1
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +2 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +2 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +2 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +90 -55
- package/dist/gaia-ops/skills/README.md +1 -1
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +0 -1
- package/dist/gaia-ops/skills/gaia-release/SKILL.md +60 -24
- package/dist/gaia-ops/skills/gaia-release/reference.md +35 -11
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +6 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +10 -2
- package/dist/gaia-ops/skills/readme-writing/SKILL.md +1 -1
- package/dist/gaia-ops/skills/readme-writing/reference.md +0 -1
- package/dist/gaia-ops/tools/scan/ui.py +20 -4
- package/dist/gaia-ops/tools/scan/verify.py +3 -3
- package/dist/gaia-ops/tools/validation/README.md +15 -24
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +2 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +2 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +2 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
- package/hooks/modules/agents/handoff_persister.py +2 -0
- package/hooks/modules/security/approval_grants.py +2 -0
- package/hooks/modules/tools/bash_validator.py +2 -0
- package/hooks/modules/validation/commit_validator.py +90 -55
- package/index.js +2 -12
- package/package.json +4 -6
- package/pyproject.toml +3 -3
- package/scripts/bootstrap_database.sh +88 -439
- package/scripts/check_schema_drift.py +208 -0
- package/scripts/migrations/README.md +78 -28
- package/scripts/migrations/schema.checksum +8 -0
- package/scripts/release-prepare.mjs +199 -0
- package/skills/README.md +1 -1
- package/skills/gaia-patterns/SKILL.md +1 -1
- package/skills/gaia-patterns/reference.md +0 -1
- package/skills/gaia-release/SKILL.md +60 -24
- package/skills/gaia-release/reference.md +35 -11
- package/skills/git-conventions/SKILL.md +6 -2
- package/skills/orchestrator-present-approval/SKILL.md +10 -2
- package/skills/readme-writing/SKILL.md +1 -1
- package/skills/readme-writing/reference.md +0 -1
- package/tools/scan/ui.py +20 -4
- package/tools/scan/verify.py +3 -3
- package/tools/validation/README.md +15 -24
- package/commands/README.md +0 -64
- package/commands/gaia.md +0 -37
- package/commands/scan-project.md +0 -74
- package/config/crons-schema.md +0 -81
- package/config/git_standards.json +0 -72
- package/dist/gaia-ops/commands/gaia.md +0 -37
- package/dist/gaia-ops/config/crons-schema.md +0 -81
- package/dist/gaia-ops/config/git_standards.json +0 -72
- package/dist/gaia-ops/tools/agentic-loop/decide-status.py +0 -210
- package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +0 -106
- package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +0 -223
- package/git-hooks/commit-msg +0 -41
- package/scripts/migrations/v10_to_v11.sql +0 -170
- package/scripts/migrations/v10_to_v11_fresh.sql +0 -18
- package/scripts/migrations/v11_to_v12.sql +0 -195
- package/scripts/migrations/v11_to_v12_fresh.sql +0 -19
- package/scripts/migrations/v12_to_v13.sql +0 -48
- package/scripts/migrations/v12_to_v13_fresh.sql +0 -17
- package/scripts/migrations/v13_to_v14.sql +0 -44
- package/scripts/migrations/v13_to_v14_fresh.sql +0 -17
- package/scripts/migrations/v14_to_v15.sql +0 -71
- package/scripts/migrations/v14_to_v15_fresh.sql +0 -19
- package/scripts/migrations/v15_to_v16.sql +0 -57
- package/scripts/migrations/v15_to_v16_fresh.sql +0 -18
- package/scripts/migrations/v16_to_v17.sql +0 -51
- package/scripts/migrations/v16_to_v17_fresh.sql +0 -18
- package/scripts/migrations/v17_to_v18.sql +0 -66
- package/scripts/migrations/v17_to_v18_fresh.sql +0 -24
- package/scripts/migrations/v1_to_v2.sql +0 -97
- package/scripts/migrations/v2_to_v3.sql +0 -68
- package/scripts/migrations/v2_to_v3_merge.sql +0 -69
- package/scripts/migrations/v3_to_v4.sql +0 -67
- package/scripts/migrations/v3_to_v4_fresh.sql +0 -20
- package/scripts/migrations/v4_to_v5.sql +0 -55
- package/scripts/migrations/v4_to_v5_fresh.sql +0 -20
- package/scripts/migrations/v5_to_v6.sql +0 -48
- package/scripts/migrations/v5_to_v6_fresh.sql +0 -17
- package/scripts/migrations/v6_to_v7.sql +0 -26
- package/scripts/migrations/v6_to_v7_fresh.sql +0 -13
- package/scripts/migrations/v7_to_v8.sql +0 -44
- package/scripts/migrations/v7_to_v8_fresh.sql +0 -14
- package/scripts/migrations/v8_to_v9.sql +0 -87
- package/scripts/migrations/v8_to_v9_fresh.sql +0 -15
- package/scripts/migrations/v9_to_v10.sql +0 -109
- package/scripts/migrations/v9_to_v10_episodes_workspace.sql +0 -109
- package/scripts/migrations/v9_to_v10_fresh.sql +0 -18
- package/templates/README.md +0 -70
- package/templates/managed-settings.template.json +0 -43
- package/tools/agentic-loop/decide-status.py +0 -210
- package/tools/agentic-loop/parse-metric.py +0 -106
- package/tools/agentic-loop/record-iteration.py +0 -223
|
@@ -45,8 +45,8 @@ class CheckResult:
|
|
|
45
45
|
def check_symlinks(project_root: Path) -> CheckResult:
|
|
46
46
|
"""Verify that all expected symlinks exist in .claude/.
|
|
47
47
|
|
|
48
|
-
Checks for: agents, tools, hooks, commands,
|
|
49
|
-
skills, CHANGELOG.md (
|
|
48
|
+
Checks for: agents, tools, hooks, commands, config,
|
|
49
|
+
skills, CHANGELOG.md (7 total).
|
|
50
50
|
|
|
51
51
|
Args:
|
|
52
52
|
project_root: Project root directory.
|
|
@@ -56,7 +56,7 @@ def check_symlinks(project_root: Path) -> CheckResult:
|
|
|
56
56
|
"""
|
|
57
57
|
names = [
|
|
58
58
|
"agents", "tools", "hooks", "commands",
|
|
59
|
-
"
|
|
59
|
+
"config", "skills",
|
|
60
60
|
"CHANGELOG.md",
|
|
61
61
|
]
|
|
62
62
|
valid = 0
|
|
@@ -25,7 +25,7 @@ without requiring explicit imports in agent code.
|
|
|
25
25
|
- ✅ Subject line rules (max 72 chars, no period at end)
|
|
26
26
|
- ✅ Forbidden footers (no "Generated with" footers)
|
|
27
27
|
|
|
28
|
-
**Configuration:**
|
|
28
|
+
**Configuration:** Standards are inlined as module-level constants in `hooks/modules/validation/commit_validator.py` (`TYPE_ALLOWED`, `SUBJECT_MAX_LENGTH`, `SUBJECT_RULES`, `BODY_MAX_LINE_LENGTH`, `ENFORCEMENT`). Forbidden-footer detection lives in `bash_validator`.
|
|
29
29
|
**Logs:** `.claude/logs/commit-violations.jsonl`
|
|
30
30
|
|
|
31
31
|
---
|
|
@@ -108,16 +108,11 @@ This validation module works with skills in a **hybrid model**:
|
|
|
108
108
|
|
|
109
109
|
```
|
|
110
110
|
┌──────────────────────────────────────────────────────────┐
|
|
111
|
-
│ config/git_standards.json (SSOT) │
|
|
112
|
-
│ - Conventional commit types │
|
|
113
|
-
│ - Forbidden footers │
|
|
114
|
-
│ - Max lengths │
|
|
115
|
-
└──────────────────────────────────────────────────────────┘
|
|
116
|
-
│
|
|
117
|
-
▼
|
|
118
|
-
┌──────────────────────────────────────────────────────────┐
|
|
119
111
|
│ hooks/modules/validation/ (Commit Validation) │
|
|
120
112
|
│ └─ commit_validator.py │
|
|
113
|
+
│ ├─ Standards inlined as module-level constants │
|
|
114
|
+
│ │ (types, subject/body max lengths, rules) │
|
|
115
|
+
│ ├─ Forbidden footers handled by bash_validator │
|
|
121
116
|
│ └─ Used by bash_validator.py only │
|
|
122
117
|
└──────────────────────────────────────────────────────────┘
|
|
123
118
|
│
|
|
@@ -178,24 +173,20 @@ Note: commit_validator.py moved to hooks/modules/validation/
|
|
|
178
173
|
|
|
179
174
|
## Configuration
|
|
180
175
|
|
|
181
|
-
**Git Standards:**
|
|
176
|
+
**Git Standards:** Inlined as module-level constants in `hooks/modules/validation/commit_validator.py`.
|
|
182
177
|
|
|
183
178
|
Example:
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"enforcement": {
|
|
192
|
-
"enabled": true,
|
|
193
|
-
"block_on_failure": true,
|
|
194
|
-
"log_violations": true
|
|
195
|
-
}
|
|
196
|
-
}
|
|
179
|
+
```python
|
|
180
|
+
TYPE_ALLOWED = ("feat", "fix", "refactor", "docs", "test", "chore",
|
|
181
|
+
"ci", "perf", "style", "build")
|
|
182
|
+
SUBJECT_MAX_LENGTH = 72
|
|
183
|
+
SUBJECT_RULES = {"no_period_at_end": True, "no_emoji": True,
|
|
184
|
+
"imperative_mood": True, "capitalize_first_letter": False}
|
|
185
|
+
ENFORCEMENT = {"enabled": True, "block_on_failure": True, "log_violations": True}
|
|
197
186
|
```
|
|
198
187
|
|
|
188
|
+
Forbidden-footer detection lives, hardcoded, in `bash_validator`.
|
|
189
|
+
|
|
199
190
|
---
|
|
200
191
|
|
|
201
192
|
## Logs
|
|
@@ -232,7 +223,7 @@ Example entry:
|
|
|
232
223
|
|
|
233
224
|
## See Also
|
|
234
225
|
|
|
235
|
-
-
|
|
226
|
+
- `hooks/modules/validation/commit_validator.py` - Git standards (inlined constants)
|
|
236
227
|
- `.claude/skills/subagent-request-approval/SKILL.md` - Approval-request workflow patterns
|
|
237
228
|
- `.claude/skills/execution/SKILL.md` - Execution workflow patterns
|
|
238
229
|
- `CLAUDE.md` - Orchestrator protocol with T3 workflow
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia-security",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
4
4
|
"description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jaguilar87",
|
|
@@ -23,11 +23,75 @@ Usage:
|
|
|
23
23
|
import json
|
|
24
24
|
import os
|
|
25
25
|
import re
|
|
26
|
-
from typing import Dict, List,
|
|
26
|
+
from typing import Dict, List, Optional
|
|
27
27
|
from datetime import datetime
|
|
28
28
|
from dataclasses import dataclass
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Git commit standards -- inlined constants.
|
|
33
|
+
#
|
|
34
|
+
# These were previously loaded from config/git_standards.json. They are now
|
|
35
|
+
# module-level constants: commit_validator.py is the single runtime consumer
|
|
36
|
+
# of these format/subject/body rules, so the JSON indirection added drift risk
|
|
37
|
+
# without any benefit. Footer detection/stripping is NOT here -- it lives,
|
|
38
|
+
# hardcoded, in bash_validator (footers are bash_validator's responsibility).
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
FORMAT = "conventional_commits"
|
|
42
|
+
|
|
43
|
+
TYPE_ALLOWED = (
|
|
44
|
+
"feat",
|
|
45
|
+
"fix",
|
|
46
|
+
"refactor",
|
|
47
|
+
"docs",
|
|
48
|
+
"test",
|
|
49
|
+
"chore",
|
|
50
|
+
"ci",
|
|
51
|
+
"perf",
|
|
52
|
+
"style",
|
|
53
|
+
"build",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
SCOPE_REQUIRED = False
|
|
57
|
+
SCOPE_EXAMPLES = ("helmrelease", "terraform", "pg-non-prod", "infrastructure")
|
|
58
|
+
|
|
59
|
+
SUBJECT_MAX_LENGTH = 72
|
|
60
|
+
SUBJECT_RULES = {
|
|
61
|
+
"capitalize_first_letter": False,
|
|
62
|
+
"no_period_at_end": True,
|
|
63
|
+
"imperative_mood": True,
|
|
64
|
+
"no_emoji": True,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
BODY_MAX_LINE_LENGTH = 72
|
|
68
|
+
BODY_REQUIRED = False
|
|
69
|
+
|
|
70
|
+
EXAMPLES_VALID = (
|
|
71
|
+
"feat(helmrelease): add Phase 3.3 services",
|
|
72
|
+
"fix(pg-non-prod): correct API key environment variable mappings",
|
|
73
|
+
"refactor: simplify context provider logic",
|
|
74
|
+
"docs: update README with new workflow",
|
|
75
|
+
"chore(deps): update terraform to v1.6.0",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
EXAMPLES_INVALID = (
|
|
79
|
+
"Added new feature",
|
|
80
|
+
"Fixed bugs",
|
|
81
|
+
"Updates",
|
|
82
|
+
"feat: add feature\n\n🤖 Generated with Claude Code",
|
|
83
|
+
"feat: add new feature 🚀",
|
|
84
|
+
"fix: 🐛 correct bug",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
ENFORCEMENT = {
|
|
88
|
+
"enabled": True,
|
|
89
|
+
"block_on_failure": True,
|
|
90
|
+
"log_violations": True,
|
|
91
|
+
"log_path": ".claude/logs/commit-violations.jsonl",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
31
95
|
@dataclass
|
|
32
96
|
class ValidationResult:
|
|
33
97
|
"""Result of commit message validation."""
|
|
@@ -44,43 +108,30 @@ class CommitMessageValidator:
|
|
|
44
108
|
"""
|
|
45
109
|
Validates git commit messages against project standards.
|
|
46
110
|
|
|
47
|
-
Standards are
|
|
111
|
+
Standards are inlined as module-level constants (TYPE_ALLOWED,
|
|
112
|
+
SUBJECT_MAX_LENGTH, SUBJECT_RULES, etc.). Footer detection is not handled
|
|
113
|
+
here -- that is bash_validator's responsibility.
|
|
48
114
|
"""
|
|
49
115
|
|
|
50
116
|
def __init__(self, config_path: Optional[str] = None):
|
|
51
117
|
"""
|
|
52
|
-
Initialize validator
|
|
118
|
+
Initialize validator.
|
|
53
119
|
|
|
54
120
|
Args:
|
|
55
|
-
config_path:
|
|
56
|
-
|
|
121
|
+
config_path: Accepted for backward compatibility only. When given,
|
|
122
|
+
it anchors base_path (used to resolve the relative
|
|
123
|
+
violation log path); the rules themselves come from
|
|
124
|
+
the module-level constants, not from any file.
|
|
57
125
|
"""
|
|
58
126
|
if config_path is None:
|
|
59
|
-
#
|
|
60
|
-
# From hooks/modules/validation/ go up to gaia-ops root
|
|
127
|
+
# base_path -> gaia-ops root, used to resolve the violation log.
|
|
61
128
|
# __file__ -> hooks/modules/validation/commit_validator.py
|
|
62
|
-
# dirname(dirname(dirname(dirname(__file__)))) -> gaia-ops root
|
|
63
129
|
base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
64
|
-
config_path = os.path.join(base_path, 'config', 'git_standards.json')
|
|
65
130
|
else:
|
|
66
|
-
# If config_path provided, derive base_path from it
|
|
67
131
|
base_path = os.path.dirname(os.path.dirname(config_path))
|
|
68
132
|
|
|
69
133
|
self.base_path = base_path
|
|
70
|
-
self.
|
|
71
|
-
self.config = self._load_config()
|
|
72
|
-
self.standards = self.config.get('commit_message', {})
|
|
73
|
-
self.enforcement = self.config.get('enforcement', {})
|
|
74
|
-
|
|
75
|
-
def _load_config(self) -> Dict[str, Any]:
|
|
76
|
-
"""Load git standards configuration from JSON file."""
|
|
77
|
-
if not os.path.exists(self.config_path):
|
|
78
|
-
raise FileNotFoundError(
|
|
79
|
-
f"Git standards configuration not found at: {self.config_path}"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
with open(self.config_path, 'r') as f:
|
|
83
|
-
return json.load(f)
|
|
134
|
+
self.enforcement = ENFORCEMENT
|
|
84
135
|
|
|
85
136
|
def validate(self, message: str) -> ValidationResult:
|
|
86
137
|
"""
|
|
@@ -95,19 +146,19 @@ class CommitMessageValidator:
|
|
|
95
146
|
errors = []
|
|
96
147
|
warnings = []
|
|
97
148
|
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
149
|
+
# Note: forbidden-footer detection is intentionally NOT done here.
|
|
150
|
+
# Footers are bash_validator's responsibility (stripping/detection
|
|
151
|
+
# is hardcoded there).
|
|
101
152
|
|
|
102
|
-
#
|
|
153
|
+
# 1. Check conventional commits format
|
|
103
154
|
format_errors = self._check_conventional_format(message)
|
|
104
155
|
errors.extend(format_errors)
|
|
105
156
|
|
|
106
|
-
#
|
|
157
|
+
# 2. Check subject line rules
|
|
107
158
|
subject_errors = self._check_subject_rules(message)
|
|
108
159
|
errors.extend(subject_errors)
|
|
109
160
|
|
|
110
|
-
#
|
|
161
|
+
# 3. Check body rules (warnings only)
|
|
111
162
|
body_warnings = self._check_body_rules(message)
|
|
112
163
|
warnings.extend(body_warnings)
|
|
113
164
|
|
|
@@ -121,22 +172,6 @@ class CommitMessageValidator:
|
|
|
121
172
|
warnings=warnings
|
|
122
173
|
)
|
|
123
174
|
|
|
124
|
-
def _check_forbidden_footers(self, message: str) -> List[Dict[str, str]]:
|
|
125
|
-
"""Check for forbidden footers in commit message."""
|
|
126
|
-
errors = []
|
|
127
|
-
forbidden = self.standards.get('footer_forbidden', [])
|
|
128
|
-
|
|
129
|
-
for forbidden_text in forbidden:
|
|
130
|
-
if forbidden_text.lower() in message.lower():
|
|
131
|
-
errors.append({
|
|
132
|
-
'type': 'FORBIDDEN_FOOTER',
|
|
133
|
-
'message': f"Commit message contains forbidden footer: '{forbidden_text}'",
|
|
134
|
-
'fix': f"Remove all occurrences of '{forbidden_text}'",
|
|
135
|
-
'severity': 'error'
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
return errors
|
|
139
|
-
|
|
140
175
|
def _check_conventional_format(self, message: str) -> List[Dict[str, str]]:
|
|
141
176
|
"""Check if message follows Conventional Commits format."""
|
|
142
177
|
errors = []
|
|
@@ -147,16 +182,16 @@ class CommitMessageValidator:
|
|
|
147
182
|
|
|
148
183
|
# Pattern: type(scope)?: description
|
|
149
184
|
# Examples: feat: add feature, fix(api): correct bug
|
|
150
|
-
allowed_types = '|'.join(
|
|
185
|
+
allowed_types = '|'.join(TYPE_ALLOWED)
|
|
151
186
|
pattern = rf'^({allowed_types})(\(.+?\))?: .+$'
|
|
152
187
|
|
|
153
188
|
if not re.match(pattern, subject):
|
|
154
189
|
errors.append({
|
|
155
190
|
'type': 'INVALID_FORMAT',
|
|
156
191
|
'message': 'Commit message does not follow Conventional Commits format',
|
|
157
|
-
'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(
|
|
192
|
+
'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(TYPE_ALLOWED)}",
|
|
158
193
|
'severity': 'error',
|
|
159
|
-
'examples':
|
|
194
|
+
'examples': list(EXAMPLES_VALID)
|
|
160
195
|
})
|
|
161
196
|
|
|
162
197
|
return errors
|
|
@@ -175,7 +210,7 @@ class CommitMessageValidator:
|
|
|
175
210
|
description = match.group(2)
|
|
176
211
|
|
|
177
212
|
# Check max length
|
|
178
|
-
max_length =
|
|
213
|
+
max_length = SUBJECT_MAX_LENGTH
|
|
179
214
|
if len(subject) > max_length:
|
|
180
215
|
errors.append({
|
|
181
216
|
'type': 'SUBJECT_TOO_LONG',
|
|
@@ -185,7 +220,7 @@ class CommitMessageValidator:
|
|
|
185
220
|
})
|
|
186
221
|
|
|
187
222
|
# Check for period at end
|
|
188
|
-
rules =
|
|
223
|
+
rules = SUBJECT_RULES
|
|
189
224
|
if rules.get('no_period_at_end', True) and description.endswith('.'):
|
|
190
225
|
errors.append({
|
|
191
226
|
'type': 'SUBJECT_ENDS_WITH_PERIOD',
|
|
@@ -242,7 +277,7 @@ class CommitMessageValidator:
|
|
|
242
277
|
})
|
|
243
278
|
|
|
244
279
|
# Check body line length
|
|
245
|
-
max_length =
|
|
280
|
+
max_length = BODY_MAX_LINE_LENGTH
|
|
246
281
|
for i, line in enumerate(lines[2:], start=3): # Skip subject and blank line
|
|
247
282
|
if len(line) > max_length and not line.startswith('http'):
|
|
248
283
|
warnings.append({
|
|
@@ -285,13 +320,13 @@ class CommitMessageValidator:
|
|
|
285
320
|
def get_examples(self) -> Dict[str, List[str]]:
|
|
286
321
|
"""Get example commit messages (valid and invalid)."""
|
|
287
322
|
return {
|
|
288
|
-
'valid':
|
|
289
|
-
'invalid':
|
|
323
|
+
'valid': list(EXAMPLES_VALID),
|
|
324
|
+
'invalid': list(EXAMPLES_INVALID)
|
|
290
325
|
}
|
|
291
326
|
|
|
292
327
|
def get_allowed_types(self) -> List[str]:
|
|
293
328
|
"""Get list of allowed commit types."""
|
|
294
|
-
return
|
|
329
|
+
return list(TYPE_ALLOWED)
|
|
295
330
|
|
|
296
331
|
def format_error_message(self, validation: ValidationResult) -> str:
|
|
297
332
|
"""
|
|
@@ -23,11 +23,75 @@ Usage:
|
|
|
23
23
|
import json
|
|
24
24
|
import os
|
|
25
25
|
import re
|
|
26
|
-
from typing import Dict, List,
|
|
26
|
+
from typing import Dict, List, Optional
|
|
27
27
|
from datetime import datetime
|
|
28
28
|
from dataclasses import dataclass
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Git commit standards -- inlined constants.
|
|
33
|
+
#
|
|
34
|
+
# These were previously loaded from config/git_standards.json. They are now
|
|
35
|
+
# module-level constants: commit_validator.py is the single runtime consumer
|
|
36
|
+
# of these format/subject/body rules, so the JSON indirection added drift risk
|
|
37
|
+
# without any benefit. Footer detection/stripping is NOT here -- it lives,
|
|
38
|
+
# hardcoded, in bash_validator (footers are bash_validator's responsibility).
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
FORMAT = "conventional_commits"
|
|
42
|
+
|
|
43
|
+
TYPE_ALLOWED = (
|
|
44
|
+
"feat",
|
|
45
|
+
"fix",
|
|
46
|
+
"refactor",
|
|
47
|
+
"docs",
|
|
48
|
+
"test",
|
|
49
|
+
"chore",
|
|
50
|
+
"ci",
|
|
51
|
+
"perf",
|
|
52
|
+
"style",
|
|
53
|
+
"build",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
SCOPE_REQUIRED = False
|
|
57
|
+
SCOPE_EXAMPLES = ("helmrelease", "terraform", "pg-non-prod", "infrastructure")
|
|
58
|
+
|
|
59
|
+
SUBJECT_MAX_LENGTH = 72
|
|
60
|
+
SUBJECT_RULES = {
|
|
61
|
+
"capitalize_first_letter": False,
|
|
62
|
+
"no_period_at_end": True,
|
|
63
|
+
"imperative_mood": True,
|
|
64
|
+
"no_emoji": True,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
BODY_MAX_LINE_LENGTH = 72
|
|
68
|
+
BODY_REQUIRED = False
|
|
69
|
+
|
|
70
|
+
EXAMPLES_VALID = (
|
|
71
|
+
"feat(helmrelease): add Phase 3.3 services",
|
|
72
|
+
"fix(pg-non-prod): correct API key environment variable mappings",
|
|
73
|
+
"refactor: simplify context provider logic",
|
|
74
|
+
"docs: update README with new workflow",
|
|
75
|
+
"chore(deps): update terraform to v1.6.0",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
EXAMPLES_INVALID = (
|
|
79
|
+
"Added new feature",
|
|
80
|
+
"Fixed bugs",
|
|
81
|
+
"Updates",
|
|
82
|
+
"feat: add feature\n\n🤖 Generated with Claude Code",
|
|
83
|
+
"feat: add new feature 🚀",
|
|
84
|
+
"fix: 🐛 correct bug",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
ENFORCEMENT = {
|
|
88
|
+
"enabled": True,
|
|
89
|
+
"block_on_failure": True,
|
|
90
|
+
"log_violations": True,
|
|
91
|
+
"log_path": ".claude/logs/commit-violations.jsonl",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
31
95
|
@dataclass
|
|
32
96
|
class ValidationResult:
|
|
33
97
|
"""Result of commit message validation."""
|
|
@@ -44,43 +108,30 @@ class CommitMessageValidator:
|
|
|
44
108
|
"""
|
|
45
109
|
Validates git commit messages against project standards.
|
|
46
110
|
|
|
47
|
-
Standards are
|
|
111
|
+
Standards are inlined as module-level constants (TYPE_ALLOWED,
|
|
112
|
+
SUBJECT_MAX_LENGTH, SUBJECT_RULES, etc.). Footer detection is not handled
|
|
113
|
+
here -- that is bash_validator's responsibility.
|
|
48
114
|
"""
|
|
49
115
|
|
|
50
116
|
def __init__(self, config_path: Optional[str] = None):
|
|
51
117
|
"""
|
|
52
|
-
Initialize validator
|
|
118
|
+
Initialize validator.
|
|
53
119
|
|
|
54
120
|
Args:
|
|
55
|
-
config_path:
|
|
56
|
-
|
|
121
|
+
config_path: Accepted for backward compatibility only. When given,
|
|
122
|
+
it anchors base_path (used to resolve the relative
|
|
123
|
+
violation log path); the rules themselves come from
|
|
124
|
+
the module-level constants, not from any file.
|
|
57
125
|
"""
|
|
58
126
|
if config_path is None:
|
|
59
|
-
#
|
|
60
|
-
# From hooks/modules/validation/ go up to gaia-ops root
|
|
127
|
+
# base_path -> gaia-ops root, used to resolve the violation log.
|
|
61
128
|
# __file__ -> hooks/modules/validation/commit_validator.py
|
|
62
|
-
# dirname(dirname(dirname(dirname(__file__)))) -> gaia-ops root
|
|
63
129
|
base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
64
|
-
config_path = os.path.join(base_path, 'config', 'git_standards.json')
|
|
65
130
|
else:
|
|
66
|
-
# If config_path provided, derive base_path from it
|
|
67
131
|
base_path = os.path.dirname(os.path.dirname(config_path))
|
|
68
132
|
|
|
69
133
|
self.base_path = base_path
|
|
70
|
-
self.
|
|
71
|
-
self.config = self._load_config()
|
|
72
|
-
self.standards = self.config.get('commit_message', {})
|
|
73
|
-
self.enforcement = self.config.get('enforcement', {})
|
|
74
|
-
|
|
75
|
-
def _load_config(self) -> Dict[str, Any]:
|
|
76
|
-
"""Load git standards configuration from JSON file."""
|
|
77
|
-
if not os.path.exists(self.config_path):
|
|
78
|
-
raise FileNotFoundError(
|
|
79
|
-
f"Git standards configuration not found at: {self.config_path}"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
with open(self.config_path, 'r') as f:
|
|
83
|
-
return json.load(f)
|
|
134
|
+
self.enforcement = ENFORCEMENT
|
|
84
135
|
|
|
85
136
|
def validate(self, message: str) -> ValidationResult:
|
|
86
137
|
"""
|
|
@@ -95,19 +146,19 @@ class CommitMessageValidator:
|
|
|
95
146
|
errors = []
|
|
96
147
|
warnings = []
|
|
97
148
|
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
149
|
+
# Note: forbidden-footer detection is intentionally NOT done here.
|
|
150
|
+
# Footers are bash_validator's responsibility (stripping/detection
|
|
151
|
+
# is hardcoded there).
|
|
101
152
|
|
|
102
|
-
#
|
|
153
|
+
# 1. Check conventional commits format
|
|
103
154
|
format_errors = self._check_conventional_format(message)
|
|
104
155
|
errors.extend(format_errors)
|
|
105
156
|
|
|
106
|
-
#
|
|
157
|
+
# 2. Check subject line rules
|
|
107
158
|
subject_errors = self._check_subject_rules(message)
|
|
108
159
|
errors.extend(subject_errors)
|
|
109
160
|
|
|
110
|
-
#
|
|
161
|
+
# 3. Check body rules (warnings only)
|
|
111
162
|
body_warnings = self._check_body_rules(message)
|
|
112
163
|
warnings.extend(body_warnings)
|
|
113
164
|
|
|
@@ -121,22 +172,6 @@ class CommitMessageValidator:
|
|
|
121
172
|
warnings=warnings
|
|
122
173
|
)
|
|
123
174
|
|
|
124
|
-
def _check_forbidden_footers(self, message: str) -> List[Dict[str, str]]:
|
|
125
|
-
"""Check for forbidden footers in commit message."""
|
|
126
|
-
errors = []
|
|
127
|
-
forbidden = self.standards.get('footer_forbidden', [])
|
|
128
|
-
|
|
129
|
-
for forbidden_text in forbidden:
|
|
130
|
-
if forbidden_text.lower() in message.lower():
|
|
131
|
-
errors.append({
|
|
132
|
-
'type': 'FORBIDDEN_FOOTER',
|
|
133
|
-
'message': f"Commit message contains forbidden footer: '{forbidden_text}'",
|
|
134
|
-
'fix': f"Remove all occurrences of '{forbidden_text}'",
|
|
135
|
-
'severity': 'error'
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
return errors
|
|
139
|
-
|
|
140
175
|
def _check_conventional_format(self, message: str) -> List[Dict[str, str]]:
|
|
141
176
|
"""Check if message follows Conventional Commits format."""
|
|
142
177
|
errors = []
|
|
@@ -147,16 +182,16 @@ class CommitMessageValidator:
|
|
|
147
182
|
|
|
148
183
|
# Pattern: type(scope)?: description
|
|
149
184
|
# Examples: feat: add feature, fix(api): correct bug
|
|
150
|
-
allowed_types = '|'.join(
|
|
185
|
+
allowed_types = '|'.join(TYPE_ALLOWED)
|
|
151
186
|
pattern = rf'^({allowed_types})(\(.+?\))?: .+$'
|
|
152
187
|
|
|
153
188
|
if not re.match(pattern, subject):
|
|
154
189
|
errors.append({
|
|
155
190
|
'type': 'INVALID_FORMAT',
|
|
156
191
|
'message': 'Commit message does not follow Conventional Commits format',
|
|
157
|
-
'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(
|
|
192
|
+
'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(TYPE_ALLOWED)}",
|
|
158
193
|
'severity': 'error',
|
|
159
|
-
'examples':
|
|
194
|
+
'examples': list(EXAMPLES_VALID)
|
|
160
195
|
})
|
|
161
196
|
|
|
162
197
|
return errors
|
|
@@ -175,7 +210,7 @@ class CommitMessageValidator:
|
|
|
175
210
|
description = match.group(2)
|
|
176
211
|
|
|
177
212
|
# Check max length
|
|
178
|
-
max_length =
|
|
213
|
+
max_length = SUBJECT_MAX_LENGTH
|
|
179
214
|
if len(subject) > max_length:
|
|
180
215
|
errors.append({
|
|
181
216
|
'type': 'SUBJECT_TOO_LONG',
|
|
@@ -185,7 +220,7 @@ class CommitMessageValidator:
|
|
|
185
220
|
})
|
|
186
221
|
|
|
187
222
|
# Check for period at end
|
|
188
|
-
rules =
|
|
223
|
+
rules = SUBJECT_RULES
|
|
189
224
|
if rules.get('no_period_at_end', True) and description.endswith('.'):
|
|
190
225
|
errors.append({
|
|
191
226
|
'type': 'SUBJECT_ENDS_WITH_PERIOD',
|
|
@@ -242,7 +277,7 @@ class CommitMessageValidator:
|
|
|
242
277
|
})
|
|
243
278
|
|
|
244
279
|
# Check body line length
|
|
245
|
-
max_length =
|
|
280
|
+
max_length = BODY_MAX_LINE_LENGTH
|
|
246
281
|
for i, line in enumerate(lines[2:], start=3): # Skip subject and blank line
|
|
247
282
|
if len(line) > max_length and not line.startswith('http'):
|
|
248
283
|
warnings.append({
|
|
@@ -285,13 +320,13 @@ class CommitMessageValidator:
|
|
|
285
320
|
def get_examples(self) -> Dict[str, List[str]]:
|
|
286
321
|
"""Get example commit messages (valid and invalid)."""
|
|
287
322
|
return {
|
|
288
|
-
'valid':
|
|
289
|
-
'invalid':
|
|
323
|
+
'valid': list(EXAMPLES_VALID),
|
|
324
|
+
'invalid': list(EXAMPLES_INVALID)
|
|
290
325
|
}
|
|
291
326
|
|
|
292
327
|
def get_allowed_types(self) -> List[str]:
|
|
293
328
|
"""Get list of allowed commit types."""
|
|
294
|
-
return
|
|
329
|
+
return list(TYPE_ALLOWED)
|
|
295
330
|
|
|
296
331
|
def format_error_message(self, validation: ValidationResult) -> str:
|
|
297
332
|
"""
|