From 6670c72e65f89606d7ca86e3cd613a0a78797b73 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Tue, 7 Jul 2026 13:31:40 -0400 Subject: [PATCH 1/2] feat: add canonical workflow prompt library to web UI (Closes #428) 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) --- docs/webui-local-dev.md | 13 +- tests/test_webui_prompt_library.py | 81 ++++++++++++ tests/test_webui_skeleton.py | 7 +- webui/app.py | 12 +- webui/layout.py | 33 ++++- webui/prompt_library.py | 192 +++++++++++++++++++++++++++++ webui/prompt_views.py | 80 ++++++++++++ 7 files changed, 410 insertions(+), 8 deletions(-) create mode 100644 tests/test_webui_prompt_library.py create mode 100644 webui/prompt_library.py create mode 100644 webui/prompt_views.py diff --git a/docs/webui-local-dev.md b/docs/webui-local-dev.md index 011b71a..4b94aed 100644 --- a/docs/webui-local-dev.md +++ b/docs/webui-local-dev.md @@ -39,7 +39,8 @@ Optional environment variables: | `/projects` | Project registry list (#427) | | `/projects/{id}` | Project detail + onboarding checklist | | `/api/projects` | JSON registry export | -| `/prompts` | Stub — prompt library (#428) | +| `/prompts` | Prompt library with per-prompt copy buttons (#428) | +| `/api/prompts` | JSON prompt export with workflow hashes | | `/runtime` | Stub — MCP runtime health (#430) | | `/audit` | Stub — report audit paste (#431) | | `/worktrees` | Stub — hygiene dashboard (#432) | @@ -60,8 +61,16 @@ or credentials. Seed entry: **Gitea-Tools** on `https://gitea.prgs.cc` with `prgs-author`, `prgs-reviewer`, and `prgs-reconciler` profiles. +## Prompt library (#428) + +Prompts are generated at load time from canonical workflow files under +`skills/llm-project-workflow/workflows/`. SHA-256 hashes are computed from +`WEBUI_REPO_ROOT` (defaults to the repository root). Prompt bodies are short +copy/paste starters; canonical workflow files remain the only full policy +source. + ## Tests ```bash -pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py -q +pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py -q ``` \ No newline at end of file diff --git a/tests/test_webui_prompt_library.py b/tests/test_webui_prompt_library.py new file mode 100644 index 0000000..442800d --- /dev/null +++ b/tests/test_webui_prompt_library.py @@ -0,0 +1,81 @@ +"""Tests for web UI prompt library (#428).""" +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from starlette.testclient import TestClient + +from webui.app import create_app +from webui.prompt_library import load_prompt_library, prompt_to_dict + + +class TestWebuiPromptLibrary(unittest.TestCase): + def setUp(self): + self.client = TestClient(create_app()) + + def test_library_has_required_prompts(self): + entries = load_prompt_library() + slugs = {entry.slug for entry in entries} + self.assertEqual( + slugs, + { + "review-pr", + "work-issue", + "create-issue", + "comment-issue", + "cleanup", + "audit", + "onboarding", + }, + ) + + def test_workflow_entries_include_hash_and_task_mode(self): + review = next(e for e in load_prompt_library() if e.slug == "review-pr") + self.assertEqual( + review.workflow_path, + "skills/llm-project-workflow/workflows/review-merge-pr.md", + ) + self.assertEqual(review.task_mode, "review-merge-pr") + self.assertIsNotNone(review.workflow_hash) + self.assertGreater(len(review.workflow_hash), 12) + self.assertIn("eligible open PR", review.prompt_text) + + def test_comment_issue_defers_to_workflow(self): + comment = next(e for e in load_prompt_library() if e.slug == "comment-issue") + self.assertIn("comment_issue", comment.prompt_text) + self.assertIn("create-issue", comment.workflow_path) + self.assertIn("§16", comment.source_note) + + def test_prompts_page_renders_cards_and_copy_buttons(self): + response = self.client.get("/prompts") + self.assertEqual(response.status_code, 200) + text = response.text + self.assertIn("Prompt library", text) + self.assertIn("review-merge-pr.md", text) + self.assertIn("Copy prompt", text) + self.assertIn('data-copy-target="prompt-review-pr"', text) + self.assertNotIn("child issue", text.lower()) + + def test_api_prompts_json(self): + response = self.client.get("/api/prompts") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], 7) + self.assertEqual(len(data["prompts"]), 7) + first = data["prompts"][0] + self.assertIn("slug", first) + self.assertIn("prompt_text", first) + self.assertIn("workflow_path", first) + self.assertIn("workflow_hash", first) + + def test_prompt_to_dict_round_trip(self): + entry = load_prompt_library()[0] + payload = prompt_to_dict(entry) + self.assertEqual(payload["slug"], entry.slug) + self.assertEqual(payload["prompt_text"], entry.prompt_text) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_webui_skeleton.py b/tests/test_webui_skeleton.py index 7e139c6..937b095 100644 --- a/tests/test_webui_skeleton.py +++ b/tests/test_webui_skeleton.py @@ -30,12 +30,17 @@ class TestWebuiSkeleton(unittest.TestCase): self.assertIn("Read-only MVP", response.text) def test_route_stubs_render(self): - for path in ("/prompts", "/runtime", "/audit"): + for path in ("/runtime", "/audit"): with self.subTest(path=path): response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertIn("child issue", response.text.lower()) + def test_prompts_is_implemented(self): + response = self.client.get("/prompts") + self.assertEqual(response.status_code, 200) + self.assertIn("Prompt library", response.text) + def test_projects_is_implemented(self): response = self.client.get("/projects") self.assertEqual(response.status_code, 200) diff --git a/webui/app.py b/webui/app.py index 4f1cba9..dc99cdb 100644 --- a/webui/app.py +++ b/webui/app.py @@ -12,6 +12,8 @@ from starlette.routing import Route from webui.layout import render_page from webui.project_registry import find_project, load_registry, registry_to_dict from webui.project_views import render_project_detail, render_projects_list +from webui.prompt_library import library_to_dict +from webui.prompt_views import render_prompts_page _READ_ONLY_METHODS = frozenset({"GET", "HEAD", "OPTIONS"}) @@ -80,10 +82,11 @@ async def api_projects(_request: Request) -> JSONResponse: async def prompts(_request: Request) -> HTMLResponse: - return _stub_page( - "Prompts", - "Prompt library will surface canonical workflows from skills/llm-project-workflow/.", - ) + return HTMLResponse(render_prompts_page()) + + +async def api_prompts(_request: Request) -> JSONResponse: + return JSONResponse(library_to_dict()) async def runtime(_request: Request) -> HTMLResponse: @@ -134,6 +137,7 @@ def create_app() -> Starlette: Route("/projects/{project_id}", project_detail, methods=["GET"]), Route("/api/projects", api_projects, methods=["GET"]), Route("/prompts", prompts, methods=["GET"]), + Route("/api/prompts", api_prompts, methods=["GET"]), Route("/runtime", runtime, methods=["GET"]), Route("/audit", audit, methods=["GET"]), Route("/worktrees", worktrees, methods=["GET"]), diff --git a/webui/layout.py b/webui/layout.py index fc75efb..d948204 100644 --- a/webui/layout.py +++ b/webui/layout.py @@ -18,7 +18,7 @@ MVP_NOTICE = ( ) -def render_page(*, title: str, body_html: str) -> str: +def render_page(*, title: str, body_html: str, extra_head: str = "") -> str: nav_links = "".join( f'{label}' for href, label in NAV_ITEMS ) @@ -115,7 +115,38 @@ def render_page(*, title: str, body_html: str) -> str: }} ol.checklist li {{ margin-bottom: 0.85rem; }} ol.checklist p {{ margin: 0.25rem 0 0; font-size: 0.9rem; }} + .prompt-card {{ + margin: 1.25rem 0 1.75rem; + padding: 1rem 1.1rem; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + }} + .prompt-card h3 {{ margin: 0 0 0.5rem; font-size: 1.05rem; }} + pre.prompt-text {{ + white-space: pre-wrap; + word-break: break-word; + margin: 0.75rem 0; + padding: 0.75rem 0.85rem; + border-radius: 6px; + background: var(--bg); + border: 1px solid var(--border); + font-size: 0.9rem; + color: var(--text); + }} + .copy-btn {{ + background: var(--accent); + color: #0b1219; + border: none; + border-radius: 6px; + padding: 0.4rem 0.85rem; + font-size: 0.85rem; + cursor: pointer; + }} + .copy-btn:hover {{ filter: brightness(1.08); }} + .muted {{ color: var(--muted); }} + {extra_head}
diff --git a/webui/prompt_library.py b/webui/prompt_library.py new file mode 100644 index 0000000..5d2e768 --- /dev/null +++ b/webui/prompt_library.py @@ -0,0 +1,192 @@ +"""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], + } \ No newline at end of file diff --git a/webui/prompt_views.py b/webui/prompt_views.py new file mode 100644 index 0000000..1a6b22c --- /dev/null +++ b/webui/prompt_views.py @@ -0,0 +1,80 @@ +"""HTML views for the prompt library (#428).""" + +from __future__ import annotations + +import html + +from webui.prompt_library import PromptEntry, load_prompt_library + + +def _escape(text: str) -> str: + return html.escape(text, quote=True) + + +def render_prompts_page() -> str: + entries = load_prompt_library() + cards: list[str] = [] + for entry in entries: + cards.append(_render_prompt_card(entry)) + body = ( + "

Prompt library

" + "

Short copy/paste task prompts derived from canonical workflows. " + "Full policy remains in the cited workflow files — not duplicated here.

" + f"{''.join(cards)}" + f"{PROMPT_PAGE_SCRIPT}" + ) + from webui.layout import render_page + + return render_page(title="Prompts", body_html=body) + + +def _render_prompt_card(entry: PromptEntry) -> str: + hash_short = ( + f"{_escape(entry.workflow_hash[:12])}" + if entry.workflow_hash + else "n/a" + ) + task_mode = ( + f"{_escape(entry.task_mode)}" + if entry.task_mode + else "n/a" + ) + note = ( + f'

{_escape(entry.source_note)}

' + if entry.source_note + else "" + ) + prompt_id = f"prompt-{entry.slug}" + return ( + f'
' + f"

{_escape(entry.label)}

" + f'

Workflow: {_escape(entry.workflow_path)} · ' + f"task_mode: {task_mode} · sha256: {hash_short}

" + f'
{_escape(entry.prompt_text)}
' + f'" + f"{note}" + "
" + ) + + +PROMPT_PAGE_SCRIPT = """ + +""" \ No newline at end of file -- 2.43.7 From f845864889a2b9d9f236abde873c3b90dff9c041 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Tue, 7 Jul 2026 13:32:05 -0400 Subject: [PATCH 2/2] feat: add web UI prompt library from canonical workflows (#428) Surface short copy/paste operator prompts derived from canonical workflow files with workflow path and SHA-256 citations. Adds /prompts, /prompts/{id}, and /api/prompts with one-click copy. Closes #428 --- tests/test_webui_prompt_library.py | 113 ++++++++++++++++------------- webui/app.py | 23 +++++- webui/prompt_library.py | 35 +++++---- webui/prompt_views.py | 43 ++++++----- 4 files changed, 127 insertions(+), 87 deletions(-) diff --git a/tests/test_webui_prompt_library.py b/tests/test_webui_prompt_library.py index 442800d..3538c2d 100644 --- a/tests/test_webui_prompt_library.py +++ b/tests/test_webui_prompt_library.py @@ -1,4 +1,5 @@ """Tests for web UI prompt library (#428).""" +import json import sys import unittest from pathlib import Path @@ -8,73 +9,81 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from starlette.testclient import TestClient from webui.app import create_app -from webui.prompt_library import load_prompt_library, prompt_to_dict +from webui.prompt_library import find_prompt, library_to_dict, load_prompt_library, prompt_to_dict + +REQUIRED_PROMPT_SLUGS = frozenset({ + "review-pr", + "work-issue", + "create-issue", + "comment-issue", + "cleanup", + "audit", + "onboarding", +}) -class TestWebuiPromptLibrary(unittest.TestCase): +class TestPromptLibraryLoader(unittest.TestCase): + def test_library_loads_required_prompts(self): + entries = load_prompt_library() + slugs = {entry.slug for entry in entries} + self.assertEqual(slugs, REQUIRED_PROMPT_SLUGS) + + def test_workflow_hashes_present(self): + review = find_prompt("review-pr") + self.assertIsNotNone(review) + assert review is not None + self.assertTrue(review.workflow_hash) + self.assertEqual(len(review.workflow_hash), 64) + + def test_prompt_text_is_short(self): + for entry in load_prompt_library(): + self.assertLess(len(entry.prompt_text), 400) + self.assertNotIn("Do not improvise around the gates", entry.prompt_text) + + +class TestPromptLibraryRoutes(unittest.TestCase): def setUp(self): self.client = TestClient(create_app()) - def test_library_has_required_prompts(self): - entries = load_prompt_library() - slugs = {entry.slug for entry in entries} - self.assertEqual( - slugs, - { - "review-pr", - "work-issue", - "create-issue", - "comment-issue", - "cleanup", - "audit", - "onboarding", - }, - ) - - def test_workflow_entries_include_hash_and_task_mode(self): - review = next(e for e in load_prompt_library() if e.slug == "review-pr") - self.assertEqual( - review.workflow_path, - "skills/llm-project-workflow/workflows/review-merge-pr.md", - ) - self.assertEqual(review.task_mode, "review-merge-pr") - self.assertIsNotNone(review.workflow_hash) - self.assertGreater(len(review.workflow_hash), 12) - self.assertIn("eligible open PR", review.prompt_text) - - def test_comment_issue_defers_to_workflow(self): - comment = next(e for e in load_prompt_library() if e.slug == "comment-issue") - self.assertIn("comment_issue", comment.prompt_text) - self.assertIn("create-issue", comment.workflow_path) - self.assertIn("§16", comment.source_note) - - def test_prompts_page_renders_cards_and_copy_buttons(self): + def test_prompts_page_lists_all_entries(self): response = self.client.get("/prompts") self.assertEqual(response.status_code, 200) - text = response.text - self.assertIn("Prompt library", text) - self.assertIn("review-merge-pr.md", text) - self.assertIn("Copy prompt", text) - self.assertIn('data-copy-target="prompt-review-pr"', text) - self.assertNotIn("child issue", text.lower()) + for label in ( + "Review PR", + "Work issue", + "Create issue", + "Comment on issue", + "Post-merge cleanup", + "Reconciliation audit", + "Project onboarding", + ): + self.assertIn(label, response.text) + self.assertIn("Copy prompt", response.text) + self.assertIn("sha256:", response.text) + self.assertNotIn("child issue", response.text.lower()) + + def test_prompt_detail_route(self): + response = self.client.get("/prompts/work-issue") + self.assertEqual(response.status_code, 200) + self.assertIn("work-issue.md", response.text) + self.assertIn("Copy prompt", response.text) + + def test_prompt_detail_404(self): + self.assertEqual(self.client.get("/prompts/missing").status_code, 404) def test_api_prompts_json(self): response = self.client.get("/api/prompts") self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(data["count"], 7) - self.assertEqual(len(data["prompts"]), 7) - first = data["prompts"][0] - self.assertIn("slug", first) - self.assertIn("prompt_text", first) - self.assertIn("workflow_path", first) - self.assertIn("workflow_hash", first) + review = next(item for item in data["prompts"] if item["slug"] == "review-pr") + self.assertIn("workflow_hash", review) + self.assertIn("review-merge-pr.md", review["workflow_path"]) - def test_prompt_to_dict_round_trip(self): + def test_prompt_to_dict_roundtrip(self): entry = load_prompt_library()[0] - payload = prompt_to_dict(entry) - self.assertEqual(payload["slug"], entry.slug) - self.assertEqual(payload["prompt_text"], entry.prompt_text) + encoded = json.dumps(prompt_to_dict(entry)) + self.assertIn("workflow_path", encoded) if __name__ == "__main__": diff --git a/webui/app.py b/webui/app.py index dc99cdb..d1e1c38 100644 --- a/webui/app.py +++ b/webui/app.py @@ -12,8 +12,8 @@ from starlette.routing import Route from webui.layout import render_page from webui.project_registry import find_project, load_registry, registry_to_dict from webui.project_views import render_project_detail, render_projects_list -from webui.prompt_library import library_to_dict -from webui.prompt_views import render_prompts_page +from webui.prompt_library import find_prompt, library_to_dict +from webui.prompt_views import render_prompt_detail, render_prompts_page _READ_ONLY_METHODS = frozenset({"GET", "HEAD", "OPTIONS"}) @@ -85,6 +85,24 @@ async def prompts(_request: Request) -> HTMLResponse: return HTMLResponse(render_prompts_page()) +async def prompt_detail(request: Request) -> HTMLResponse: + prompt_id = request.path_params["prompt_id"] + prompt = find_prompt(prompt_id) + if prompt is None: + return HTMLResponse( + render_page( + title="Prompt not found", + body_html=( + "

Prompt not found

" + f"

No library entry for {prompt_id}.

" + '

← All prompts

' + ), + ), + status_code=404, + ) + return HTMLResponse(render_prompt_detail(prompt)) + + async def api_prompts(_request: Request) -> JSONResponse: return JSONResponse(library_to_dict()) @@ -137,6 +155,7 @@ def create_app() -> Starlette: Route("/projects/{project_id}", project_detail, methods=["GET"]), Route("/api/projects", api_projects, methods=["GET"]), Route("/prompts", prompts, methods=["GET"]), + Route("/prompts/{prompt_id}", prompt_detail, methods=["GET"]), Route("/api/prompts", api_prompts, methods=["GET"]), Route("/runtime", runtime, methods=["GET"]), Route("/audit", audit, methods=["GET"]), diff --git a/webui/prompt_library.py b/webui/prompt_library.py index 5d2e768..e5ec1d7 100644 --- a/webui/prompt_library.py +++ b/webui/prompt_library.py @@ -128,7 +128,7 @@ def load_prompt_library() -> tuple[PromptEntry, ...]: ), _static_entry( slug="comment-issue", - label="Comment issue", + label="Comment on issue", workflow_path=str(_WORKFLOW_ROOT / "create-issue.md"), prompt_text=( "Comment on the target Gitea issue only if exact comment_issue " @@ -140,16 +140,14 @@ def load_prompt_library() -> tuple[PromptEntry, ...]: ), _static_entry( slug="cleanup", - label="PR queue cleanup", - workflow_path=str(_WORKFLOW_ROOT / "pr-queue-cleanup.md"), + label="Post-merge cleanup", + workflow_path="skills/llm-project-workflow/templates/worktree-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." + "Task: clean up branch/worktree for PR # / issue # after merge. " + "Confirm the merge on remote master before any deletion; never " + "force-remove a dirty worktree." ), - source_note="Derived from pr-queue-cleanup.md; composes with review-merge-pr.md.", + source_note="Full cleanup steps live in templates/worktree-cleanup.md.", ), _entry_from_workflow( slug="audit", @@ -159,19 +157,26 @@ def load_prompt_library() -> tuple[PromptEntry, ...]: _static_entry( slug="onboarding", label="Project onboarding", - workflow_path="docs/webui-local-dev.md", + workflow_path="skills/llm-project-workflow/SKILL.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." + "Onboard this repository into the MCP Control Plane: prove identity " + "and task capability, configure author/reviewer/reconciler profiles " + "in separate namespaces, then complete the checklist at /projects. " + "Canonical router: skills/llm-project-workflow/SKILL.md." ), - source_note="Operator onboarding; checklist details live in the project registry UI.", + source_note="Checklist details live in webui/data/projects.registry.json and /projects.", ), ) return entries +def find_prompt(slug: str) -> PromptEntry | None: + for entry in load_prompt_library(): + if entry.slug == slug: + return entry + return None + + def prompt_to_dict(entry: PromptEntry) -> dict[str, Any]: return { "slug": entry.slug, diff --git a/webui/prompt_views.py b/webui/prompt_views.py index 1a6b22c..fe6d687 100644 --- a/webui/prompt_views.py +++ b/webui/prompt_views.py @@ -4,6 +4,7 @@ from __future__ import annotations import html +from webui.layout import render_page from webui.prompt_library import PromptEntry, load_prompt_library @@ -11,23 +12,6 @@ def _escape(text: str) -> str: return html.escape(text, quote=True) -def render_prompts_page() -> str: - entries = load_prompt_library() - cards: list[str] = [] - for entry in entries: - cards.append(_render_prompt_card(entry)) - body = ( - "

Prompt library

" - "

Short copy/paste task prompts derived from canonical workflows. " - "Full policy remains in the cited workflow files — not duplicated here.

" - f"{''.join(cards)}" - f"{PROMPT_PAGE_SCRIPT}" - ) - from webui.layout import render_page - - return render_page(title="Prompts", body_html=body) - - def _render_prompt_card(entry: PromptEntry) -> str: hash_short = ( f"{_escape(entry.workflow_hash[:12])}" @@ -77,4 +61,27 @@ document.querySelectorAll('.copy-btn').forEach((btn) => { }); }); -""" \ No newline at end of file +""" + + +def render_prompts_page() -> str: + entries = load_prompt_library() + cards = "".join(_render_prompt_card(entry) for entry in entries) + body = ( + "

Prompt library

" + "

Short copy/paste task prompts derived from canonical workflows. " + "Full policy remains in the cited workflow files — not duplicated here.

" + f"{cards}" + "

JSON API

" + f"{PROMPT_PAGE_SCRIPT}" + ) + return render_page(title="Prompts", body_html=body) + + +def render_prompt_detail(entry: PromptEntry) -> str: + body = ( + f"

← All prompts

" + f"{_render_prompt_card(entry)}" + f"{PROMPT_PAGE_SCRIPT}" + ) + return render_page(title=entry.label, body_html=body) \ No newline at end of file -- 2.43.7