Files changed: - .gitea/workflows/ci.yml - .gitignore - .vibe/hooks.toml - AGENTS.md - CHANGES.md - EVALS.md - INSTALL-MCP.md - INSTALL.md - README.md - VERSION - docs/dev/development.md - docs/explanation/README.md - docs/explanation/architecture.md - docs/explanation/language-boundaries.md - docs/explanation/telemetry-and-evaluation.md - docs/how-to/README.md - docs/how-to/browse-with-obsidian.md - docs/how-to/capture-repository-docs.md - docs/how-to/export-guidelines.md - docs/how-to/ingest-a-source.md - docs/how-to/maintain-the-wiki.md - docs/how-to/query-the-wiki.md - docs/how-to/run-the-weekly-review.md - docs/how-to/set-up-the-mcp-server.md - docs/how-to/trace-and-score-sessions.md - docs/how-to/troubleshoot.md - docs/how-to/update-an-instance.md - docs/reference/README.md - docs/reference/commands/distribution-and-versioning.md - docs/reference/commands/instance-health.md - docs/reference/commands/raw-material-and-uploads.md - docs/reference/commands/telemetry.md - docs/reference/commands/types-instructions-and-docs.md - docs/reference/configuration.md - instructions/dev/dev-setup.md - instructions/dev/doc-pull-through.md - instructions/dev/stack-build/SKILL.md - instructions/dev/testing-conventions.md - instructions/dev/version-parts.md - instructions/setup-instance.md - instructions/upgrade-instance.md - reports/CONTRACT.md - tools/.coveragerc - tools/CONTRACT.md - tools/README.md - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/eval_cmd.py - tools/chemenu/commands/git_publish.py - tools/chemenu/commands/upload_cmd.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/evals/__init__.py - tools/chemenu/evals/scorecard.py - tools/chemenu/evals/trajectory.py - tools/chemenu/telemetry/policy.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_git_publish.py - tools/chemenu/tests/test_instructions_shell.py - tools/chemenu/tests/test_pipeline_l0.py - tools/chemenu/toc.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
136 lines
5.6 KiB
Python
136 lines
5.6 KiB
Python
"""The stack's shipped instructions read the same in bash, Git Bash and PowerShell 7.
|
|
|
|
`instructions/CONTRACT.md` § Writing an instruction states the rule; this is the check behind it.
|
|
An instruction is run by whichever harness the operator uses - Claude Code under Git Bash on
|
|
Windows, Copilot under PowerShell 7 - so a command block that only one shell reads sends the
|
|
other agent off to translate it, which is where the Weg-D install went wrong (Gitea #140, #153).
|
|
|
|
Scope: every instruction `dist export` ships, minus `instructions/migrations/`, which belong to
|
|
the release they shipped with. A test rather than an `instructions verify` rule because the rule
|
|
binds what the stack ships; an instance's own instructions are its own decision.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO = Path(__file__).resolve().parents[3]
|
|
INSTRUCTIONS = REPO / "instructions"
|
|
|
|
# The info strings of a block an agent runs; a json/yaml/markdown block is content, not a command.
|
|
COMMAND_FENCES = {"", "bash", "sh", "shell", "console", "powershell", "pwsh"}
|
|
|
|
FORBIDDEN = (
|
|
("heredoc", re.compile(r"<<")),
|
|
("export", re.compile(r"(^|[;&|]\s*)export\s")),
|
|
("command substitution", re.compile(r"\$\(")),
|
|
("shell variable", re.compile(r"\$\{|\$[A-Z_][A-Z0-9_]*\b")),
|
|
("inline environment", re.compile(r"^[A-Z_][A-Z0-9_]*=\S*\s+\S")),
|
|
("&&", re.compile(r"&&")),
|
|
("for loop", re.compile(r"^\s*for\s.*;\s*do\b")),
|
|
("cp", re.compile(r"(^|[;&|]\s*)cp\s")),
|
|
("cat >", re.compile(r"\bcat\s+>")),
|
|
("sha256sum", re.compile(r"\bsha256sum\b")),
|
|
("curl", re.compile(r"\bcurl\b")),
|
|
("tar", re.compile(r"(^|[;&|]\s*)tar\s")),
|
|
# Which name starts a real Python differs per shell and platform: on Windows `python3` - in
|
|
# PowerShell also `python` - can be the Microsoft Store's alias. A command goes through a
|
|
# launcher that finds the interpreter itself (`tools/wikitool`, `tools/bugreport`).
|
|
("bare interpreter", re.compile(r"(^|[;&|]\s*)(python3?|py)(\.exe)?\s")),
|
|
)
|
|
|
|
# The two sanctioned exceptions, each one line per shell (instructions/CONTRACT.md): the session
|
|
# id (D26) and the preflight download before an instance exists (E2). Keyed by exact line, so a
|
|
# second use of the same construct elsewhere still fails.
|
|
ALLOWED = {
|
|
("session-setup.md", 'export WIKITOOL_SESSION_ID="wiki-20261001-1430"'),
|
|
("session-setup.md", "$env:WIKITOOL_SESSION_ID = 'wiki-20261001-1430'"),
|
|
("setup-instance.md", "curl -fLO <browser_download_url of preflight.sh>"),
|
|
}
|
|
|
|
|
|
def shipped_instructions() -> list[Path]:
|
|
return sorted(
|
|
path
|
|
for path in INSTRUCTIONS.rglob("*.md")
|
|
if not {"dev", "migrations"} & set(path.relative_to(INSTRUCTIONS).parts[:-1])
|
|
)
|
|
|
|
|
|
def command_lines(path: Path):
|
|
"""`(line number, line)` for every line inside a command fence."""
|
|
fence = None
|
|
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
stripped = line.strip()
|
|
if stripped.startswith("```"):
|
|
fence = None if fence is not None else stripped[3:].strip()
|
|
continue
|
|
if fence is not None and fence in COMMAND_FENCES and stripped:
|
|
yield number, stripped
|
|
|
|
|
|
def test_the_scope_is_not_empty():
|
|
names = {path.name for path in shipped_instructions()}
|
|
assert {"setup-instance.md", "session-setup.md", "upgrade-instance.md"} <= names
|
|
assert not any("migrations" in path.parts for path in shipped_instructions())
|
|
|
|
|
|
@pytest.mark.parametrize("path", shipped_instructions(), ids=lambda p: str(p.relative_to(INSTRUCTIONS)))
|
|
def test_a_shipped_instruction_uses_no_shell_specific_syntax(path):
|
|
found = [
|
|
f"{path.relative_to(REPO)}:{number}: {name}: {line}"
|
|
for number, line in command_lines(path)
|
|
if (path.name, line) not in ALLOWED
|
|
for name, pattern in FORBIDDEN
|
|
if pattern.search(line)
|
|
]
|
|
assert not found, "\n".join(found)
|
|
|
|
|
|
def test_every_exception_is_still_in_use():
|
|
"""An allowance nobody uses any more is a gap waiting for the next construct."""
|
|
used = {
|
|
(path.name, line)
|
|
for path in shipped_instructions()
|
|
for _, line in command_lines(path)
|
|
}
|
|
assert ALLOWED <= used, ALLOWED - used
|
|
|
|
|
|
SHIPPED_ROOT_DOCS = ("AGENTS.md", "README.md", "INSTALL.md")
|
|
|
|
|
|
def shipped_docs() -> list[Path]:
|
|
"""The root documents an operator reads, plus every page `dist export` ships under `docs/` -
|
|
the how-to guides among them name the preflight call an operator types."""
|
|
docs = REPO / "docs"
|
|
pages = sorted(
|
|
path for path in docs.rglob("*.md") if path.relative_to(docs).parts[0] != "dev"
|
|
)
|
|
return [REPO / name for name in SHIPPED_ROOT_DOCS] + pages
|
|
|
|
# A line that starts the PowerShell preflight - bare (`.\tools\preflight.ps1`, `& preflight.ps1`)
|
|
# or through pwsh - as opposed to one that merely names the file (a comment, a download).
|
|
PREFLIGHT_CALL = re.compile(r"^(?:&\s*)?(?:\.[\\/])?(?:tools[\\/])?preflight\.ps1\b|^pwsh\b.*preflight\.ps1")
|
|
BYPASS = "pwsh -NoProfile -ExecutionPolicy Bypass -File "
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
shipped_docs() + shipped_instructions(),
|
|
ids=lambda p: str(p.relative_to(REPO)),
|
|
)
|
|
def test_every_powershell_preflight_call_carries_the_bypass(path):
|
|
"""Copilot started `.\\tools\\preflight.ps1` bare in the #151 hand check; in a checkout that
|
|
carries a Mark of the Web that call fails with PowerShell's own refusal and no guidance."""
|
|
if not path.is_file():
|
|
pytest.skip(f"{path.name} is not in this tree")
|
|
bare = [
|
|
f"{number}: {line}"
|
|
for number, line in command_lines(path)
|
|
if PREFLIGHT_CALL.match(line) and not line.startswith(BYPASS)
|
|
]
|
|
assert not bare, "\n".join(bare)
|