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:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "^mcp__taiga__(user_story|task|issue)_create$",
|
||||
"matcher": "^mcp__taiga__(createUserStory|createTask|createIssue|batchCreateUserStories|batchCreateTasks|batchCreateIssues)$",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/taiga-write-guard.py\"",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -8,9 +8,10 @@ description: Automatically track material IT Infrastructure & Operations impleme
|
||||
Use this workflow automatically when the prompt requests material IT Infra & Ops work. The user does not need to ask for a Taiga update.
|
||||
|
||||
1. Do not create or update Taiga for reading, investigation without a change, planning only, no-op work, or secrets.
|
||||
2. Before creating or activating an item, find the sprint that covers today in WIB. If none exists, create the Monday–Sunday `YYYY-WNN Operations` sprint, then use it.
|
||||
2. Before creating or activating an item, find the sprint that covers today in WIB. If none exists, create the Monday–Sunday `YYYY-WNN Operations` sprint with `createMilestone`, then use it.
|
||||
3. Find an existing matching open item before creating a new one. Reuse its local state reference when available.
|
||||
4. Classify a standalone daily operation as an Issue. Use one User Story only for one outcome with two or more related Tasks; create those Tasks beneath it.
|
||||
5. Every active User Story, Task, and Issue needs an owner, current sprint, one area tag, one `source:<agent>` tag, and one `repo:<name>` or `system:<name>` tag.
|
||||
6. Write all Taiga-facing natural-language text in Bahasa Indonesia: titles, descriptions, comments, summaries, verification, blockers, and handoffs. Preserve code, commands, branch/PR names, repository and service names, domains, tags, statuses, and established technical terms in English.
|
||||
7. Use the Wiki template for the selected item type. Record only material `start`, `progress`, `blocked`, and verified `done` events. Never put credentials or sensitive evidence in Taiga or local state.
|
||||
5. Every active User Story, Task, and Issue needs one area tag, one `source:<agent>` tag, and one `repo:<name>` or `system:<name>` tag on the create call itself — the write-guard hook enforces this and blocks creation when it is missing.
|
||||
6. Owner and sprint are not creation-time fields on this Taiga MCP server. Immediately after creating a User Story or Issue, call `assignUserStoryToSprint` / `assignIssue` / `addIssueToSprint` to set the current sprint and owner before treating the item as active. Tasks have no assignee or sprint tool at all — track Task ownership and sprint through the parent User Story instead.
|
||||
7. Write all Taiga-facing natural-language text in Bahasa Indonesia: titles, descriptions, comments, summaries, verification, blockers, and handoffs. Preserve code, commands, branch/PR names, repository and service names, domains, tags, statuses, and established technical terms in English.
|
||||
8. Use the Wiki template for the selected item type. Record only material `start`, `progress`, `blocked`, and verified `done` events. Never put credentials or sensitive evidence in Taiga or local state.
|
||||
|
||||
Reference in New Issue
Block a user