Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/agent_tools/core/models/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class Result:
workflow: str = ""
next_step: str = "" # Semantic label for the next logical activity (e.g., 'resolve_conflicts')
resume_point: str = "" # Machine-readable string to pass back to the orchestrator (e.g., '--point current_rebase')
instruction: str = "" # Specific, actionable instructions for the LLM agent
constraints: list[str] = field(default_factory=list) # Explicit prohibitions for the LLM agent
strict_protocol: bool = True # If True, the LLM must strictly follow the workflow and never bypass it
instruction: str = "" # Specific, actionable instructions and explicit prohibitions for the LLM agent
details: dict[str, Any] = field(default_factory=dict)

def to_json(self) -> str:
Expand Down
15 changes: 8 additions & 7 deletions src/agent_tools/core/orchestrators/gh_pr_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
)

from agent_tools.infrastructure.config.manager import (
get_full_commit_rules,
get_commit_subject_regex,
get_commit_body_wrap_length,
)
from agent_tools.infrastructure.clients.github.client import run_gh

Expand Down Expand Up @@ -114,13 +115,12 @@ def _handle_sense() -> Result:
"【STRICT PROTOCOL / 严格协议】您当前处于受控工作流中。禁止跳过步骤、禁止执行任何未授权的裸命令。\n"
"【ACTION】\n"
"1. Review 'commits' in details.\n"
"2. Synthesize a professional PR title (Conventional Commit style) and a structured markdown body.\n"
"2. Synthesize a professional PR title for `draft_json_str` following the 'details.json_format' layout.\n"
" - 'title': A professional pr commit subject. 【STRICT】MUST satisfy 'details.subject_regex'.\n"
" - 'body': The detailed body should explain the 'why' and 'how' (not just 'what'), especially for complex logic changes. 【STRICT】single line max `details.body_wrap_length` chars.\n"
"3. Mention any relevant issue numbers if known.\n"
"4. Call 'gh_pr_create_flow' with point='create' and your 'draft_json_str', formatted according to 'details.json_format'."
"4. Call 'gh_pr_create_flow' with point='create' and your 'draft_json_str', formatted according to 'details.json_format'.\n"
),
constraints=[
"Each commit message MUST follow Conventional Commits and `commit_rules`.",
],
details={
"owner": repo_info.owner,
"repo": repo_info.repo,
Expand All @@ -131,7 +131,8 @@ def _handle_sense() -> Result:
"title": "feat(core): add JSON schema to results",
"body": "## Summary\n- Added json_format to handoff details.\n\n## Test Plan\n- Verified via pytest.",
},
"commit_rules": get_full_commit_rules(),
"subject_regex": get_commit_subject_regex(),
"body_wrap_length": get_commit_body_wrap_length(),
},
)

Expand Down
27 changes: 15 additions & 12 deletions src/agent_tools/core/orchestrators/gh_pr_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
run_git,
)
from agent_tools.infrastructure.clients.github.client import run_gh
from agent_tools.infrastructure.config.manager import get_full_commit_rules
from agent_tools.infrastructure.config.manager import (
get_commit_subject_regex,
get_commit_body_wrap_length,
)

logger = logging.getLogger(__name__)
WORKFLOW = "gh_pr_merge"
Expand Down Expand Up @@ -166,20 +169,20 @@ def _handle_sense() -> Result:
instruction=(
"【STRICT PROTOCOL / 严格协议】您当前处于受控工作流中。禁止跳过步骤、禁止执行任何未授权的裸命令。\n"
"【ACTION】\n"
"1. Review PR metadata in details.\n"
"2. Synthesize a professional squash commit message following Conventional Commits.\n"
"3. Call 'gh_pr_merge_flow' with point='merge' and your 'override_json_str', formatted according to 'details.json_format'."
"1. Review PR metadata in `details.pr`.\n"
"2. Synthesize a professional squash commit message for `override_json_str` following the 'details.json_format' layout.\n"
" - 'title': A professional squash commit subject.【STRICT】MUST satisfy 'details.subject_regex'.\n"
" - 'body': The detailed body should explain the 'why' and 'how' (not just 'what'), especially for complex logic changes. 【STRICT】single line max `details.body_wrap_length` chars.\n"
"3. Call 'gh_pr_merge_flow' with point='merge' and your 'override_json_str'.\n"
),
constraints=[
"Each commit message MUST follow Conventional Commits and `commit_rules`.",
],
details={
"pr": pr_data,
"json_format": {
"title": "feat(core): merge json format logic",
"body": "Detailed summary of PR changes...",
"body": "- Detailed summary of PR changes...",
},
"commit_rules": get_full_commit_rules(),
"subject_regex": get_commit_subject_regex(),
"body_wrap_length": get_commit_body_wrap_length(),
},
)

Expand All @@ -198,11 +201,11 @@ def _handle_merge(override_json_str: str) -> Result:
)

# Validate regex
rules = get_full_commit_rules()
if body and title and not re.match(rules["message_regex"], title):
subject_regex = get_commit_subject_regex()
if not body or not title or not re.match(subject_regex, title):
return Result(
status="error",
message=f"Title '{title}' violates commit policy: {rules['message_regex']}",
message=f"Title '{title}' violates commit policy: {subject_regex}",
workflow=WORKFLOW,
)

Expand Down
34 changes: 20 additions & 14 deletions src/agent_tools/core/orchestrators/git_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
)
from agent_tools.infrastructure.config.manager import (
get_allow_direct_actions_to_protected,
get_full_commit_rules,
get_commit_body_wrap_length,
get_commit_subject_regex,
get_protected_branches,
get_separation_rules,
get_commit_grouping_signals,
get_commit_max_groups,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -79,18 +81,20 @@ def _handle_sense() -> Result:
next_step="BUILD_COMMIT_PLAN",
resume_point="commit",
instruction=(
"【STRICT PROTOCOL / 严格协议】您当前处于受控工作流中。禁止跳过步骤、禁止执行任何未授权的裸命令。\n"
"【STRICT PROTOCOL / 严格协议】您当前处于受控工作流中。禁止直接提交,禁止绕过分析步骤。\n"
"【ACTION】\n"
"1. Review 'staged' vs 'unstaged' files in details.\n"
"2. Decide which files should be committed together based on 'separation_rules'.\n"
"3. Draft commit messages following Conventional Commits.\n"
"4. Call 'git_commit_flow' with point='commit' and your 'plan_json_str', formatted according to 'details.json_format'.\n"
"【SAFETY】Check 'risk_files' carefully. NEVER commit secrets or large binaries accidentally."
"2. Group files into logical commits (max: 'details.max_groups') using these signals: 'details.grouping_signals'.\n"
"3. Synthesize 'plan_json_str' following the 'details.json_format' layout, using these field specs for each entry in 'commits':\n"
" - 'files': Array of file paths to be committed together.\n"
" - 'message': A structured Git message. \n"
" * Subject (1st line): 【STRICT】MUST satisfy 'details.subject_regex'.\n"
" * Detail body (Add a blank line after the subject): "
" * The detailed body should explain the 'why' and 'how' (not just 'what'), especially for complex logic changes. 【STRICT】single line max `details.body_wrap_length` chars.\n"
"4. Call 'git_commit_flow' with point='commit' and your 'plan_json_str'.\n"
"【CONSTRAINTS】\n"
"- Do NOT include files from 'risk_files' unless specifically authorized.\n"
),
constraints=[
"Do NOT commit files listed in 'risk_files' unless explicitly intended.",
"Each commit message MUST follow Conventional Commits and `commit_rules`.",
],
details={
"staged_files": staged,
"unstaged_files": unstaged,
Expand All @@ -100,12 +104,14 @@ def _handle_sense() -> Result:
"commits": [
{
"files": ["file1.py", "file2.py"],
"message": "feat(scope): short description\n\nDetailed body...",
"message": "feat(scope): short description\n\n- Detailed body...",
}
]
},
"commit_rules": get_full_commit_rules(),
"separation_rules": get_separation_rules(),
"subject_regex": get_commit_subject_regex,
"body_wrap_length": get_commit_body_wrap_length(),
"grouping_signals": get_commit_grouping_signals(),
"max_groups": get_commit_max_groups(),
"branch_info": asdict(get_branch_context()),
},
)
Expand Down
19 changes: 10 additions & 9 deletions src/agent_tools/core/orchestrators/git_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
run_git,
)
from agent_tools.infrastructure.config.manager import (
get_full_commit_rules,
get_release_tag_regex,
get_commit_body_wrap_length,
get_protected_branches,
get_allow_direct_actions_to_protected,
)
Expand Down Expand Up @@ -125,28 +125,29 @@ def _handle_sense() -> Result:
"3. If version bump is needed (【PAUSE】You MUST propose the new version and its rationale, then WAIT for USER approval before proceeding):\n"
f" - **IF PROTECTED (is_protected={is_protected})**: Use 'gh_pr_create_flow' for a version PR. IMPORTANT: Wait for CI checks, then merge using 'gh_pr_merge_flow'.\n"
f" - **ELSE**: Update and commit directly using 'git_commit_flow'.\n"
"4. Finalize with 'git_release_flow' (point='release'), providing a structured message with descriptive title and categorized changes (Features, Bug Fixes, Refactors)."
"4. Finalize with 'git_release_flow' (point='release'), with its 'tag_json_str' following the 'details.json_format'.\n"
" - 'name': The chosen version string. 【STRICT】MUST match 'details.tag_regex'.\n"
" - 'message': Categorized release notes. MUST follow the layout in 'details.json_format.message'.\n"
" * The field MUST include a header and categorized bullet points (Features, Bug Fixes, etc.).\n"
" * The field single line max `details.body_wrap_length` chars.\n"
"【CONSTRAINTS】\n"
"- Do NOT proceed if version files and git history are out of sync."
),
constraints=[
"Tag MUST match the regex in details.",
"Each commit message MUST follow Conventional Commits and `commit_rules`.",
"Do NOT proceed if version files and git history are out of sync.",
],
details={
"latest_tag": latest_tag,
"commits": [asdict(c) for c in commits],
"is_protected": is_protected,
"json_format": {
"name": "v1.2.3",
"message": (
"v1.2.3: Descriptive Title Summary\n\n"
"Descriptive Title Summary\n\n"
"### 🚀 Features\n- List key new features here.\n\n"
"### 🐛 Bug Fixes\n- List important bug fixes here.\n\n"
"### ⚙️ Refactors\n- List major internal improvements here."
),
},
"tag_regex": get_release_tag_regex(),
"commit_rules": get_full_commit_rules(),
"body_wrap_length": get_commit_body_wrap_length(),
"branch_info": asdict(get_branch_context()),
},
)
Expand Down
10 changes: 5 additions & 5 deletions src/agent_tools/core/orchestrators/git_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ def _pause_for_conflict(current_point: str) -> Result:
next_step="RESOLVE_CONFLICTS",
resume_point=current_point,
instruction=(
"【ACTION】\n"
f"1. Resolve conflicts in: {', '.join(files)}\n"
"2. Run 'git add <file>' for all resolved files.\n"
f"3. Resume by calling 'git_sync_flow' with point='{current_point}'."
f"3. Resume by calling 'git_sync_flow' with point='{current_point}'.\n"
"【CONSTRAINTS】\n"
"- Do NOT run 'git commit' during rebase."
"- Do NOT run 'git rebase --continue' manually; the tool handles it."
),
constraints=[
"Do NOT run 'git commit' during rebase.",
"Do NOT run 'git rebase --continue' manually; the tool handles it.",
],
details={"conflicted_files": files},
)

Expand Down
10 changes: 6 additions & 4 deletions src/agent_tools/infrastructure/clients/git/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
get_commit_allowed_types,
get_commit_body_wrap_length,
get_commit_max_groups,
get_commit_message_regex,
get_commit_subject_max_length,
get_commit_subject_regex,
load_rules,
)

Expand All @@ -28,7 +28,7 @@ def validate_plan(plan: dict, rules: dict) -> tuple[bool, str]:
)

allowed_types = get_commit_allowed_types()
regex_pattern = get_commit_message_regex()
regex_pattern = get_commit_subject_regex()
subject_max = get_commit_subject_max_length()
body_wrap = get_commit_body_wrap_length()

Expand Down Expand Up @@ -58,11 +58,13 @@ def validate_plan(plan: dict, rules: dict) -> tuple[bool, str]:

# --- Allowed types check (subject only) ---
if allowed_types:
msg_type = subject.split(":")[0].split("(")[0]
# Handle cases like "feat(scope)!: ..." or "feat!: ..."
prefix = subject.split(":")[0]
msg_type = prefix.split("(")[0].rstrip("!")
if msg_type not in allowed_types:
return (
False,
f"Commit message at index {idx} uses disallowed type '{msg_type}'.",
f"Commit message at index {idx} uses disallowed type '{msg_type}'. (Allowed: {', '.join(allowed_types)})",
)

# --- Body wrap length check ---
Expand Down
34 changes: 15 additions & 19 deletions src/agent_tools/infrastructure/config/manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
import json
import re
import logging
import os
from pathlib import Path
Expand Down Expand Up @@ -120,9 +121,20 @@ def get_commit_allowed_types() -> list[str]:
return cast(list[str], rules["git"]["commit"]["allowed_types"])


def get_commit_message_regex() -> str:
rules = load_rules()
return cast(str, rules["git"]["commit"]["message_regex"])
def get_commit_subject_regex() -> str:
"""Automatically generate regex from 'allowed_types' and 'subject_max_length' (DRY)"""
allowed_types = get_commit_allowed_types()
max_len = get_commit_subject_max_length()

# Pattern components:
# 1. Start with one of the allowed types
# 2. Optional scope in parentheses (e.g., '(core)')
# 3. Optional breaking change marker '!'
# 4. Mandatory colon and space ': '
# 5. Total length limit using positive lookahead
# This ensures the entire subject (prefix + scope + description) is within max_len
types_pattern = "|".join(re.escape(t) for t in allowed_types)
return rf"^(?=.{{1,{max_len}}}$)({types_pattern})(\([a-z0-9._\-]+\))?(!)?:\s.+$"


def get_commit_subject_max_length() -> int:
Expand Down Expand Up @@ -163,19 +175,3 @@ def get_allow_direct_actions_to_protected() -> bool:
def get_release_tag_regex() -> str:
rules = load_rules()
return cast(str, rules["git"]["release"]["tag_regex"])


def get_full_commit_rules() -> dict[str, Any]:
return {
"allowed_types": get_commit_allowed_types(),
"message_regex": get_commit_message_regex(),
"subject_max_length": get_commit_subject_max_length(),
"body_wrap_length": get_commit_body_wrap_length(),
}


def get_separation_rules() -> dict[str, Any]:
return {
"grouping_signals": get_commit_grouping_signals(),
"max_groups": get_commit_max_groups(),
}
7 changes: 2 additions & 5 deletions src/agent_tools/infrastructure/config/resources/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@ git:
- revert

# Message formatting constraints
subject_max_length: 72
body_wrap_length: 80

# Required regex pattern for commit subjects
message_regex: "^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\\([a-z0-9._\\-]+\\)(!)?: .{1,72}$"
subject_max_length: 85
body_wrap_length: 90

# Grouping strategies when generating a Smart Commit Plan
max_groups: 8
Expand Down
4 changes: 2 additions & 2 deletions src/agent_tools/infrastructure/config/resources/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
"commit": {
"type": "object",
"additionalProperties": false,
"required": ["allowed_types", "message_regex", "subject_max_length", "body_wrap_length", "grouping_signals", "max_groups"],
"required": ["allowed_types", "subject_max_length", "body_wrap_length", "grouping_signals", "max_groups"],
"properties": {
"allowed_types": {
"type": "array",
"items": { "type": "string" }
},
"message_regex": {
"subject_regex": {
"type": "string"
},
"subject_max_length": { "type": "integer" },
Expand Down
Loading
Loading