Expose short copy/paste operator prompts on /prompts derived from canonical workflow files with workflow path and SHA-256 hash. Adds JSON export at /api/prompts, copy buttons, and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
192 lines
6.1 KiB
Python
192 lines
6.1 KiB
Python
"""Canonical workflow prompt library for the internal web UI (#428)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
_WORKFLOW_ROOT = Path("skills/llm-project-workflow/workflows")
|
|
|
|
_DEFAULT_PROMPT_RE = re.compile(
|
|
r"\*\*Default task prompt:\*\*\s*\n+>\s*(.+?)(?=\n\n|\nDo not improvise)",
|
|
re.DOTALL,
|
|
)
|
|
_FRONTMATTER_TASK_MODE_RE = re.compile(r"^task_mode:\s*(\S+)", re.MULTILINE)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PromptEntry:
|
|
slug: str
|
|
label: str
|
|
prompt_text: str
|
|
workflow_path: str
|
|
task_mode: str | None
|
|
workflow_hash: str | None
|
|
source_note: str
|
|
|
|
|
|
def _repo_root() -> Path:
|
|
override = (os.environ.get("WEBUI_REPO_ROOT") or "").strip()
|
|
if override:
|
|
return Path(override).resolve()
|
|
return Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _workflow_file(path: str) -> Path:
|
|
return _repo_root() / path
|
|
|
|
|
|
def _sha256_hex(content: str) -> str:
|
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _read_workflow(path: str) -> tuple[str, str]:
|
|
file_path = _workflow_file(path)
|
|
text = file_path.read_text(encoding="utf-8")
|
|
return text, _sha256_hex(text)
|
|
|
|
|
|
def _extract_default_prompt(markdown: str) -> str | None:
|
|
match = _DEFAULT_PROMPT_RE.search(markdown)
|
|
if not match:
|
|
return None
|
|
lines = [line.strip() for line in match.group(1).splitlines()]
|
|
return " ".join(line for line in lines if line)
|
|
|
|
|
|
def _extract_task_mode(markdown: str) -> str | None:
|
|
match = _FRONTMATTER_TASK_MODE_RE.search(markdown)
|
|
return match.group(1) if match else None
|
|
|
|
|
|
def _entry_from_workflow(
|
|
*,
|
|
slug: str,
|
|
label: str,
|
|
workflow_path: str,
|
|
prompt_override: str | None = None,
|
|
source_note: str = "",
|
|
) -> PromptEntry:
|
|
markdown, digest = _read_workflow(workflow_path)
|
|
prompt_text = prompt_override or _extract_default_prompt(markdown)
|
|
if not prompt_text:
|
|
raise ValueError(f"No default task prompt found in {workflow_path}")
|
|
return PromptEntry(
|
|
slug=slug,
|
|
label=label,
|
|
prompt_text=prompt_text,
|
|
workflow_path=workflow_path,
|
|
task_mode=_extract_task_mode(markdown),
|
|
workflow_hash=digest,
|
|
source_note=source_note,
|
|
)
|
|
|
|
|
|
def _static_entry(
|
|
*,
|
|
slug: str,
|
|
label: str,
|
|
prompt_text: str,
|
|
workflow_path: str,
|
|
source_note: str,
|
|
) -> PromptEntry:
|
|
path = _workflow_file(workflow_path)
|
|
digest = _sha256_hex(path.read_text(encoding="utf-8")) if path.is_file() else None
|
|
markdown = path.read_text(encoding="utf-8") if path.is_file() else ""
|
|
return PromptEntry(
|
|
slug=slug,
|
|
label=label,
|
|
prompt_text=prompt_text,
|
|
workflow_path=workflow_path,
|
|
task_mode=_extract_task_mode(markdown) if markdown else None,
|
|
workflow_hash=digest,
|
|
source_note=source_note,
|
|
)
|
|
|
|
|
|
def load_prompt_library() -> tuple[PromptEntry, ...]:
|
|
"""Load operator prompts derived from canonical workflows."""
|
|
entries = (
|
|
_entry_from_workflow(
|
|
slug="review-pr",
|
|
label="Review PR",
|
|
workflow_path=str(_WORKFLOW_ROOT / "review-merge-pr.md"),
|
|
),
|
|
_entry_from_workflow(
|
|
slug="work-issue",
|
|
label="Work issue",
|
|
workflow_path=str(_WORKFLOW_ROOT / "work-issue.md"),
|
|
),
|
|
_entry_from_workflow(
|
|
slug="create-issue",
|
|
label="Create issue",
|
|
workflow_path=str(_WORKFLOW_ROOT / "create-issue.md"),
|
|
),
|
|
_static_entry(
|
|
slug="comment-issue",
|
|
label="Comment issue",
|
|
workflow_path=str(_WORKFLOW_ROOT / "create-issue.md"),
|
|
prompt_text=(
|
|
"Comment on the target Gitea issue only if exact comment_issue "
|
|
"capability is proven. Load the canonical create-issue workflow "
|
|
"first and follow §16 (comment-on-existing issue rule). Include "
|
|
"specific evidence; do not duplicate existing comments."
|
|
),
|
|
source_note="Derived from create-issue.md §16; full policy remains in the workflow file.",
|
|
),
|
|
_static_entry(
|
|
slug="cleanup",
|
|
label="PR queue cleanup",
|
|
workflow_path=str(_WORKFLOW_ROOT / "pr-queue-cleanup.md"),
|
|
prompt_text=(
|
|
"Run PR-only queue cleanup: build a complete open-PR inventory "
|
|
"with pagination proof, select exactly one eligible PR, dispatch "
|
|
"one canonical review for that PR, then stop after any terminal "
|
|
"review mutation. Load pr-queue-cleanup.md and review-merge-pr.md "
|
|
"before any mutation."
|
|
),
|
|
source_note="Derived from pr-queue-cleanup.md; composes with review-merge-pr.md.",
|
|
),
|
|
_entry_from_workflow(
|
|
slug="audit",
|
|
label="Reconciliation audit",
|
|
workflow_path=str(_WORKFLOW_ROOT / "reconcile-landed-pr.md"),
|
|
),
|
|
_static_entry(
|
|
slug="onboarding",
|
|
label="Project onboarding",
|
|
workflow_path="docs/webui-local-dev.md",
|
|
prompt_text=(
|
|
"Start a new MCP Control Plane session: call mcp_get_control_plane_guide, "
|
|
"prove identity and task capability, then complete the onboarding checklist "
|
|
"for this project at /projects. Keep Gitea, MCP tools, and canonical "
|
|
"workflows as the source of truth."
|
|
),
|
|
source_note="Operator onboarding; checklist details live in the project registry UI.",
|
|
),
|
|
)
|
|
return entries
|
|
|
|
|
|
def prompt_to_dict(entry: PromptEntry) -> dict[str, Any]:
|
|
return {
|
|
"slug": entry.slug,
|
|
"label": entry.label,
|
|
"prompt_text": entry.prompt_text,
|
|
"workflow_path": entry.workflow_path,
|
|
"task_mode": entry.task_mode,
|
|
"workflow_hash": entry.workflow_hash,
|
|
"source_note": entry.source_note,
|
|
}
|
|
|
|
|
|
def library_to_dict() -> dict[str, Any]:
|
|
entries = load_prompt_library()
|
|
return {
|
|
"count": len(entries),
|
|
"prompts": [prompt_to_dict(entry) for entry in entries],
|
|
} |