diff --git a/plugins/taiga-auto-sync/hooks/claude-codex-hooks.json b/plugins/taiga-auto-sync/hooks/claude-codex-hooks.json index 376afa4..7a09d02 100644 --- a/plugins/taiga-auto-sync/hooks/claude-codex-hooks.json +++ b/plugins/taiga-auto-sync/hooks/claude-codex-hooks.json @@ -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\"", diff --git a/plugins/taiga-auto-sync/hooks/taiga-write-guard.py b/plugins/taiga-auto-sync/hooks/taiga-write-guard.py index f56756e..8329859 100644 --- a/plugins/taiga-auto-sync/hooks/taiga-write-guard.py +++ b/plugins/taiga-auto-sync/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: tag, and one " + "repo:/system: 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)) diff --git a/plugins/taiga-auto-sync/skills/taiga-operations/SKILL.md b/plugins/taiga-auto-sync/skills/taiga-operations/SKILL.md index 8c0c2bf..1a514ca 100644 --- a/plugins/taiga-auto-sync/skills/taiga-operations/SKILL.md +++ b/plugins/taiga-auto-sync/skills/taiga-operations/SKILL.md @@ -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:` tag, and one `repo:` or `system:` 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:` tag, and one `repo:` or `system:` 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.