@jaguilar87/gaia 5.0.2 → 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/ARCHITECTURE.md +0 -1
- package/CHANGELOG.md +110 -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/approvals.py +23 -21
- 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 -2
- 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 -2
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +18 -0
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +214 -2
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +26 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +15 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -5
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +124 -19
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +99 -7
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +127 -24
- 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/agent-contract-handoff/SKILL.md +3 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +4 -2
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -3
- 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 +30 -7
- package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +32 -15
- 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/skills/security-tiers/SKILL.md +5 -1
- package/dist/gaia-ops/skills/security-tiers/reference.md +3 -1
- package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +43 -6
- package/dist/gaia-ops/skills/subagent-request-approval/reference.md +66 -16
- package/dist/gaia-ops/tools/context/README.md +1 -1
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +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/contract_validator.py +18 -0
- package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +214 -2
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +26 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +15 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +0 -5
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +124 -19
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +99 -7
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +127 -24
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
- package/gaia/state/transitions.py +4 -4
- package/gaia/store/writer.py +56 -0
- package/hooks/modules/README.md +2 -4
- package/hooks/modules/agents/contract_validator.py +18 -0
- package/hooks/modules/agents/handoff_persister.py +214 -2
- package/hooks/modules/agents/response_contract.py +26 -0
- package/hooks/modules/agents/transcript_reader.py +15 -0
- package/hooks/modules/security/__init__.py +0 -5
- package/hooks/modules/security/approval_grants.py +124 -19
- package/hooks/modules/security/mutative_verbs.py +99 -7
- package/hooks/modules/tools/bash_validator.py +127 -24
- 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/agent-contract-handoff/SKILL.md +3 -0
- package/skills/agent-response/SKILL.md +4 -2
- package/skills/gaia-patterns/SKILL.md +1 -1
- package/skills/gaia-patterns/reference.md +2 -3
- 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 +30 -7
- package/skills/orchestrator-present-approval/reference.md +32 -15
- package/skills/readme-writing/SKILL.md +1 -1
- package/skills/readme-writing/reference.md +0 -1
- package/skills/security-tiers/SKILL.md +5 -1
- package/skills/security-tiers/reference.md +3 -1
- package/skills/subagent-request-approval/SKILL.md +43 -6
- package/skills/subagent-request-approval/reference.md +66 -16
- package/tools/context/README.md +1 -1
- package/tools/gaia_simulator/extractor.py +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/hooks/modules/security/gitops_validator.py +0 -179
- 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/dist/gaia-security/hooks/modules/security/gitops_validator.py +0 -179
- package/git-hooks/commit-msg +0 -41
- package/hooks/modules/security/gitops_validator.py +0 -179
- 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
|
@@ -151,10 +151,10 @@ MUTATIVE_VERBS: FrozenSet[str] = frozenset({
|
|
|
151
151
|
"disconnect", "unbind", "force-delete", "force-remove", "erase",
|
|
152
152
|
# Collaboration (GitHub/GitLab CLI)
|
|
153
153
|
"comment", "label", "annotate", "approve", "close", "reopen", "tag",
|
|
154
|
-
# Helm-specific
|
|
155
|
-
"uninstall",
|
|
156
154
|
# HTTP methods (e.g., glab api -X POST, gh api -X DELETE)
|
|
157
|
-
|
|
155
|
+
# NOTE: "put" and "patch" already appear under Modification above, and
|
|
156
|
+
# "uninstall" under Deletion/removal -- so only "post" is new here.
|
|
157
|
+
"post",
|
|
158
158
|
})
|
|
159
159
|
|
|
160
160
|
SIMULATION_VERBS: FrozenSet[str] = frozenset({
|
|
@@ -283,6 +283,12 @@ COMMAND_SUBCOMMAND_TIER_EXCEPTIONS: Dict[Tuple[str, str], str] = {
|
|
|
283
283
|
# `gaia ac <verb>` (add/remove/edit/show/list/set-status): local acceptance-
|
|
284
284
|
# criteria bookkeeping — reversible, no external effects.
|
|
285
285
|
("gaia", "ac"): CATEGORY_READ_ONLY,
|
|
286
|
+
# `gaia plan <verb>` (save/edit/show/list/set-status): local planning
|
|
287
|
+
# bookkeeping in the plan store — reversible, no external effects. Anchored
|
|
288
|
+
# here (not left to the SIMULATION_VERBS['plan'] lexical collision) so the
|
|
289
|
+
# exemption is explicit and carries the same DENY-verb guard as `gaia brief`:
|
|
290
|
+
# `gaia plan delete` (whole-record destruction) stays T3.
|
|
291
|
+
("gaia", "plan"): CATEGORY_READ_ONLY,
|
|
286
292
|
}
|
|
287
293
|
|
|
288
294
|
# Verbs that stay gated even under an excepted group above. The exception
|
|
@@ -294,6 +300,37 @@ COMMAND_SUBCOMMAND_EXCEPTION_DENY_VERBS: FrozenSet[str] = frozenset({
|
|
|
294
300
|
})
|
|
295
301
|
|
|
296
302
|
|
|
303
|
+
# ============================================================================
|
|
304
|
+
# PRINCIPLE: consent-REDUCING operations are not T3.
|
|
305
|
+
# ----------------------------------------------------------------------------
|
|
306
|
+
# An operation requires T3 approval because it GRANTS capability or DESTROYS
|
|
307
|
+
# state — it moves the system toward *more* power or *less* recoverability, the
|
|
308
|
+
# directions that need informed consent. An operation that REVOKES, REJECTS,
|
|
309
|
+
# or CLEANS a consent grant Gaia itself issued moves in the opposite direction:
|
|
310
|
+
# it can only REDUCE the capability already granted. It never grants anything
|
|
311
|
+
# and never reaches outside the local approval store. Gating it creates an
|
|
312
|
+
# absurd loop — you would need an approval to clean up approvals.
|
|
313
|
+
#
|
|
314
|
+
# So: within Gaia's own consent layer (`gaia approvals ...`), verbs that REDUCE
|
|
315
|
+
# consent are exempted to read-only; the one verb that GRANTS capability
|
|
316
|
+
# (`approve`) is deliberately NOT in this set and stays T3. That asymmetry is
|
|
317
|
+
# the whole point: `approve` hands out capability without the AskUserQuestion
|
|
318
|
+
# flow, so it must remain gated; `revoke`/`reject`/`reject-all`/`clean` only
|
|
319
|
+
# take capability back, so they must not be.
|
|
320
|
+
#
|
|
321
|
+
# This is anchored to (base_cmd, group) so it applies ONLY to Gaia's own
|
|
322
|
+
# consent store, not to any other CLI's notion of "revoke"/"reject" (e.g. a
|
|
323
|
+
# cloud IAM revoke is a real remote mutation and must stay T3).
|
|
324
|
+
#
|
|
325
|
+
# Key: (base_cmd, subcommand-group) — e.g. ("gaia", "approvals").
|
|
326
|
+
# Value: frozenset of consent-REDUCING verbs under that group that are exempt.
|
|
327
|
+
CONSENT_REDUCING_SUBCOMMAND_EXCEPTIONS: Dict[Tuple[str, str], FrozenSet[str]] = {
|
|
328
|
+
("gaia", "approvals"): frozenset({
|
|
329
|
+
"revoke", "reject", "reject-all", "clean",
|
|
330
|
+
}),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
297
334
|
# ============================================================================
|
|
298
335
|
# Inline Code Detection — Language-Agnostic 3-Layer Approach
|
|
299
336
|
# ============================================================================
|
|
@@ -1159,10 +1196,30 @@ def detect_mutative_command(command: str) -> MutativeResult:
|
|
|
1159
1196
|
group_verb.split("-", 1)[0] in COMMAND_SUBCOMMAND_EXCEPTION_DENY_VERBS
|
|
1160
1197
|
or group_verb in COMMAND_SUBCOMMAND_EXCEPTION_DENY_VERBS
|
|
1161
1198
|
)
|
|
1162
|
-
if
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1199
|
+
if subcommand_key in COMMAND_SUBCOMMAND_TIER_EXCEPTIONS:
|
|
1200
|
+
if verb_is_destructive:
|
|
1201
|
+
# Whole-record destruction (e.g. `gaia plan delete`) must stay
|
|
1202
|
+
# T3 even inside an excepted group. Anchor it MUTATIVE here
|
|
1203
|
+
# instead of falling through to Step 4: the group token itself
|
|
1204
|
+
# (`plan`) collides lexically with SIMULATION_VERBS['plan'], so
|
|
1205
|
+
# the verb scanner would otherwise mis-classify the whole
|
|
1206
|
+
# command as SIMULATION and silently un-gate the delete. This
|
|
1207
|
+
# explicit return is what makes `gaia plan delete` behave like
|
|
1208
|
+
# `gaia brief delete` (where `brief` has no such collision).
|
|
1209
|
+
dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
|
|
1210
|
+
return MutativeResult(
|
|
1211
|
+
is_mutative=True,
|
|
1212
|
+
category=CATEGORY_MUTATIVE,
|
|
1213
|
+
verb=group_verb.split("-", 1)[0],
|
|
1214
|
+
dangerous_flags=dangerous_flags,
|
|
1215
|
+
cli_family=family,
|
|
1216
|
+
confidence="high",
|
|
1217
|
+
reason=(
|
|
1218
|
+
f"Whole-record destruction "
|
|
1219
|
+
f"'{base_cmd} {semantics.non_flag_tokens[0]} {group_verb}' "
|
|
1220
|
+
f"stays T3 despite the local bookkeeping exception"
|
|
1221
|
+
),
|
|
1222
|
+
)
|
|
1166
1223
|
dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
|
|
1167
1224
|
if not dangerous_flags:
|
|
1168
1225
|
target_category = COMMAND_SUBCOMMAND_TIER_EXCEPTIONS[subcommand_key]
|
|
@@ -1179,6 +1236,41 @@ def detect_mutative_command(command: str) -> MutativeResult:
|
|
|
1179
1236
|
),
|
|
1180
1237
|
)
|
|
1181
1238
|
|
|
1239
|
+
# --- Step 3f: Consent-reducing operations are not T3 (anchored) ---
|
|
1240
|
+
# Within Gaia's own consent layer (`gaia approvals ...`), verbs that REDUCE
|
|
1241
|
+
# consent (revoke/reject/reject-all/clean) can only take back capability
|
|
1242
|
+
# already granted — they never grant anything and never reach outside the
|
|
1243
|
+
# local approval store, so they are not T3. The one consent-GRANTING verb
|
|
1244
|
+
# (`approve`) is deliberately absent from CONSENT_REDUCING_SUBCOMMAND_
|
|
1245
|
+
# EXCEPTIONS and falls through to Step 4, where it stays MUTATIVE/T3. That
|
|
1246
|
+
# asymmetry is the principle: granting capability needs consent, reducing it
|
|
1247
|
+
# does not. Anchored to (base_cmd, group) so it never relaxes another CLI's
|
|
1248
|
+
# "revoke" (e.g. a cloud IAM revoke is a real remote mutation, still T3).
|
|
1249
|
+
# Dangerous flags are still scanned so a slip like `--force` re-gates.
|
|
1250
|
+
if semantics.non_flag_tokens:
|
|
1251
|
+
consent_group_key = (base_cmd, semantics.non_flag_tokens[0])
|
|
1252
|
+
consent_verb = (
|
|
1253
|
+
semantics.non_flag_tokens[1]
|
|
1254
|
+
if len(semantics.non_flag_tokens) > 1 else ""
|
|
1255
|
+
)
|
|
1256
|
+
reducing_verbs = CONSENT_REDUCING_SUBCOMMAND_EXCEPTIONS.get(consent_group_key)
|
|
1257
|
+
if reducing_verbs is not None and consent_verb in reducing_verbs:
|
|
1258
|
+
dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
|
|
1259
|
+
if not dangerous_flags:
|
|
1260
|
+
return MutativeResult(
|
|
1261
|
+
is_mutative=False,
|
|
1262
|
+
category=CATEGORY_READ_ONLY,
|
|
1263
|
+
verb=consent_verb,
|
|
1264
|
+
cli_family=family,
|
|
1265
|
+
confidence="high",
|
|
1266
|
+
reason=(
|
|
1267
|
+
f"Consent-reducing operation "
|
|
1268
|
+
f"'{base_cmd} {semantics.non_flag_tokens[0]} {consent_verb}' "
|
|
1269
|
+
f"only revokes/rejects capability already granted — "
|
|
1270
|
+
f"not state-granting, so not T3"
|
|
1271
|
+
),
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1182
1274
|
# --- Step 4: Scan semantic non-flag tokens near the command head ---
|
|
1183
1275
|
# Priority order: SIMULATION > MUTATIVE > READ_ONLY > ALIASES
|
|
1184
1276
|
for semantic_index, token in enumerate(semantics.semantic_head_tokens[1:], start=1):
|
|
@@ -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
|
|
@@ -32,7 +34,6 @@ from dataclasses import dataclass
|
|
|
32
34
|
|
|
33
35
|
from ..security.tiers import SecurityTier
|
|
34
36
|
from ..security.blocked_commands import is_blocked_command
|
|
35
|
-
from ..security.gitops_validator import validate_gitops_workflow
|
|
36
37
|
from ..security.mutative_verbs import (
|
|
37
38
|
detect_mutative_command,
|
|
38
39
|
build_t3_block_response,
|
|
@@ -96,12 +97,35 @@ class BashValidationResult:
|
|
|
96
97
|
|
|
97
98
|
|
|
98
99
|
# Patterns for AI tool attribution footers (auto-stripped from commits).
|
|
99
|
-
# Covers Claude Code, GitHub Copilot, Aider, Windsurf,
|
|
100
|
-
#
|
|
100
|
+
# Covers Claude Code, GitHub Copilot, Aider, Windsurf, Codex, Gemini, the
|
|
101
|
+
# Anthropic model family (Opus/Sonnet/Haiku), and any future tool using the
|
|
102
|
+
# Co-authored-by git trailer convention.
|
|
103
|
+
#
|
|
104
|
+
# IMPORTANT: this list is the DETECTOR (`_detect_claude_footers`). It MUST stay
|
|
105
|
+
# aligned with the line patterns in `_strip_claude_footers` -- if the stripper
|
|
106
|
+
# can remove a footer the detector cannot see, the strip never fires (the
|
|
107
|
+
# early-normalization guard only strips when the detector returns True). Every
|
|
108
|
+
# footer shape the stripper removes has a corresponding detector entry here.
|
|
109
|
+
#
|
|
110
|
+
# None of these patterns anchor on a newline, so they also catch footers that
|
|
111
|
+
# arrive in a SECOND `-m "..."` argument (no preceding newline) -- the detector
|
|
112
|
+
# fires, and the stripper's `-m`-aware branch removes them.
|
|
101
113
|
FORBIDDEN_FOOTER_PATTERNS = [
|
|
102
114
|
r"Generated with\s+Claude Code",
|
|
103
115
|
r"Generated with\s+\[?Claude Code\]?",
|
|
116
|
+
# Bare robot-emoji "Generated with ..." line (e.g. "🤖 Generated with ...")
|
|
117
|
+
# WITHOUT requiring the literal "Claude Code" after it -- the stripper has
|
|
118
|
+
# always removed this shape; the detector now sees it too.
|
|
119
|
+
r"🤖\s*Generated with",
|
|
120
|
+
# Robot emoji on its own is a strong AI-attribution signal.
|
|
121
|
+
r"🤖",
|
|
104
122
|
r"Co-Authored-By:\s+Claude\b",
|
|
123
|
+
# Anthropic model family attributed via Co-Authored-By / Co-authored-with.
|
|
124
|
+
r"Co-[Aa]uthored-(?:[Bb]y|[Ww]ith):[^\n]*\bOpus\b",
|
|
125
|
+
r"Co-[Aa]uthored-(?:[Bb]y|[Ww]ith):[^\n]*\bSonnet\b",
|
|
126
|
+
r"Co-[Aa]uthored-(?:[Bb]y|[Ww]ith):[^\n]*\bHaiku\b",
|
|
127
|
+
# "Approved-by:" attribution trailer.
|
|
128
|
+
r"Approved-by:",
|
|
105
129
|
r"Co-authored-by:\s+GitHub Copilot\b",
|
|
106
130
|
r"Co-authored-by:\s+aider\b",
|
|
107
131
|
r"Co-authored-by:\s+Windsurf\b",
|
|
@@ -466,7 +490,7 @@ class BashValidator:
|
|
|
466
490
|
# 3d. Smart sanitization (strip nohup, &, redirects)
|
|
467
491
|
# 3e. Cloud pipe/redirect/chain check (corrective deny)
|
|
468
492
|
# 3f. Dispatch to single/compound classification
|
|
469
|
-
# (mutative_verbs,
|
|
493
|
+
# (mutative_verbs, safe-by-elimination)
|
|
470
494
|
# ================================================================
|
|
471
495
|
|
|
472
496
|
# 3a. Blocked commands check on FULL command (exit 2).
|
|
@@ -624,7 +648,7 @@ class BashValidator:
|
|
|
624
648
|
if result.is_mutative:
|
|
625
649
|
# Check for a DB-backed command_set grant first (M3 path).
|
|
626
650
|
# Byte-for-byte match per D10: no normalization.
|
|
627
|
-
cs_match = match_command_set_grant(command
|
|
651
|
+
cs_match = match_command_set_grant(command)
|
|
628
652
|
if cs_match is not None:
|
|
629
653
|
cs_approval_id, cs_index = cs_match
|
|
630
654
|
try:
|
|
@@ -732,17 +756,6 @@ class BashValidator:
|
|
|
732
756
|
agent_type=agent_type,
|
|
733
757
|
)
|
|
734
758
|
|
|
735
|
-
# Check GitOps policy for kubectl/helm/flux commands
|
|
736
|
-
if any(keyword in command for keyword in ("kubectl", "helm", "flux")):
|
|
737
|
-
gitops_result = validate_gitops_workflow(command)
|
|
738
|
-
if not gitops_result.allowed:
|
|
739
|
-
return BashValidationResult(
|
|
740
|
-
allowed=False,
|
|
741
|
-
tier=SecurityTier.T3_BLOCKED,
|
|
742
|
-
reason=f"GitOps policy violation: {gitops_result.reason}",
|
|
743
|
-
suggestions=gitops_result.suggestions,
|
|
744
|
-
)
|
|
745
|
-
|
|
746
759
|
# Flag-dependent classification (sed -i, find -exec, tar -x, etc.)
|
|
747
760
|
# This supplements mutative_verbs -- it catches flag-dependent mutations
|
|
748
761
|
# that verb-based detection misses (e.g. "sed" has no mutative verb, but
|
|
@@ -775,7 +788,7 @@ class BashValidator:
|
|
|
775
788
|
# never honoured and the command re-blocks unconditionally on
|
|
776
789
|
# every retry (the flag path never reaches the matcher). The
|
|
777
790
|
# consume + return semantics replicate the verb branch exactly.
|
|
778
|
-
cs_match = match_command_set_grant(command
|
|
791
|
+
cs_match = match_command_set_grant(command)
|
|
779
792
|
if cs_match is not None:
|
|
780
793
|
cs_approval_id, cs_index = cs_match
|
|
781
794
|
try:
|
|
@@ -1004,32 +1017,77 @@ class BashValidator:
|
|
|
1004
1017
|
|
|
1005
1018
|
def _strip_claude_footers(self, command: str) -> str:
|
|
1006
1019
|
"""
|
|
1007
|
-
Strip
|
|
1020
|
+
Strip AI attribution footers from a commit command.
|
|
1008
1021
|
|
|
1009
1022
|
Removes full lines matching forbidden footer patterns.
|
|
1010
1023
|
Works on raw command string regardless of quoting/HEREDOC format.
|
|
1011
1024
|
Preserves trailing quote/paren characters that close the commit
|
|
1012
1025
|
message (e.g., the closing " in -m "...footer").
|
|
1013
1026
|
|
|
1027
|
+
Covers, kept ALIGNED with FORBIDDEN_FOOTER_PATTERNS (the detector):
|
|
1028
|
+
- Co-authored-by / Co-authored-with: Claude, Copilot, aider,
|
|
1029
|
+
Windsurf, Cursor, Codex, Gemini, and the Anthropic model family
|
|
1030
|
+
(Opus / Sonnet / Haiku)
|
|
1031
|
+
- "Generated with [Claude Code]" and the bare "🤖 Generated with ..."
|
|
1032
|
+
- a bare robot emoji 🤖 line
|
|
1033
|
+
- "Approved-by:" trailers
|
|
1034
|
+
Both newline-anchored footer LINES and footers carried in a SECOND
|
|
1035
|
+
``-m "..."`` argument (no preceding newline) are handled.
|
|
1036
|
+
|
|
1037
|
+
LIMITATION -- ``git commit -F <file>`` / ``--file=<file>``: when the
|
|
1038
|
+
message body lives in a file, the footer is NOT in the command string
|
|
1039
|
+
the PreToolUse hook receives. This stripper CANNOT see or remove it,
|
|
1040
|
+
and deliberately does NOT read the referenced file (reading arbitrary
|
|
1041
|
+
paths from a hook would be an unbounded side effect and a new attack
|
|
1042
|
+
surface). Footer suppression for ``-F`` commits is therefore out of
|
|
1043
|
+
scope here and must be enforced elsewhere (e.g. a commit-msg git hook).
|
|
1044
|
+
|
|
1014
1045
|
Args:
|
|
1015
1046
|
command: Raw command string
|
|
1016
1047
|
|
|
1017
1048
|
Returns:
|
|
1018
1049
|
Command with footer lines removed
|
|
1019
1050
|
"""
|
|
1020
|
-
#
|
|
1051
|
+
# Author/model alternation reused across line- and -m-shaped patterns.
|
|
1052
|
+
_authors = (
|
|
1053
|
+
r"Claude|GitHub Copilot|aider|Windsurf|Cursor|Codex|Gemini"
|
|
1054
|
+
r"|Opus|Sonnet|Haiku"
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
# (1) Remove full lines that contain AI attribution patterns.
|
|
1021
1058
|
# Each pattern matches the newline + footer content, then uses a
|
|
1022
1059
|
# lookahead to stop before any trailing quote/paren/bracket
|
|
1023
1060
|
# sequence that closes the command structure. The captured group
|
|
1024
1061
|
# is replaced with empty string, leaving the closing chars intact.
|
|
1025
1062
|
footer_line_patterns = [
|
|
1026
|
-
r'\n\s*Co-[Aa]uthored-[Bb]y:\s+(?:
|
|
1063
|
+
r'\n\s*Co-[Aa]uthored-(?:[Bb]y|[Ww]ith):\s+(?:' + _authors + r')[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
1064
|
+
# Co-authored-* lines naming an Anthropic model anywhere on the line.
|
|
1065
|
+
r'\n\s*Co-[Aa]uthored-(?:[Bb]y|[Ww]ith):[^\n]*?\b(?:Opus|Sonnet|Haiku)\b[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
1066
|
+
r'\n\s*Approved-by:[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
1027
1067
|
r'\n\s*Generated with\s+\[?Claude Code\]?[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
1028
1068
|
r'\n\s*🤖\s*Generated with[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
1069
|
+
# Bare robot-emoji line (emoji not followed by "Generated with").
|
|
1070
|
+
r'\n\s*🤖[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
1029
1071
|
]
|
|
1030
1072
|
for pattern in footer_line_patterns:
|
|
1031
1073
|
command = re.sub(pattern, '', command, flags=re.IGNORECASE)
|
|
1032
1074
|
|
|
1075
|
+
# (2) Remove footers carried in a SEPARATE ``-m "..."`` / ``-m '...'``
|
|
1076
|
+
# argument. Repeated ``-m`` flags are concatenated by git as separate
|
|
1077
|
+
# paragraphs, so an attribution footer often arrives as
|
|
1078
|
+
# git commit -m "real message" -m "Co-Authored-By: ... Opus"
|
|
1079
|
+
# with NO preceding newline -- the line patterns above cannot see it.
|
|
1080
|
+
# Drop the entire trailing ``-m "<footer>"`` flag+value when its value
|
|
1081
|
+
# is (essentially) just an attribution footer.
|
|
1082
|
+
m_footer_patterns = [
|
|
1083
|
+
r'''\s+-m\s+(["'])\s*Co-[Aa]uthored-(?:[Bb]y|[Ww]ith):\s+(?:''' + _authors + r''')[^"']*\1''',
|
|
1084
|
+
r'''\s+-m\s+(["'])\s*Approved-by:[^"']*\1''',
|
|
1085
|
+
r'''\s+-m\s+(["'])\s*🤖[^"']*\1''',
|
|
1086
|
+
r'''\s+-m\s+(["'])\s*Generated with\s+\[?Claude Code\]?[^"']*\1''',
|
|
1087
|
+
]
|
|
1088
|
+
for pattern in m_footer_patterns:
|
|
1089
|
+
command = re.sub(pattern, '', command, flags=re.IGNORECASE)
|
|
1090
|
+
|
|
1033
1091
|
# Clean up trailing whitespace inside quotes/heredoc
|
|
1034
1092
|
# Collapse 3+ consecutive newlines to 2
|
|
1035
1093
|
command = re.sub(r'\n{3,}', '\n\n', command)
|
|
@@ -1222,6 +1280,7 @@ def _build_sealed_payload(
|
|
|
1222
1280
|
verb: str,
|
|
1223
1281
|
category: str,
|
|
1224
1282
|
agent_type: str = "",
|
|
1283
|
+
command_set: list | None = None,
|
|
1225
1284
|
) -> dict:
|
|
1226
1285
|
"""Build a sealed_payload dict from hook-intercepted command context.
|
|
1227
1286
|
|
|
@@ -1229,16 +1288,51 @@ def _build_sealed_payload(
|
|
|
1229
1288
|
and calls store.insert_requested(). The 7 D13 fields are populated from
|
|
1230
1289
|
what is available at intercept time.
|
|
1231
1290
|
|
|
1291
|
+
Single vs. multi-command (COMMAND_SET):
|
|
1292
|
+
By default this builds a SINGLE-command payload -- ``commands`` is
|
|
1293
|
+
``[command]`` and no ``command_set`` key is present, so activation
|
|
1294
|
+
mints a single-use SCOPE_SEMANTIC_SIGNATURE grant.
|
|
1295
|
+
|
|
1296
|
+
When ``command_set`` is supplied (a list of ``{command, rationale}``
|
|
1297
|
+
dicts representing more than one command the agent wants under ONE
|
|
1298
|
+
consent), the payload additionally carries a ``command_set`` key
|
|
1299
|
+
verbatim and ``commands`` lists every command string in the set. This
|
|
1300
|
+
is the signal ``activate_db_pending_by_prefix`` reads to branch into
|
|
1301
|
+
``create_command_set_grant`` instead of degrading to a single command.
|
|
1302
|
+
The set is NOT collapsed -- every item survives into the grant.
|
|
1303
|
+
|
|
1232
1304
|
Args:
|
|
1233
|
-
command: The full Bash command string that was blocked
|
|
1305
|
+
command: The full Bash command string that was blocked (the primary /
|
|
1306
|
+
first command; used for ``exact_content`` and the singular display).
|
|
1234
1307
|
verb: The detected mutative verb (e.g. 'push', 'delete').
|
|
1235
1308
|
category: The verb category string (e.g. 'MUTATIVE').
|
|
1236
1309
|
agent_type: Name of the originating agent (may be empty).
|
|
1310
|
+
command_set: Optional list of ``{command, rationale}`` dicts. When it
|
|
1311
|
+
contains more than one item, the payload becomes a COMMAND_SET
|
|
1312
|
+
envelope. A list with a single item (or None) keeps the singular
|
|
1313
|
+
semantic-signature behaviour.
|
|
1237
1314
|
|
|
1238
1315
|
Returns:
|
|
1239
|
-
Dict with the 7 sealed_payload fields from D13
|
|
1316
|
+
Dict with the 7 sealed_payload fields from D13, plus an optional
|
|
1317
|
+
``command_set`` key when a multi-command set was supplied.
|
|
1240
1318
|
"""
|
|
1241
|
-
|
|
1319
|
+
# Normalize the command_set into the canonical [{command, rationale}, ...]
|
|
1320
|
+
# shape and decide whether this is a genuine multi-command envelope. A set
|
|
1321
|
+
# of length <= 1 is NOT multi-command -- it stays the singular path so we
|
|
1322
|
+
# never mint a COMMAND_SET grant for one command.
|
|
1323
|
+
normalized_set: list = []
|
|
1324
|
+
if command_set:
|
|
1325
|
+
for item in command_set:
|
|
1326
|
+
if isinstance(item, dict) and item.get("command"):
|
|
1327
|
+
normalized_set.append(
|
|
1328
|
+
{
|
|
1329
|
+
"command": item["command"],
|
|
1330
|
+
"rationale": item.get("rationale", ""),
|
|
1331
|
+
}
|
|
1332
|
+
)
|
|
1333
|
+
is_command_set = len(normalized_set) > 1
|
|
1334
|
+
|
|
1335
|
+
payload = {
|
|
1242
1336
|
"operation": f"{category} command intercepted: {verb}",
|
|
1243
1337
|
"exact_content": command,
|
|
1244
1338
|
"scope": command.split()[0] if command.strip() else "unknown",
|
|
@@ -1250,9 +1344,18 @@ def _build_sealed_payload(
|
|
|
1250
1344
|
if agent_type
|
|
1251
1345
|
else f"A {category.lower()} ({verb}) command requires user approval per T3 policy."
|
|
1252
1346
|
),
|
|
1253
|
-
"commands":
|
|
1347
|
+
"commands": (
|
|
1348
|
+
[it["command"] for it in normalized_set] if is_command_set else [command]
|
|
1349
|
+
),
|
|
1254
1350
|
}
|
|
1255
1351
|
|
|
1352
|
+
if is_command_set:
|
|
1353
|
+
# Carry the full {command, rationale} set verbatim. This is the
|
|
1354
|
+
# multi-command signal the activation path branches on.
|
|
1355
|
+
payload["command_set"] = normalized_set
|
|
1356
|
+
|
|
1357
|
+
return payload
|
|
1358
|
+
|
|
1256
1359
|
|
|
1257
1360
|
def decide_t3_outcome(
|
|
1258
1361
|
command: str,
|
|
@@ -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
|
"""
|
|
@@ -10,10 +10,10 @@ The Contract workflow transitions live in
|
|
|
10
10
|
here for symmetry; the brief lifecycle transitions live in
|
|
11
11
|
``gaia.briefs.store._LEGAL_TRANSITIONS`` and are likewise imported.
|
|
12
12
|
|
|
13
|
-
The plan and task lifecycle transitions are defined here directly
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
``
|
|
13
|
+
The plan and task lifecycle transitions are defined here directly. The
|
|
14
|
+
``gaia plan`` and ``gaia task`` commands (``bin/cli/plan.py`` and
|
|
15
|
+
``bin/cli/task.py``) consume these helpers when driving the ``plans`` and
|
|
16
|
+
``tasks`` lifecycle.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|