feat: mount canonical gitea-workflow skill for Codex and MCP inventory (Closes #551)

Expose gitea-workflow / llm-project-workflow / git-pr-workflows as one
workflow router in mcp_list_project_skills, add preflight + Codex install
script, and document multi-runtime skill name parity.
This commit is contained in:
2026-07-08 22:43:46 -04:00
parent 9a2e585a9e
commit 44cf2a4ce8
9 changed files with 496 additions and 1 deletions
+160
View File
@@ -0,0 +1,160 @@
"""Canonical workflow skill naming and preflight for multi-runtime agents (#551).
Controller prompts and Claude historically require ``gitea-workflow``. The
portable in-repo skill is ``skills/llm-project-workflow``. Codex must resolve
the same canonical names so sessions do not proceed without the workflow wall.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
# Canonical names that all runtimes (Claude, Codex, Gemini, MCP inventory)
# must treat as the same workflow router skill.
CANONICAL_WORKFLOW_SKILL_NAMES = (
"gitea-workflow",
"llm-project-workflow",
"git-pr-workflows",
)
# Primary checked-in skill path (portable source of truth).
REPO_SKILL_REL_PATH = "skills/llm-project-workflow/SKILL.md"
# In-repo alias directory so scanners that look for skills/gitea-workflow work.
REPO_ALIAS_SKILL_REL_PATH = "skills/gitea-workflow/SKILL.md"
CODEX_SKILLS_ENV = "CODEX_HOME"
DEFAULT_CODEX_SKILLS = os.path.expanduser("~/.codex/skills")
def default_codex_skills_dir() -> str:
home = (os.environ.get(CODEX_SKILLS_ENV) or "").strip()
if home:
return os.path.join(os.path.expanduser(home), "skills")
return DEFAULT_CODEX_SKILLS
def resolve_repo_skill_path(project_root: str) -> Path:
return Path(project_root) / REPO_SKILL_REL_PATH
def resolve_repo_alias_skill_path(project_root: str) -> Path:
return Path(project_root) / REPO_ALIAS_SKILL_REL_PATH
def normalize_skill_name(name: str | None) -> str:
return (name or "").strip().lower().replace("_", "-")
def is_canonical_workflow_skill_name(name: str | None) -> bool:
return normalize_skill_name(name) in CANONICAL_WORKFLOW_SKILL_NAMES
def assess_workflow_skill_presence(
project_root: str,
*,
codex_skills_dir: str | None = None,
) -> dict[str, Any]:
"""Return presence proof for the canonical workflow skill.
Fail-closed when the in-repo skill file is missing. Codex mount is advisory
in the report (operators install via script) but ``codex_skill_mounted`` is
explicit so preflight can BLOCK when required.
"""
root = (project_root or "").strip()
reasons: list[str] = []
repo_path = resolve_repo_skill_path(root) if root else None
alias_path = resolve_repo_alias_skill_path(root) if root else None
repo_present = bool(repo_path and repo_path.is_file())
alias_present = bool(alias_path and alias_path.is_file())
if not root:
reasons.append("project_root not provided (fail closed)")
elif not repo_present:
reasons.append(
f"canonical workflow skill missing at {REPO_SKILL_REL_PATH} "
"(fail closed — do not mutate without workflow wall)"
)
codex_dir = Path(codex_skills_dir or default_codex_skills_dir())
codex_mounts: dict[str, bool] = {}
for name in CANONICAL_WORKFLOW_SKILL_NAMES:
skill_md = codex_dir / name / "SKILL.md"
codex_mounts[name] = skill_md.is_file()
codex_any = any(codex_mounts.values())
blocked = bool(reasons)
return {
"canonical_names": list(CANONICAL_WORKFLOW_SKILL_NAMES),
"primary_name": "gitea-workflow",
"portable_name": "llm-project-workflow",
"repo_skill_rel_path": REPO_SKILL_REL_PATH,
"repo_skill_present": repo_present,
"repo_alias_rel_path": REPO_ALIAS_SKILL_REL_PATH,
"repo_alias_present": alias_present,
"codex_skills_dir": str(codex_dir),
"codex_skill_mounts": codex_mounts,
"codex_skill_mounted": codex_any,
"workflow_skill_ready": repo_present,
"blocked": blocked,
"block_reason": reasons[0] if reasons else None,
"reasons": reasons,
"safe_next_action": (
"Install or repair skills/llm-project-workflow/SKILL.md in the "
"repo; run scripts/install-codex-workflow-skill.sh for Codex; "
"call mcp_list_project_skills and load gitea-workflow / "
"llm-project-workflow before any git/Gitea mutation."
if blocked or not codex_any
else "Load gitea-workflow or llm-project-workflow, then proceed."
),
}
def workflow_skill_registry_entries() -> dict[str, dict[str, Any]]:
"""Shared skill metadata for mcp_list_project_skills / guides."""
shared_steps = [
"Call mcp_list_project_skills and confirm gitea-workflow (or "
"llm-project-workflow) is listed.",
"Call mcp_check_workflow_skill_preflight and stop if blocked.",
"Load skills/llm-project-workflow/SKILL.md (or the gitea-workflow "
"alias) and route to the matching workflow file for the task mode.",
"Do not perform git or Gitea mutations until the workflow is loaded.",
"Codex: ensure ~/.codex/skills/gitea-workflow -> repo skill via "
"scripts/install-codex-workflow-skill.sh.",
]
notes = (
f"Canonical names: {', '.join(CANONICAL_WORKFLOW_SKILL_NAMES)}. "
f"Portable source: {REPO_SKILL_REL_PATH}. "
"Controller prompts may say gitea-workflow; Codex and MCP inventory "
"must resolve the same skill."
)
base = {
"description": (
"Canonical Gitea/LLM project workflow router: pick task mode, "
"load the matching workflow, enforce isolation, emit the final "
"report schema."
),
"when_to_use": (
"At the start of every author, review, merge, reconcile, or "
"issue-filing session — before any mutation."
),
"required_operations": ["gitea.read"],
"status": "available",
"notes": notes,
"steps": shared_steps,
"repo_path": REPO_SKILL_REL_PATH,
"canonical": True,
}
return {
"gitea-workflow": dict(base),
"llm-project-workflow": dict(base),
"git-pr-workflows": {
**dict(base),
"description": (
"Legacy alias for the canonical Gitea/LLM project workflow "
"router (same as gitea-workflow / llm-project-workflow)."
),
"notes": notes + " Prefer gitea-workflow in new prompts.",
},
}