fix(taiga-auto-sync): match real Taiga MCP tool names and drop impossible owner/sprint checks

The write-guard's matcher regex never matched the actual tool names
(createUserStory/createTask/createIssue, not user_story_create etc.), so
the metadata guard has never actually fired. It also checked for
assigned_to/milestone fields that don't exist on any create tool's
schema, which would have denied every create once the matcher was fixed.
Guard now validates only the tag contract (area/source/target), covers
the batch-create tools per item, and SKILL.md documents the real
follow-up calls (assignIssue/assignUserStoryToSprint/addIssueToSprint)
needed to set owner and sprint after creation.
This commit is contained in:
2026-09-14 00:31:53 +07:00
parent 98dca237f4
commit 6a9c18f5a5
3 changed files with 57 additions and 18 deletions

View File

@@ -1,5 +1,13 @@
#!/usr/bin/env python3
"""Reject Taiga item creation without the active-item metadata contract."""
"""Reject Taiga item creation that is missing the required tag contract.
Only checks tags. Owner and sprint are not creation-time fields on the
Taiga MCP server's create tools (createUserStory/createTask/createIssue and
their batch variants) -- they are set afterwards via assignIssue,
assignUserStoryToSprint, or addIssueToSprint. Tasks have no assignee/sprint
tool at all, so their owner and sprint are tracked through the parent
User Story instead.
"""
import json
import sys
@@ -8,15 +16,14 @@ AREAS = {
"access", "automation", "support", "vendor",
}
# Batch-create tools nest each item's tags under one of these list keys
# instead of exposing a top-level "tags" array.
BATCH_LIST_KEYS = ("userStories", "tasks", "issues")
def missing_fields(payload):
item = payload.get("tool_input", {})
tags = set(item.get("tags") or [])
def missing_tag_fields(tags):
tags = set(tags or [])
missing = []
if not item.get("assigned_to"):
missing.append("owner")
if not item.get("milestone"):
missing.append("current sprint")
if not tags.intersection(AREAS):
missing.append("area tag")
if not any(tag.startswith("source:") for tag in tags):
@@ -26,6 +33,21 @@ def missing_fields(payload):
return missing
def missing_fields(payload):
item = payload.get("tool_input", {})
for key in BATCH_LIST_KEYS:
entries = item.get(key)
if isinstance(entries, list):
problems = []
for index, entry in enumerate(entries):
missing = missing_tag_fields(entry.get("tags"))
if missing:
label = entry.get("subject") or f"item {index}"
problems.append(f"{label!r}: missing " + ", ".join(missing))
return problems
return missing_tag_fields(item.get("tags"))
def result(payload):
missing = missing_fields(payload)
if not missing:
@@ -35,8 +57,13 @@ def result(payload):
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": (
"Taiga item creation requires " + ", ".join(missing) +
". Resolve or create the current WIB sprint first."
"Taiga item is missing required tags: " + "; ".join(missing) + ". "
"Apply one area tag, one source:<agent> tag, and one "
"repo:<name>/system:<name> tag. Owner and sprint are not "
"creation-time fields here -- set them right after creation "
"with assignIssue / assignUserStoryToSprint / addIssueToSprint "
"(Tasks have no assignee/sprint tool; track Task ownership and "
"sprint through the parent User Story)."
),
}
}
@@ -44,9 +71,20 @@ def result(payload):
def main():
if sys.argv[1:] == ["--self-check"]:
assert result({"tool_input": {"assigned_to": 1, "milestone": 1,
"tags": ["infra", "source:codex", "repo:brain"]}}) is None
assert result({"tool_input": {"tags": []}})
ok_tags = ["infra", "source:codex", "repo:brain"]
assert result({"tool_input": {"tags": ok_tags}}) is None
assert result({"tool_input": {"tags": []}}) is not None
ok_batch = {"tool_input": {"issues": [{"subject": "a", "tags": ok_tags}]}}
assert result(ok_batch) is None
bad_batch = {"tool_input": {"issues": [
{"subject": "a", "tags": ok_tags},
{"subject": "b", "tags": []},
]}}
assert result(bad_batch) is not None
print("self-check: ok")
return
output = result(json.load(sys.stdin))