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.
161 lines
6.0 KiB
Python
161 lines
6.0 KiB
Python
"""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.",
|
|
},
|
|
}
|