docs: add Shell Spawn Hard-Stop Rule for agent workflows (#258)
Documents fail-closed behavior when the shell executor fails to spawn (exit_code: -1, empty stdout/stderr): probe once, mark shell unavailable, hard-stop after two consecutive spawn failures, and emit a recovery report (restart session, kill hung background terminals, prefer MCP-native paths) instead of retry-spiralling. - skills/llm-project-workflow/SKILL.md: portable rule section - docs/llm-workflow-runbooks.md: Gitea runbook section - gitea_mcp_server.py: shell_spawn_hard_stop operator guide rule - tests/test_shell_spawn_hard_stop_docs.py: doc-contract tests (5) - tests/test_operator_guide.py: require the new guide key Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Doc-contract checks for the shell-spawn hard-stop rule (#258).
|
||||
|
||||
When the shell executor fails to spawn (exit_code: -1 with empty
|
||||
stdout/stderr), agents must probe once, mark shell unavailable, hard-stop
|
||||
after two consecutive spawn failures, and emit a recovery report instead of
|
||||
retry-spiralling. These tests pin that guidance in the runbooks, the portable
|
||||
skill doc, and the operator guide so it cannot silently regress.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
RUNBOOK = REPO_ROOT / "docs" / "llm-workflow-runbooks.md"
|
||||
SKILL = REPO_ROOT / "skills" / "llm-project-workflow" / "SKILL.md"
|
||||
|
||||
|
||||
def _normalize(text: str) -> str:
|
||||
"""Collapse whitespace so phrase checks survive markdown line wrapping."""
|
||||
return " ".join(text.split())
|
||||
|
||||
|
||||
def _runbook_text() -> str:
|
||||
return _normalize(RUNBOOK.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _skill_text() -> str:
|
||||
return _normalize(SKILL.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_runbook_has_shell_spawn_hard_stop_section():
|
||||
text = _runbook_text()
|
||||
assert "## Shell Spawn Hard-Stop Rule" in text
|
||||
for phrase in (
|
||||
"exit_code: -1",
|
||||
"empty stdout/stderr",
|
||||
"trivial probe",
|
||||
"two consecutive spawn failures",
|
||||
"mark shell unavailable",
|
||||
"recovery report",
|
||||
):
|
||||
assert phrase in text, f"runbook missing phrase: {phrase!r}"
|
||||
|
||||
|
||||
def test_runbook_recovery_report_names_required_steps():
|
||||
text = _runbook_text()
|
||||
for phrase in (
|
||||
"restart the session",
|
||||
"kill hung background terminals",
|
||||
"MCP-native",
|
||||
):
|
||||
assert phrase in text, f"runbook recovery guidance missing: {phrase!r}"
|
||||
|
||||
|
||||
def test_runbook_forbids_retry_spirals():
|
||||
text = _runbook_text()
|
||||
assert "never retry the same failing spawn" in text
|
||||
|
||||
|
||||
def test_skill_doc_declares_shell_spawn_hard_stop_rule():
|
||||
text = _skill_text()
|
||||
assert "## Shell Spawn Hard-Stop Rule" in text
|
||||
for phrase in (
|
||||
"exit_code: -1",
|
||||
"two consecutive spawn failures",
|
||||
"recovery report",
|
||||
):
|
||||
assert phrase in text, f"SKILL.md missing phrase: {phrase!r}"
|
||||
|
||||
|
||||
def test_operator_guide_rules_include_shell_spawn_hard_stop():
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
import gitea_mcp_server
|
||||
|
||||
rule = gitea_mcp_server._GUIDE_RULES["shell_spawn_hard_stop"]
|
||||
for phrase in (
|
||||
"exit_code: -1",
|
||||
"two consecutive",
|
||||
"recovery report",
|
||||
):
|
||||
assert phrase in rule, f"operator guide rule missing: {phrase!r}"
|
||||
Reference in New Issue
Block a user