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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "kyson-mcp-agent-tools"
version = "0.1.27"
version = "0.1.28"
description = "Industrial-grade Agent Git Workflow Tools"
readme = "README.md"
requires-python = ">=3.13"
Expand Down
10 changes: 7 additions & 3 deletions src/agent_tools/core/models/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ class Result:

def to_json(self) -> str:
"""Serialize result to JSON string."""
# We no longer auto-prepend headers here to avoid redundancy.
# Orchestrators are responsible for high-quality instructions.
return json.dumps(asdict(self), indent=2, ensure_ascii=False)

def serialize(obj):
if callable(obj):
return f"<function {obj.__name__}>"
return str(obj)

return json.dumps(asdict(self), default=serialize, indent=2, ensure_ascii=False)

@property
def ok(self) -> bool:
Expand Down
19 changes: 7 additions & 12 deletions src/agent_tools/core/orchestrators/gh_pr_create.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging
from dataclasses import asdict
from typing import Literal
Expand Down Expand Up @@ -112,14 +111,14 @@ def _handle_sense() -> Result:
next_step="SYNTHESIZE_PR_DESCRIPTION",
resume_point="create",
instruction=(
"【STRICT PROTOCOL / 严格协议】您当前处于受控工作流中。禁止跳过步骤、禁止执行任何未授权的裸命令。\n"
"【STRICT PROTOCOL】You are in a controlled workflow. Skipping steps or executing unauthorized commands is strictly prohibited.\n"
"【ACTION】\n"
"1. Review 'commits' in details.\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'.\n"
"4. Call 'gh_pr_create_flow' with point='create' and your 'draft' object.\n"
),
details={
"owner": repo_info.owner,
Expand All @@ -137,14 +136,10 @@ def _handle_sense() -> Result:
)


def _handle_create(draft_json_str: str) -> Result:
def _handle_create(draft: dict) -> Result:
"""Stage 2: Execution via GitHub CLI."""
try:
draft_data = json.loads(draft_json_str)
title = draft_data.get("title")
body = draft_data.get("body")
except json.JSONDecodeError:
return Result(status="error", message="Invalid JSON in draft_json_str.", workflow=WORKFLOW)
title = draft.get("title")
body = draft.get("body")

if not title or not body:
return Result(
Expand Down Expand Up @@ -197,12 +192,12 @@ def _handle_create(draft_json_str: str) -> Result:
)


def gh_pr_create_flow(point: Literal["init", "sense", "create"] = "init", draft_json_str: str = "") -> Result:
def gh_pr_create_flow(point: Literal["init", "sense", "create"] = "init", draft: dict | None = None) -> Result:
"""Industrial-grade GitHub PR creation flow orchestrator."""
handlers = {
"init": _handle_init,
"sense": _handle_sense,
"create": lambda: _handle_create(draft_json_str),
"create": lambda: _handle_create(draft or {}),
}

try:
Expand Down
24 changes: 8 additions & 16 deletions src/agent_tools/core/orchestrators/gh_pr_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,13 @@ def _handle_sense() -> Result:
next_step="SYNTHESIZE_SQUASH_MESSAGE",
resume_point="merge",
instruction=(
"【STRICT PROTOCOL / 严格协议】您当前处于受控工作流中。禁止跳过步骤、禁止执行任何未授权的裸命令。\n"
"【STRICT PROTOCOL】You are in a controlled workflow. Skipping steps or executing unauthorized commands is strictly prohibited.\n"
"【ACTION】\n"
"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"
"2. Synthesize a professional squash commit message for `override` 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"
"3. Call 'gh_pr_merge_flow' with point='merge' and your 'override' object.\n"
),
details={
"pr": pr_data,
Expand All @@ -187,18 +187,10 @@ def _handle_sense() -> Result:
)


def _handle_merge(override_json_str: str) -> Result:
def _handle_merge(override: dict) -> Result:
"""Stage 2: Execution and local cleanup."""
try:
data = json.loads(override_json_str)
title = data.get("title")
body = data.get("body")
except json.JSONDecodeError:
return Result(
status="error",
message="Invalid JSON in override_json_str.",
workflow=WORKFLOW,
)
title = override.get("title")
body = override.get("body")

# Validate regex
subject_regex = get_commit_subject_regex()
Expand Down Expand Up @@ -270,12 +262,12 @@ def _handle_merge(override_json_str: str) -> Result:
return Result(status="success", message=cleanup_msg, workflow=WORKFLOW)


def gh_pr_merge_flow(point: Literal["init", "sense", "merge"] = "init", override_json_str: str = "") -> Result:
def gh_pr_merge_flow(point: Literal["init", "sense", "merge"] = "init", override: dict | None = None) -> Result:
"""Industrial-grade GitHub PR merging flow orchestrator."""
handlers = {
"init": _handle_init,
"sense": _handle_sense,
"merge": lambda: _handle_merge(override_json_str),
"merge": lambda: _handle_merge(override or {}),
}

try:
Expand Down
28 changes: 8 additions & 20 deletions src/agent_tools/core/orchestrators/git_commit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import json
import logging
from dataclasses import asdict
from typing import Literal

from agent_tools.core.models.workflow import Result
Expand All @@ -16,6 +14,7 @@
get_protected_branches,
get_commit_grouping_signals,
get_commit_max_groups,
get_sensitive_patterns,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -59,7 +58,7 @@ def _handle_sense() -> Result:
diff_info = get_diff_summary()

# Check for sensitive files (e.g., .env, secrets)
sensitive_patterns = [".env", "key", "secret", "token", "password"]
sensitive_patterns = get_sensitive_patterns()
risk_files = [
f.filepath for f in diff_info.changed_files if any(p in f.filepath.lower() for p in sensitive_patterns)
]
Expand All @@ -81,7 +80,7 @@ def _handle_sense() -> Result:
next_step="BUILD_COMMIT_PLAN",
resume_point="commit",
instruction=(
"【STRICT PROTOCOL / 严格协议】您当前处于受控工作流中。禁止直接提交,禁止绕过分析步骤。\n"
"【STRICT PROTOCOL】You are in a controlled workflow. Direct commits or bypassing analysis is strictly prohibited.\n"
"【ACTION】\n"
"1. Review 'staged' vs 'unstaged' files in details.\n"
"2. Group files into logical commits (max: 'details.max_groups') using these signals: 'details.grouping_signals'.\n"
Expand All @@ -91,7 +90,7 @@ def _handle_sense() -> Result:
" * 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"
"4. Call 'git_commit_flow' with point='commit' and your 'plan' object.\n"
"【CONSTRAINTS】\n"
"- Do NOT include files from 'risk_files' unless specifically authorized.\n"
),
Expand All @@ -108,27 +107,16 @@ def _handle_sense() -> Result:
}
]
},
"subject_regex": get_commit_subject_regex,
"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()),
},
)


def _handle_commit(plan_json_str: str) -> Result:
def _handle_commit(plan: dict) -> Result:
"""Stage 2: Execute the provided commit plan with validation."""
try:
plan = json.loads(plan_json_str)
except json.JSONDecodeError:
return Result(
status="handoff",
message="Invalid JSON format in plan.",
workflow=WORKFLOW,
resume_point="commit",
instruction="Fix the JSON formatting error and resubmit the plan.",
)

# execute_commit_plan handles 'git add' for files specified in the plan
commit_res = execute_commit_plan(plan)
Expand All @@ -148,11 +136,11 @@ def _handle_commit(plan_json_str: str) -> Result:
)


def git_commit_flow(point: Literal["sense", "commit"] = "sense", plan_json_str: str = "") -> Result:
def git_commit_flow(point: Literal["sense", "commit"] = "sense", plan: dict | None = None) -> Result:
"""Industrial-grade git commit flow orchestrator."""
handlers = {
"sense": _handle_sense,
"commit": lambda: _handle_commit(plan_json_str),
"commit": lambda: _handle_commit(plan or {}),
}

try:
Expand Down
30 changes: 10 additions & 20 deletions src/agent_tools/core/orchestrators/git_release.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging
import re
from dataclasses import asdict
Expand Down Expand Up @@ -92,6 +91,7 @@ def _is_protected_branch() -> bool:
def _handle_sense() -> Result:
"""Stage 1: Context gathering and planning."""

branch_info = get_branch_context()
latest_tag = get_latest_tag()
if latest_tag:
commits = get_commits_ahead(latest_tag)
Expand Down Expand Up @@ -119,13 +119,13 @@ def _handle_sense() -> Result:
next_step="PLAN_VERSION_BUMP",
resume_point="release",
instruction=(
"【STRICT PROTOCOL / 严格协议】\n"
"【STRICT PROTOCOL】You are in a controlled workflow. Direct commits or bypassing analysis is strictly prohibited.\n"
"1. Analyze 'commits' in details to determine the next SemVer increment.\n"
"2. Read versioning files to find current version. If already updated via recent merges, proceed to step 4.\n"
"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'), with its 'tag_json_str' following the 'details.json_format'.\n"
f" - **IF PROTECTED (is_protected={is_protected})**: Create a task branch (e.g. release/vX.Y.Z), update version, commit via 'git_commit_flow', push, and then 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 to {branch_info.current_branch} using 'git_commit_flow'.\n"
"4. Finalize with 'git_release_flow' (point='release'), with its 'tag_data' 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"
Expand All @@ -148,24 +148,14 @@ def _handle_sense() -> Result:
},
"tag_regex": get_release_tag_regex(),
"body_wrap_length": get_commit_body_wrap_length(),
"branch_info": asdict(get_branch_context()),
},
)


def _handle_release(tag_json_str: str) -> Result:
def _handle_release(tag_data: dict) -> Result:
"""Stage 2: Physical tagging and atomic push."""
try:
data = json.loads(tag_json_str)
name = data.get("name")
message = data.get("message")
except json.JSONDecodeError:
return Result(
status="error",
message="Invalid JSON in tag_json_str.",
workflow=WORKFLOW,
instruction="Fix the JSON format and retry.",
)
name = tag_data.get("name")
message = tag_data.get("message")

if not name or not message:
return Result(status="error", message="Missing 'name' or 'message'.", workflow=WORKFLOW)
Expand Down Expand Up @@ -205,12 +195,12 @@ def _handle_release(tag_json_str: str) -> Result:
)


def git_release_flow(point: Literal["init", "sense", "release"] = "init", tag_json_str: str = "") -> Result:
def git_release_flow(point: Literal["init", "sense", "release"] = "init", tag_data: dict | None = None) -> Result:
"""Industrial-grade git release flow orchestrator."""
handlers = {
"init": _handle_init,
"sense": _handle_sense,
"release": lambda: _handle_release(tag_json_str),
"release": lambda: _handle_release(tag_data or {}),
}

try:
Expand Down
5 changes: 5 additions & 0 deletions src/agent_tools/infrastructure/config/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ def get_allow_direct_actions_to_protected() -> bool:
return cast(bool, rules["git"]["safety"]["allow_direct_actions_to_protected"])


def get_sensitive_patterns() -> list[str]:
rules = load_rules()
return cast(list[str], rules["git"]["safety"]["sensitive_patterns"])


def get_release_tag_regex() -> str:
rules = load_rules()
return cast(str, rules["git"]["release"]["tag_regex"])
8 changes: 8 additions & 0 deletions src/agent_tools/infrastructure/config/resources/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ git:
# If false, the agent must refuse direct commits/pushes to the branches above
allow_direct_actions_to_protected: false

# Patterns to identify sensitive files (e.g., secrets, tokens)
sensitive_patterns:
- ".env"
- "key"
- "secret"
- "token"
- "password"

# Performance and Token Limits for Diff Analysis
diff:
max_diff_lines_per_file: 500
Expand Down
6 changes: 5 additions & 1 deletion src/agent_tools/infrastructure/config/resources/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@
"safety": {
"type": "object",
"additionalProperties": false,
"required": ["protected_branches", "allow_direct_actions_to_protected"],
"required": ["protected_branches", "allow_direct_actions_to_protected", "sensitive_patterns"],
"properties": {
"protected_branches": {
"type": "array",
"items": { "type": "string" }
},
"allow_direct_actions_to_protected": {
"type": "boolean"
},
"sensitive_patterns": {
"type": "array",
"items": { "type": "string" }
}
}
},
Expand Down
Loading
Loading