Merge pull request 'feat: mount canonical gitea-workflow skill for Codex and MCP inventory (Closes #551)' (#565) from feat/issue-551-codex-gitea-workflow into master
This commit was merged in pull request #565.
This commit is contained in:
@@ -413,6 +413,24 @@ The guard blocks when the **control checkout** (repository root) is:
|
|||||||
`branches/...` directories are disposable role worktrees; the root checkout is
|
`branches/...` directories are disposable role worktrees; the root checkout is
|
||||||
the stable orchestration surface only.
|
the stable orchestration surface only.
|
||||||
|
|
||||||
|
## Canonical workflow skill names (#551)
|
||||||
|
|
||||||
|
Controller prompts and sessions must load the **same** workflow skill wall
|
||||||
|
regardless of runtime (Claude, Codex, Gemini):
|
||||||
|
|
||||||
|
| Name | Role |
|
||||||
|
|------|------|
|
||||||
|
| `gitea-workflow` | Primary controller / Codex skill name |
|
||||||
|
| `llm-project-workflow` | Portable in-repo package |
|
||||||
|
| `git-pr-workflows` | Legacy alias |
|
||||||
|
|
||||||
|
- Inventory: `mcp_list_project_skills` lists all three.
|
||||||
|
- Preflight: `mcp_check_workflow_skill_preflight` before mutations.
|
||||||
|
- Codex install: `scripts/install-codex-workflow-skill.sh`
|
||||||
|
- Full doc: [`docs/workflow-skill-mount.md`](workflow-skill-mount.md)
|
||||||
|
|
||||||
|
If the skill is missing, stop with BLOCKED + DIAGNOSE — do not mutate.
|
||||||
|
|
||||||
## No direct-import mutation path (#558)
|
## No direct-import mutation path (#558)
|
||||||
|
|
||||||
Never `import gitea_mcp_server` or call `gitea_auth.get_auth_header` /
|
Never `import gitea_mcp_server` or call `gitea_auth.get_auth_header` /
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Workflow skill mount across runtimes (#551)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Controller prompts require **`gitea-workflow`**, but:
|
||||||
|
|
||||||
|
- Claude may load `~/.claude/skills/gitea-workflow`
|
||||||
|
- Codex often has **no** `~/.codex/skills/gitea-workflow`
|
||||||
|
- The portable package in-repo is `skills/llm-project-workflow`
|
||||||
|
- `mcp_list_project_skills` historically listed operational guides only, not
|
||||||
|
the workflow router
|
||||||
|
|
||||||
|
Sessions then either **block** incorrectly or **proceed without** the workflow
|
||||||
|
wall.
|
||||||
|
|
||||||
|
## Canonical names (must resolve to the same skill)
|
||||||
|
|
||||||
|
| Name | Use |
|
||||||
|
|------|-----|
|
||||||
|
| `gitea-workflow` | **Primary** controller / Codex skill name |
|
||||||
|
| `llm-project-workflow` | Portable in-repo package name |
|
||||||
|
| `git-pr-workflows` | Legacy alias |
|
||||||
|
|
||||||
|
Source of truth: `skills/llm-project-workflow/SKILL.md`
|
||||||
|
In-repo alias stub: `skills/gitea-workflow/SKILL.md`
|
||||||
|
|
||||||
|
## Codex install
|
||||||
|
|
||||||
|
From a `branches/` worktree (or any clone of the repo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/install-codex-workflow-skill.sh
|
||||||
|
# optional:
|
||||||
|
./scripts/install-codex-workflow-skill.sh --dry-run
|
||||||
|
./scripts/install-codex-workflow-skill.sh --skills-dir "$HOME/.codex/skills"
|
||||||
|
```
|
||||||
|
|
||||||
|
This symlinks the portable package under all three names. **Restart Codex**
|
||||||
|
after install.
|
||||||
|
|
||||||
|
## MCP discovery
|
||||||
|
|
||||||
|
- `mcp_list_project_skills` includes `gitea-workflow`, `llm-project-workflow`,
|
||||||
|
and `git-pr-workflows`.
|
||||||
|
- `mcp_get_skill_guide("<name>")` returns the same router steps for each.
|
||||||
|
- `mcp_check_workflow_skill_preflight` proves the in-repo skill file exists and
|
||||||
|
reports Codex mount status.
|
||||||
|
|
||||||
|
## Preflight rule
|
||||||
|
|
||||||
|
Before any git or Gitea mutation:
|
||||||
|
|
||||||
|
1. Call `mcp_check_workflow_skill_preflight`.
|
||||||
|
2. If `blocked` / `workflow_skill_ready` is false → **BLOCKED + DIAGNOSE**; do
|
||||||
|
not mutate.
|
||||||
|
3. Load the skill by **any** canonical name and follow the router.
|
||||||
|
|
||||||
|
Missing Codex mount alone does not block if the in-repo skill is present and
|
||||||
|
loaded via MCP/docs; operators should still install the Codex symlink so
|
||||||
|
prompt names resolve natively.
|
||||||
|
|
||||||
|
## Controller prompts
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
> Invoke skill `gitea-workflow` (alias of `llm-project-workflow`).
|
||||||
|
|
||||||
|
Do not require a name that is only available on one runtime.
|
||||||
+37
-1
@@ -828,6 +828,7 @@ import reconciliation_workflow # noqa: E402
|
|||||||
import audit_reconciliation_mode # noqa: E402
|
import audit_reconciliation_mode # noqa: E402
|
||||||
import review_merge_state_machine # noqa: E402
|
import review_merge_state_machine # noqa: E402
|
||||||
import pr_work_lease # noqa: E402
|
import pr_work_lease # noqa: E402
|
||||||
|
import workflow_skill # noqa: E402
|
||||||
import conflict_fix_classification # noqa: E402
|
import conflict_fix_classification # noqa: E402
|
||||||
import native_mcp_preference # noqa: E402
|
import native_mcp_preference # noqa: E402
|
||||||
import master_parity_gate # noqa: E402
|
import master_parity_gate # noqa: E402
|
||||||
@@ -7273,6 +7274,9 @@ _PROJECT_SKILLS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Canonical workflow router skill names for Claude/Codex/Gemini parity (#551).
|
||||||
|
_PROJECT_SKILLS.update(workflow_skill.workflow_skill_registry_entries())
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def mcp_get_control_plane_guide(
|
def mcp_get_control_plane_guide(
|
||||||
@@ -7394,7 +7398,7 @@ def mcp_get_control_plane_guide(
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def mcp_list_project_skills() -> dict:
|
def mcp_list_project_skills() -> dict:
|
||||||
"""List the project's workflow skills and when to use them (#128).
|
"""List the project's workflow skills and when to use them (#128, #551).
|
||||||
|
|
||||||
Read-only; makes no API calls. Each skill reports its name,
|
Read-only; makes no API calls. Each skill reports its name,
|
||||||
description, when-to-use guidance, required operations, status
|
description, when-to-use guidance, required operations, status
|
||||||
@@ -7402,6 +7406,9 @@ def mcp_list_project_skills() -> dict:
|
|||||||
'operator-only'), and whether the current profile's operations permit it. Use
|
'operator-only'), and whether the current profile's operations permit it. Use
|
||||||
mcp_get_skill_guide(name) for the step-by-step guide.
|
mcp_get_skill_guide(name) for the step-by-step guide.
|
||||||
|
|
||||||
|
Includes the canonical workflow router under gitea-workflow,
|
||||||
|
llm-project-workflow, and git-pr-workflows (#551).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict with 'read_only', 'count', and 'skills' (list). Never secrets.
|
dict with 'read_only', 'count', and 'skills' (list). Never secrets.
|
||||||
"""
|
"""
|
||||||
@@ -7426,10 +7433,39 @@ def mcp_list_project_skills() -> dict:
|
|||||||
}
|
}
|
||||||
if meta.get("notes"):
|
if meta.get("notes"):
|
||||||
entry["notes"] = meta["notes"]
|
entry["notes"] = meta["notes"]
|
||||||
|
if meta.get("repo_path"):
|
||||||
|
entry["repo_path"] = meta["repo_path"]
|
||||||
|
if meta.get("canonical"):
|
||||||
|
entry["canonical"] = True
|
||||||
skills.append(entry)
|
skills.append(entry)
|
||||||
return {"read_only": True, "count": len(skills), "skills": skills}
|
return {"read_only": True, "count": len(skills), "skills": skills}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def mcp_check_workflow_skill_preflight(
|
||||||
|
project_root: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Fail-closed preflight for the canonical workflow skill wall (#551).
|
||||||
|
|
||||||
|
Verifies skills/llm-project-workflow/SKILL.md exists in the project tree
|
||||||
|
and reports whether Codex has a matching mount under ~/.codex/skills.
|
||||||
|
Call before git/Gitea mutations when the controller requires gitea-workflow.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Optional override; defaults to this MCP process root.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with workflow_skill_ready, blocked, canonical_names, codex mount
|
||||||
|
status, and safe_next_action. Never secrets.
|
||||||
|
"""
|
||||||
|
root = (project_root or PROJECT_ROOT).strip()
|
||||||
|
result = workflow_skill.assess_workflow_skill_presence(root)
|
||||||
|
result["success"] = not result.get("blocked")
|
||||||
|
result["project_root"] = root
|
||||||
|
result["read_only"] = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def mcp_get_skill_guide(skill_name: str) -> dict:
|
def mcp_get_skill_guide(skill_name: str) -> dict:
|
||||||
"""Step-by-step guide for one named project skill (#128).
|
"""Step-by-step guide for one named project skill (#128).
|
||||||
|
|||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install canonical Gitea workflow skill into Codex skills dir (#551).
|
||||||
|
# Symlinks the in-repo skills/llm-project-workflow package under the names
|
||||||
|
# gitea-workflow, llm-project-workflow, and git-pr-workflows.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: scripts/install-codex-workflow-skill.sh [--dry-run] [--skills-dir DIR]
|
||||||
|
|
||||||
|
Symlink the portable skills/llm-project-workflow package into the Codex
|
||||||
|
skills directory under the canonical names:
|
||||||
|
gitea-workflow
|
||||||
|
llm-project-workflow
|
||||||
|
git-pr-workflows
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
skills dir: $CODEX_HOME/skills or ~/.codex/skills
|
||||||
|
source: <repo>/skills/llm-project-workflow
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
skills_dir=""
|
||||||
|
while [[ "${1:-}" == --* ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) dry_run=1 ;;
|
||||||
|
--skills-dir)
|
||||||
|
shift
|
||||||
|
skills_dir="${1:-}"
|
||||||
|
;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd "$script_dir/.." && pwd)"
|
||||||
|
source_skill="$repo_root/skills/llm-project-workflow"
|
||||||
|
|
||||||
|
if [[ ! -f "$source_skill/SKILL.md" ]]; then
|
||||||
|
echo "Error: missing $source_skill/SKILL.md" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$skills_dir" ]]; then
|
||||||
|
if [[ -n "${CODEX_HOME:-}" ]]; then
|
||||||
|
skills_dir="$CODEX_HOME/skills"
|
||||||
|
else
|
||||||
|
skills_dir="${HOME}/.codex/skills"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
names=(gitea-workflow llm-project-workflow git-pr-workflows)
|
||||||
|
|
||||||
|
echo "source: $source_skill"
|
||||||
|
echo "codex skills dir: $skills_dir"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 0 ]]; then
|
||||||
|
mkdir -p "$skills_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for name in "${names[@]}"; do
|
||||||
|
target="$skills_dir/$name"
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "dry-run: ln -sfn $source_skill $target"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -e "$target" || -L "$target" ]]; then
|
||||||
|
if [[ -L "$target" ]]; then
|
||||||
|
rm -f "$target"
|
||||||
|
else
|
||||||
|
echo "Error: $target exists and is not a symlink (refuse to overwrite)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
ln -sfn "$source_skill" "$target"
|
||||||
|
echo "linked $target -> $source_skill"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "done. Restart Codex so skills reload."
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: gitea-workflow
|
||||||
|
description: >-
|
||||||
|
Canonical alias for the Gitea/LLM project workflow router. Same skill as
|
||||||
|
llm-project-workflow. Use at the start of any Gitea-Tools author, review,
|
||||||
|
merge, reconcile, or issue-filing task. Trigger terms: gitea-workflow,
|
||||||
|
llm-project-workflow, git-pr-workflows, gitea, PR review, work-issue.
|
||||||
|
---
|
||||||
|
|
||||||
|
# gitea-workflow (canonical alias)
|
||||||
|
|
||||||
|
This directory is the **stable name** for controller prompts and Codex mounts
|
||||||
|
(`gitea-workflow`). The portable implementation lives at:
|
||||||
|
|
||||||
|
**[`../llm-project-workflow/SKILL.md`](../llm-project-workflow/SKILL.md)**
|
||||||
|
|
||||||
|
## Required action
|
||||||
|
|
||||||
|
1. Open and follow `skills/llm-project-workflow/SKILL.md` (router).
|
||||||
|
2. Load the matching workflow under `skills/llm-project-workflow/workflows/`.
|
||||||
|
3. Do not mutate git/Gitea until the workflow is loaded.
|
||||||
|
|
||||||
|
## Multi-runtime names (must resolve identically)
|
||||||
|
|
||||||
|
| Name | Role |
|
||||||
|
|------|------|
|
||||||
|
| `gitea-workflow` | Canonical controller / Codex skill name |
|
||||||
|
| `llm-project-workflow` | Portable in-repo skill package |
|
||||||
|
| `git-pr-workflows` | Legacy alias |
|
||||||
|
|
||||||
|
Install for Codex:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/install-codex-workflow-skill.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Preflight via MCP: `mcp_check_workflow_skill_preflight`.
|
||||||
@@ -6,6 +6,8 @@ description: >-
|
|||||||
report schema. Use at the start of any implementation, review, merge,
|
report schema. Use at the start of any implementation, review, merge,
|
||||||
reconciliation, or issue-filing task.
|
reconciliation, or issue-filing task.
|
||||||
---
|
---
|
||||||
|
> **Also known as:** `gitea-workflow`, `git-pr-workflows` (canonical multi-runtime names — see docs/workflow-skill-mount.md / #551).
|
||||||
|
|
||||||
|
|
||||||
# LLM Project Workflow Skill
|
# LLM Project Workflow Skill
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ EXPECTED_SKILLS = [
|
|||||||
"jenkins-mcp",
|
"jenkins-mcp",
|
||||||
"glitchtip-mcp",
|
"glitchtip-mcp",
|
||||||
"release-operator",
|
"release-operator",
|
||||||
|
# Canonical workflow router (#551) — same skill, multi-runtime names
|
||||||
|
"gitea-workflow",
|
||||||
|
"llm-project-workflow",
|
||||||
|
"git-pr-workflows",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Tests for canonical workflow skill naming and preflight (#551)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import workflow_skill
|
||||||
|
import mcp_server
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkflowSkillModule(unittest.TestCase):
|
||||||
|
def test_canonical_names_include_gitea_workflow(self):
|
||||||
|
names = workflow_skill.CANONICAL_WORKFLOW_SKILL_NAMES
|
||||||
|
self.assertIn("gitea-workflow", names)
|
||||||
|
self.assertIn("llm-project-workflow", names)
|
||||||
|
self.assertIn("git-pr-workflows", names)
|
||||||
|
|
||||||
|
def test_repo_skill_present_in_this_checkout(self):
|
||||||
|
root = str(Path(__file__).resolve().parent.parent)
|
||||||
|
result = workflow_skill.assess_workflow_skill_presence(root)
|
||||||
|
self.assertTrue(result["repo_skill_present"])
|
||||||
|
self.assertTrue(result["repo_alias_present"])
|
||||||
|
self.assertTrue(result["workflow_skill_ready"])
|
||||||
|
self.assertFalse(result["blocked"])
|
||||||
|
|
||||||
|
def test_missing_repo_skill_blocks(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
result = workflow_skill.assess_workflow_skill_presence(tmp)
|
||||||
|
self.assertTrue(result["blocked"])
|
||||||
|
self.assertFalse(result["workflow_skill_ready"])
|
||||||
|
self.assertTrue(any("missing" in r for r in result["reasons"]))
|
||||||
|
|
||||||
|
def test_codex_mount_detection(self):
|
||||||
|
root = str(Path(__file__).resolve().parent.parent)
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
skill_dir = Path(tmp) / "gitea-workflow"
|
||||||
|
skill_dir.mkdir()
|
||||||
|
(skill_dir / "SKILL.md").write_text("# x\n", encoding="utf-8")
|
||||||
|
result = workflow_skill.assess_workflow_skill_presence(
|
||||||
|
root, codex_skills_dir=tmp
|
||||||
|
)
|
||||||
|
self.assertTrue(result["codex_skill_mounted"])
|
||||||
|
self.assertTrue(result["codex_skill_mounts"]["gitea-workflow"])
|
||||||
|
|
||||||
|
def test_registry_entries_cover_aliases(self):
|
||||||
|
entries = workflow_skill.workflow_skill_registry_entries()
|
||||||
|
for name in workflow_skill.CANONICAL_WORKFLOW_SKILL_NAMES:
|
||||||
|
self.assertIn(name, entries)
|
||||||
|
self.assertEqual(entries[name]["status"], "available")
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkflowSkillMcpTools(unittest.TestCase):
|
||||||
|
def test_list_includes_canonical_names(self):
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"GITEA_PROFILE_NAME": "author-test",
|
||||||
|
"GITEA_ALLOWED_OPERATIONS": "gitea.read",
|
||||||
|
"GITEA_FORBIDDEN_OPERATIONS": "",
|
||||||
|
},
|
||||||
|
clear=False,
|
||||||
|
):
|
||||||
|
result = mcp_server.mcp_list_project_skills()
|
||||||
|
names = {s["name"] for s in result["skills"]}
|
||||||
|
for name in workflow_skill.CANONICAL_WORKFLOW_SKILL_NAMES:
|
||||||
|
self.assertIn(name, names)
|
||||||
|
|
||||||
|
def test_get_skill_guide_gitea_workflow(self):
|
||||||
|
guide = mcp_server.mcp_get_skill_guide("gitea-workflow")
|
||||||
|
self.assertTrue(guide["success"])
|
||||||
|
self.assertTrue(guide["steps"])
|
||||||
|
self.assertIn("workflow", " ".join(guide["steps"]).lower())
|
||||||
|
|
||||||
|
def test_preflight_tool_ok_on_repo(self):
|
||||||
|
result = mcp_server.mcp_check_workflow_skill_preflight()
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertTrue(result["workflow_skill_ready"])
|
||||||
|
self.assertFalse(result["blocked"])
|
||||||
|
self.assertIn("gitea-workflow", result["canonical_names"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -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.",
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user