docs(prompts): ORCH-092 — аудит 6 агент-промптов (расхардкод, escalation, чистка)
Эпилог эпика ORCH-52. Docs/prompts-only: src/**, STAGE_TRANSITIONS, QG_CHECKS, machine-verdict ключи и схема БД не тронуты; frontmatter_validation_strict=False. - FR-1/FR-2: копируемые frontmatter-примеры всех 6 промптов расхардкожены (created_at: <YYYY-MM-DD> / model_used: <resolve ORCH-41> + врезка «не копируй буквально, подставь date +%F и модель из конфига»); литерал claude-opus-4-8 — только справка в таблице полей. - FR-3: имена check_* в промптах сверены с QG_CHECKS — несовпадений нет (закреплено интеграционным тестом TC-03). - FR-4: developer «PR>1500 → разбивай» переформулирован в эскалацию на уровне задач. - FR-5: секция <escalation> у developer/reviewer/tester (после </success_criteria>): back-to:analysis / back-to:dev / REQUEST_CHANGES. - FR-6: deployer — критичные self-hosting-запреты в видной рамке в начале <context>. - FR-7: tester обогащён worktree-путём, smoke serial_gate (ORCH-088), покрытием TC. - FR-8: из reviewer удалена мёртвая строка «тот же экземпляр Developer». - FR-9 (ADR-001 D1): убран ручной git rebase origin/main — свежесть базы держит движок (serial-gate ORCH-088 + auto_rebase_onto_main под merge-lease). - FR-10 (ADR-001 D2): deployer.md оставлен на английском как нормативное исключение. - FR-11: расширен tests/test_agent_prompts_canon.py (ORCH-092 TC-01…TC-08); канон 52d и test_agent_frontmatter_no_model.py зелёные; полный регресс 1278 зелёный. Документация: 6 промптов, CLAUDE.md, docs/architecture/README.md, CHANGELOG.md. Refs: ORCH-092 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ Covers test-plan TC-01..TC-07. TC-08 lives in
|
||||
regression (TC-10) is the rest of `tests/`.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -279,3 +280,156 @@ def test_reviewer_carries_overview_docs_axis():
|
||||
assert "ORCH-079" in text, (
|
||||
"reviewer.md does not anchor the overview-docs axis to ORCH-079"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ORCH-092 (epilogue of epic ORCH-52): prompt audit of the 6 agents —
|
||||
# de-hardcode date/model, gate-name parity, escalation sections, dead-line
|
||||
# removal, tester enrichment, deployer ban-frame. Pure-text checks; only
|
||||
# TC-03 imports `src/` (the QG_CHECKS registry parity check).
|
||||
# Covers test-plan TC-01..TC-08 (TC-09/TC-10/TC-11 = existing canon + full regression).
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def _fenced_blocks(text: str) -> list[str]:
|
||||
"""Return the body of every ``` fenced code block (the *copyable* examples)."""
|
||||
blocks: list[str] = []
|
||||
inside = False
|
||||
buf: list[str] = []
|
||||
for line in text.splitlines():
|
||||
if line.lstrip().startswith("```"):
|
||||
if inside:
|
||||
blocks.append("\n".join(buf))
|
||||
buf = []
|
||||
inside = not inside
|
||||
continue
|
||||
if inside:
|
||||
buf.append(line)
|
||||
return blocks
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent", _AGENTS)
|
||||
def test_orch092_created_at_is_placeholder_not_literal(agent):
|
||||
"""TC-01 (AC-1): copyable example uses a date placeholder + a substitution note.
|
||||
|
||||
The field name `created_at` stays; only its value becomes a placeholder. No
|
||||
literal date may survive inside a ``` fenced (copyable) block, else an agent
|
||||
would copy a stale date verbatim.
|
||||
"""
|
||||
text = _read(agent)
|
||||
assert "created_at: <YYYY-MM-DD>" in text, (
|
||||
f"{agent}.md does not use the created_at: <YYYY-MM-DD> placeholder"
|
||||
)
|
||||
for block in _fenced_blocks(text):
|
||||
assert re.search(r"created_at:\s*\d", block) is None, (
|
||||
f"{agent}.md still hardcodes a literal created_at date in a copyable block"
|
||||
)
|
||||
assert "date +%F" in text, (
|
||||
f"{agent}.md does not instruct to substitute the actual date (date +%F)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent", _AGENTS)
|
||||
def test_orch092_model_used_is_placeholder_not_literal(agent):
|
||||
"""TC-02 (AC-2): copyable example uses a model placeholder, not the literal model.
|
||||
|
||||
`model_used: claude-opus-4-8` is allowed as a reference in the field table
|
||||
(outside the fenced block) but must NOT appear in a copyable example.
|
||||
"""
|
||||
text = _read(agent)
|
||||
assert "model_used: <resolve ORCH-41>" in text, (
|
||||
f"{agent}.md does not use the model_used: <resolve ORCH-41> placeholder"
|
||||
)
|
||||
for block in _fenced_blocks(text):
|
||||
assert "model_used: claude-opus-4-8" not in block, (
|
||||
f"{agent}.md still hardcodes model_used: claude-opus-4-8 in a copyable block"
|
||||
)
|
||||
|
||||
|
||||
def test_orch092_gate_names_match_qg_registry():
|
||||
"""TC-03 (AC-3): every check_* named in the 6 prompts is a real QG_CHECKS key.
|
||||
|
||||
The only test in this module that imports `src/` (integration). Guards against
|
||||
a prompt naming a non-existent gate; confirms check_tests_passed is valid.
|
||||
"""
|
||||
from src.qg.checks import QG_CHECKS
|
||||
|
||||
pattern = re.compile(r"check_[a-z_]+")
|
||||
for agent in _AGENTS:
|
||||
for name in sorted(set(pattern.findall(_read(agent)))):
|
||||
assert name in QG_CHECKS, (
|
||||
f"{agent}.md references gate {name!r} which is absent from QG_CHECKS"
|
||||
)
|
||||
assert "check_tests_passed" in QG_CHECKS, "check_tests_passed must remain a real gate"
|
||||
|
||||
|
||||
def test_orch092_developer_pr_oversize_is_escalation_not_split():
|
||||
"""TC-04 (AC-4): the 'split into smaller PRs' instruction became an escalation."""
|
||||
text = _read("developer")
|
||||
assert "разбивай на меньшие PR" not in text, (
|
||||
"developer.md still carries the unrealisable 'split into smaller PRs' instruction"
|
||||
)
|
||||
assert "на уровне задач" in text and "декомпозиц" in text, (
|
||||
"developer.md does not reframe an oversize PR as task-level decomposition"
|
||||
)
|
||||
assert "свой PR" in text, "developer.md lost the 'свой PR' marker"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent", ("developer", "reviewer", "tester"))
|
||||
def test_orch092_escalation_section_present_after_success(agent):
|
||||
"""TC-05 (AC-5): dev/reviewer/tester carry <escalation> after </success_criteria>."""
|
||||
text = _read(agent)
|
||||
# The real section tags sit on their own line (an inline `<escalation>` mention
|
||||
# in <constraints> uses backticks and must not be mistaken for the section).
|
||||
open_m = re.search(r"(?m)^<escalation>\s*$", text)
|
||||
close_m = re.search(r"(?m)^</escalation>\s*$", text)
|
||||
assert open_m and close_m, f"{agent}.md is missing the <escalation> section"
|
||||
success_m = re.search(r"(?m)^</success_criteria>\s*$", text)
|
||||
assert success_m and open_m.start() > success_m.start(), (
|
||||
f"{agent}.md places <escalation> before </success_criteria> (breaks section order)"
|
||||
)
|
||||
|
||||
|
||||
def test_orch092_escalation_routes_are_role_specific():
|
||||
"""TC-05 (AC-5): escalation routes match each role."""
|
||||
assert "back-to:analysis" in _read("developer"), "developer lacks back-to:analysis route"
|
||||
assert "back-to:dev" in _read("tester"), "tester lacks back-to:dev route"
|
||||
assert "REQUEST_CHANGES" in _read("reviewer"), "reviewer lacks REQUEST_CHANGES route"
|
||||
|
||||
|
||||
def test_orch092_tester_enriched():
|
||||
"""TC-06 (AC-7): tester gains worktree path, serial_gate smoke and TRZ coverage."""
|
||||
text = _read("tester")
|
||||
assert "worktree" in text, "tester.md does not mention the task-branch worktree path"
|
||||
assert "serial_gate" in text, "tester.md /queue smoke omits the serial_gate block check"
|
||||
assert "04-test-plan.yaml" in text, "tester.md does not require coverage of every TRZ TC"
|
||||
for marker in _ANTI_REGRESS["tester"]:
|
||||
assert marker in text, f"tester.md lost anti-regress marker {marker!r}"
|
||||
|
||||
|
||||
def test_orch092_deployer_prominent_ban_frame():
|
||||
"""TC-07 (AC-6): deployer carries a prominent prod-8500 ban frame inside <context>."""
|
||||
text = _read("deployer")
|
||||
context = text[text.index("<context>"):text.index("</context>")]
|
||||
assert "8500" in context, "deployer.md <context> frame does not name the prod 8500"
|
||||
assert "NEVER restart the prod" in context, (
|
||||
"deployer.md does not raise the 'NEVER restart prod 8500' ban into the context frame"
|
||||
)
|
||||
for marker in _ANTI_REGRESS["deployer"]:
|
||||
assert marker in text, f"deployer.md lost anti-regress marker {marker!r}"
|
||||
|
||||
|
||||
def test_orch092_reviewer_dead_line_removed():
|
||||
"""TC-08 (AC-8): the dead 'same Developer instance' line is gone; live markers stay."""
|
||||
text = _read("reviewer")
|
||||
assert "того же экземпляра" not in text, (
|
||||
"reviewer.md still carries the dead 'same Developer instance' instruction"
|
||||
)
|
||||
for marker in (
|
||||
"REQUEST_CHANGES",
|
||||
"НЕ обновлена",
|
||||
"TRACEABILITY.md",
|
||||
"Известные ограничения",
|
||||
"ORCH-079",
|
||||
):
|
||||
assert marker in text, f"reviewer.md lost live invariant marker {marker!r}"
|
||||
|
||||
Reference in New Issue
Block a user