@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.
Files changed (113) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/INSTALL.md +0 -2
  5. package/README.md +1 -6
  6. package/bin/README.md +0 -1
  7. package/bin/cli/_install_helpers.py +1 -1
  8. package/bin/cli/cleanup.py +0 -1
  9. package/bin/cli/doctor.py +1 -1
  10. package/bin/cli/memory.py +2 -0
  11. package/bin/cli/update.py +1 -1
  12. package/bin/pre-publish-validate.js +48 -5
  13. package/config/README.md +22 -44
  14. package/config/surface-routing.json +0 -1
  15. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  16. package/dist/gaia-ops/config/README.md +22 -44
  17. package/dist/gaia-ops/config/surface-routing.json +0 -1
  18. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +2 -0
  19. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +2 -0
  20. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +2 -0
  21. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +90 -55
  22. package/dist/gaia-ops/skills/README.md +1 -1
  23. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
  24. package/dist/gaia-ops/skills/gaia-patterns/reference.md +0 -1
  25. package/dist/gaia-ops/skills/gaia-release/SKILL.md +60 -24
  26. package/dist/gaia-ops/skills/gaia-release/reference.md +35 -11
  27. package/dist/gaia-ops/skills/git-conventions/SKILL.md +6 -2
  28. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +10 -2
  29. package/dist/gaia-ops/skills/readme-writing/SKILL.md +1 -1
  30. package/dist/gaia-ops/skills/readme-writing/reference.md +0 -1
  31. package/dist/gaia-ops/tools/scan/ui.py +20 -4
  32. package/dist/gaia-ops/tools/scan/verify.py +3 -3
  33. package/dist/gaia-ops/tools/validation/README.md +15 -24
  34. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  35. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +2 -0
  36. package/dist/gaia-security/hooks/modules/security/approval_grants.py +2 -0
  37. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +2 -0
  38. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
  39. package/hooks/modules/agents/handoff_persister.py +2 -0
  40. package/hooks/modules/security/approval_grants.py +2 -0
  41. package/hooks/modules/tools/bash_validator.py +2 -0
  42. package/hooks/modules/validation/commit_validator.py +90 -55
  43. package/index.js +2 -12
  44. package/package.json +4 -6
  45. package/pyproject.toml +3 -3
  46. package/scripts/bootstrap_database.sh +88 -439
  47. package/scripts/check_schema_drift.py +208 -0
  48. package/scripts/migrations/README.md +78 -28
  49. package/scripts/migrations/schema.checksum +8 -0
  50. package/scripts/release-prepare.mjs +199 -0
  51. package/skills/README.md +1 -1
  52. package/skills/gaia-patterns/SKILL.md +1 -1
  53. package/skills/gaia-patterns/reference.md +0 -1
  54. package/skills/gaia-release/SKILL.md +60 -24
  55. package/skills/gaia-release/reference.md +35 -11
  56. package/skills/git-conventions/SKILL.md +6 -2
  57. package/skills/orchestrator-present-approval/SKILL.md +10 -2
  58. package/skills/readme-writing/SKILL.md +1 -1
  59. package/skills/readme-writing/reference.md +0 -1
  60. package/tools/scan/ui.py +20 -4
  61. package/tools/scan/verify.py +3 -3
  62. package/tools/validation/README.md +15 -24
  63. package/commands/README.md +0 -64
  64. package/commands/gaia.md +0 -37
  65. package/commands/scan-project.md +0 -74
  66. package/config/crons-schema.md +0 -81
  67. package/config/git_standards.json +0 -72
  68. package/dist/gaia-ops/commands/gaia.md +0 -37
  69. package/dist/gaia-ops/config/crons-schema.md +0 -81
  70. package/dist/gaia-ops/config/git_standards.json +0 -72
  71. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +0 -210
  72. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +0 -106
  73. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +0 -223
  74. package/git-hooks/commit-msg +0 -41
  75. package/scripts/migrations/v10_to_v11.sql +0 -170
  76. package/scripts/migrations/v10_to_v11_fresh.sql +0 -18
  77. package/scripts/migrations/v11_to_v12.sql +0 -195
  78. package/scripts/migrations/v11_to_v12_fresh.sql +0 -19
  79. package/scripts/migrations/v12_to_v13.sql +0 -48
  80. package/scripts/migrations/v12_to_v13_fresh.sql +0 -17
  81. package/scripts/migrations/v13_to_v14.sql +0 -44
  82. package/scripts/migrations/v13_to_v14_fresh.sql +0 -17
  83. package/scripts/migrations/v14_to_v15.sql +0 -71
  84. package/scripts/migrations/v14_to_v15_fresh.sql +0 -19
  85. package/scripts/migrations/v15_to_v16.sql +0 -57
  86. package/scripts/migrations/v15_to_v16_fresh.sql +0 -18
  87. package/scripts/migrations/v16_to_v17.sql +0 -51
  88. package/scripts/migrations/v16_to_v17_fresh.sql +0 -18
  89. package/scripts/migrations/v17_to_v18.sql +0 -66
  90. package/scripts/migrations/v17_to_v18_fresh.sql +0 -24
  91. package/scripts/migrations/v1_to_v2.sql +0 -97
  92. package/scripts/migrations/v2_to_v3.sql +0 -68
  93. package/scripts/migrations/v2_to_v3_merge.sql +0 -69
  94. package/scripts/migrations/v3_to_v4.sql +0 -67
  95. package/scripts/migrations/v3_to_v4_fresh.sql +0 -20
  96. package/scripts/migrations/v4_to_v5.sql +0 -55
  97. package/scripts/migrations/v4_to_v5_fresh.sql +0 -20
  98. package/scripts/migrations/v5_to_v6.sql +0 -48
  99. package/scripts/migrations/v5_to_v6_fresh.sql +0 -17
  100. package/scripts/migrations/v6_to_v7.sql +0 -26
  101. package/scripts/migrations/v6_to_v7_fresh.sql +0 -13
  102. package/scripts/migrations/v7_to_v8.sql +0 -44
  103. package/scripts/migrations/v7_to_v8_fresh.sql +0 -14
  104. package/scripts/migrations/v8_to_v9.sql +0 -87
  105. package/scripts/migrations/v8_to_v9_fresh.sql +0 -15
  106. package/scripts/migrations/v9_to_v10.sql +0 -109
  107. package/scripts/migrations/v9_to_v10_episodes_workspace.sql +0 -109
  108. package/scripts/migrations/v9_to_v10_fresh.sql +0 -18
  109. package/templates/README.md +0 -70
  110. package/templates/managed-settings.template.json +0 -43
  111. package/tools/agentic-loop/decide-status.py +0 -210
  112. package/tools/agentic-loop/parse-metric.py +0 -106
  113. 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, templates, config,
49
- skills, CHANGELOG.md (8 total).
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
- "templates", "config", "skills",
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:** `.claude/config/git_standards.json` (SSOT)
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:** `.claude/config/git_standards.json`
176
+ **Git Standards:** Inlined as module-level constants in `hooks/modules/validation/commit_validator.py`.
182
177
 
183
178
  Example:
184
- ```json
185
- {
186
- "commit_message": {
187
- "type_allowed": ["feat", "fix", "refactor", "docs", "test", "chore"],
188
- "subject_max_length": 72,
189
- "footer_forbidden": ["Generated with Claude Code"]
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
- - `.claude/config/git_standards.json` - Git standards configuration
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.4",
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",
@@ -10,6 +10,8 @@ arise if the adapter imported _persist_handoff directly from subagent_stop
10
10
  (which itself imports from the adapter's dependency tree).
11
11
  """
12
12
 
13
+ from __future__ import annotations
14
+
13
15
  import logging
14
16
 
15
17
  logger = logging.getLogger(__name__)
@@ -76,6 +76,8 @@ fallback plane retained for grants created before the DB cutover. The active
76
76
  flow runs through the DB plane in gaia.store.writer.
77
77
  """
78
78
 
79
+ from __future__ import annotations
80
+
79
81
  import json
80
82
  import logging
81
83
  import os
@@ -23,6 +23,8 @@ Earlier flat-pipeline order preserved within phases for backward compat:
23
23
  - Blocked commands run before cloud_pipe and mutative_verbs in phase 3
24
24
  """
25
25
 
26
+ from __future__ import annotations
27
+
26
28
  import os
27
29
  import re
28
30
  import json
@@ -23,11 +23,75 @@ Usage:
23
23
  import json
24
24
  import os
25
25
  import re
26
- from typing import Dict, List, Any, Optional
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 defined in .claude/config/git_standards.json
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 with configuration.
118
+ Initialize validator.
53
119
 
54
120
  Args:
55
- config_path: Optional path to git_standards.json
56
- If None, uses default location
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
- # Default path relative to this file
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.config_path = config_path
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
- # 1. Check for forbidden footers (CRITICAL)
99
- footer_errors = self._check_forbidden_footers(message)
100
- errors.extend(footer_errors)
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
- # 2. Check conventional commits format
153
+ # 1. Check conventional commits format
103
154
  format_errors = self._check_conventional_format(message)
104
155
  errors.extend(format_errors)
105
156
 
106
- # 3. Check subject line rules
157
+ # 2. Check subject line rules
107
158
  subject_errors = self._check_subject_rules(message)
108
159
  errors.extend(subject_errors)
109
160
 
110
- # 4. Check body rules (warnings only)
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(self.standards.get('type_allowed', []))
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(self.standards.get('type_allowed', []))}",
192
+ 'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(TYPE_ALLOWED)}",
158
193
  'severity': 'error',
159
- 'examples': self.standards.get('examples_valid', [])
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 = self.standards.get('subject_max_length', 72)
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 = self.standards.get('subject_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 = self.standards.get('body_max_line_length', 72)
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': self.standards.get('examples_valid', []),
289
- 'invalid': self.standards.get('examples_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 self.standards.get('type_allowed', [])
329
+ return list(TYPE_ALLOWED)
295
330
 
296
331
  def format_error_message(self, validation: ValidationResult) -> str:
297
332
  """
@@ -10,6 +10,8 @@ arise if the adapter imported _persist_handoff directly from subagent_stop
10
10
  (which itself imports from the adapter's dependency tree).
11
11
  """
12
12
 
13
+ from __future__ import annotations
14
+
13
15
  import logging
14
16
 
15
17
  logger = logging.getLogger(__name__)
@@ -76,6 +76,8 @@ fallback plane retained for grants created before the DB cutover. The active
76
76
  flow runs through the DB plane in gaia.store.writer.
77
77
  """
78
78
 
79
+ from __future__ import annotations
80
+
79
81
  import json
80
82
  import logging
81
83
  import os
@@ -23,6 +23,8 @@ Earlier flat-pipeline order preserved within phases for backward compat:
23
23
  - Blocked commands run before cloud_pipe and mutative_verbs in phase 3
24
24
  """
25
25
 
26
+ from __future__ import annotations
27
+
26
28
  import os
27
29
  import re
28
30
  import json
@@ -23,11 +23,75 @@ Usage:
23
23
  import json
24
24
  import os
25
25
  import re
26
- from typing import Dict, List, Any, Optional
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 defined in .claude/config/git_standards.json
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 with configuration.
118
+ Initialize validator.
53
119
 
54
120
  Args:
55
- config_path: Optional path to git_standards.json
56
- If None, uses default location
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
- # Default path relative to this file
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.config_path = config_path
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
- # 1. Check for forbidden footers (CRITICAL)
99
- footer_errors = self._check_forbidden_footers(message)
100
- errors.extend(footer_errors)
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
- # 2. Check conventional commits format
153
+ # 1. Check conventional commits format
103
154
  format_errors = self._check_conventional_format(message)
104
155
  errors.extend(format_errors)
105
156
 
106
- # 3. Check subject line rules
157
+ # 2. Check subject line rules
107
158
  subject_errors = self._check_subject_rules(message)
108
159
  errors.extend(subject_errors)
109
160
 
110
- # 4. Check body rules (warnings only)
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(self.standards.get('type_allowed', []))
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(self.standards.get('type_allowed', []))}",
192
+ 'fix': f"Use format: type(scope): description\nAllowed types: {', '.join(TYPE_ALLOWED)}",
158
193
  'severity': 'error',
159
- 'examples': self.standards.get('examples_valid', [])
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 = self.standards.get('subject_max_length', 72)
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 = self.standards.get('subject_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 = self.standards.get('body_max_line_length', 72)
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': self.standards.get('examples_valid', []),
289
- 'invalid': self.standards.get('examples_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 self.standards.get('type_allowed', [])
329
+ return list(TYPE_ALLOWED)
295
330
 
296
331
  def format_error_message(self, validation: ValidationResult) -> str:
297
332
  """