From 950176d162055ca6e7e35c0cb11231dded549dab Mon Sep 17 00:00:00 2001 From: POWERFULMOVES <142271328+POWERFULMOVES@users.noreply.github.com> Date: Sat, 8 Aug 2026 14:14:37 -0400 Subject: [PATCH] feat(ratchet): verify first contact and the guard's own routing table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The anchor ratchet checked docs, skills and runbooks — but not the two surfaces that decide whether an agent entering this repo succeeds or breaks something. FIRST CONTACT. .claude/CLAUDE.md, BOOTSTRAP.md, PATTERNS.md, CATALOG.md, the root CLAUDE.md and AGENTS.md are ALWAYS LOADED. An agent reads them before it reads anything else, so a dead reference there is the most expensive kind: the agent is misdirected at step one, improvises, breaks something, and the model gets blamed for the field's error. They were not scanned. Now they are, along with .claude/context, .claude/commands and .claude/agents. Four real ghosts surfaced immediately, all in always-loaded files: .claude/CLAUDE.md `make -C pmoves worktree-sitrep-strict` — described in-file as "Authoritative worktree sitrep. Prefer this to per-worktree spot checks." No such target. .claude/PATTERNS.md `worktree-sitrep`, `worktree-sitrep-strict` .claude/BOOTSTRAP.md `health-quick` .claude/learnings/ is excluded alongside archive/: a learnings file records what was true in a past session. It is a log, not a promise. THE GUARD'S OWN ROUTING TABLE. .claude/hooks/damage-control/patterns.yaml is the best thing in this repo — 121 entries, each carrying why-this-is-wrong, the correct path, the valid parameter set, and how to verify afterwards, with ask:true rather than a hard no. It routes a blocked agent to safety. But nothing checked where it routes TO. New GHOST_ROAD class points the same GHOST_TARGET question one layer inward, at the thing that answers that question for everyone else. Two of its 13 offered targets do not exist: mini-cli stop/down/reset -> "use make -C pmoves targets" GHOST schema changes -> "apply with make -C pmoves db-migrate" GHOST A well-behaved agent that follows the offered road lands in a wall at the moment it is least able to recover. TWO DISCRIMINATORS, so the gate does not punish the docs doing this right. .claude/PATTERNS.md:38 and AGENTS.md:28 both carry a "| Raw command (blocked) | Known Road |" table mapping `docker volume rm` -> `make -C pmoves volume-reset SERVICE=...`. They name the dangerous operation in order to route away from it. A line that names its Known Road on the same line, or that frames the command as blocked/dangerous/anti-pattern, is documentation rather than instruction. Semantic, deliberately not a file whitelist — a whitelist would also hide a real instruction hiding in the same file. Also fixed: the first cut tested the inline SPAN rather than the LINE it sits on, so the road sitting next to the command in a table row was invisible and both correct docs were flagged. And `up-` placeholders render as `up-` after extraction; the trailing hyphen is the tell. One bug worth recording because it has now bitten twice in one day: writing a literal backslash-b into the discriminator through a heredoc escape-interprets it to 0x08, silently replacing the word boundary with a backspace. The regex then matched nothing and the exemption looked broken rather than absent. Same class as the corruption that turned `feat/` into `eat/` in the claim register (#2476). Regexes are now built rather than typed, and a test asserts no control characters survive in any of them. Baseline 409 -> 418. 32 tests. The four orientation ghosts and two dead roads are baselined, not approved. They are the first things that should be fixed, and now they cannot silently multiply. Co-Authored-By: Claude Opus 5 --- .../validate-command-anchors-ratchet.yml | 12 ++ .../configs/command_anchors/_known_gaps.yaml | 33 +++-- .../tests/test_validate_command_anchors.py | 54 ++++++++ pmoves/tools/validate_command_anchors.py | 117 +++++++++++++++--- 4 files changed, 184 insertions(+), 32 deletions(-) diff --git a/.github/workflows/validate-command-anchors-ratchet.yml b/.github/workflows/validate-command-anchors-ratchet.yml index eab667b56e..6644c72efe 100644 --- a/.github/workflows/validate-command-anchors-ratchet.yml +++ b/.github/workflows/validate-command-anchors-ratchet.yml @@ -31,6 +31,18 @@ on: paths: - "pmoves/docs/**/*.md" - ".claude/skills/**/*.md" + # The always-loaded orientation layer — first contact for any agent + # entering this repo, and therefore the highest-cost place for a dead + # reference. .claude/CLAUDE.md currently names a target that does not exist. + - ".claude/*.md" + - "CLAUDE.md" + - "AGENTS.md" + - ".claude/context/**/*.md" + - ".claude/commands/**/*.md" + - ".claude/agents/**/*.md" + # The damage-control routing table: every "correct path" it offers is a + # promise made to an agent at its least recoverable moment. + - ".claude/hooks/damage-control/patterns.yaml" - "deploy/runbooks/**/*.md" - "pmoves/Makefile" - "pmoves/mk/*.mk" diff --git a/pmoves/configs/command_anchors/_known_gaps.yaml b/pmoves/configs/command_anchors/_known_gaps.yaml index 2b7fdce250..421d5e36f3 100644 --- a/pmoves/configs/command_anchors/_known_gaps.yaml +++ b/pmoves/configs/command_anchors/_known_gaps.yaml @@ -7,11 +7,24 @@ known_gaps: - "GHOST_PATH|pmoves/docs/AGENTS/AGNOTE4482PHI.t1.md|pmoves/services/flute-gateway/pipecat/pipelines/voice_agent.py" - "GHOST_PATH|pmoves/docs/reviews/2026-03-01/tokenism-multi-review.md|.claude/skills/pmoves-integration/tools/nats-monitor.ts" - "GHOST_PATH|pmoves/docs/reviews/2026-03-01/tokenism-multi-review.md|.github/workflows/ci.yml" + - "GHOST_ROAD|.claude/hooks/damage-control/patterns.yaml|guard offers `make -C pmoves db-migrate` as the correct path; no such target" + - "GHOST_ROAD|.claude/hooks/damage-control/patterns.yaml|guard offers `make -C pmoves targets` as the correct path; no such target" + - "GHOST_TARGET|.claude/BOOTSTRAP.md|health-quick" + - "GHOST_TARGET|.claude/CLAUDE.md|worktree-sitrep-strict" + - "GHOST_TARGET|.claude/PATTERNS.md|worktree-sitrep" + - "GHOST_TARGET|.claude/PATTERNS.md|worktree-sitrep-strict" + - "GHOST_TARGET|.claude/commands/test/smoke.md|test-smoke" + - "GHOST_TARGET|.claude/commands/test/smoke.md|test-smoke-critical" + - "GHOST_TARGET|.claude/commands/test/smoke.md|test-smoke-health" + - "GHOST_TARGET|.claude/commands/test/smoke.md|test-smoke-quick" + - "GHOST_TARGET|.claude/context/testing-strategy.md|discord-smoke" + - "GHOST_TARGET|.claude/context/testing-strategy.md|smoke-creator-pipeline" + - "GHOST_TARGET|.claude/context/testing-strategy.md|smoke-prerequisites" + - "GHOST_TARGET|.claude/context/testing-strategy.md|smoke-tensorzero-observability" + - "GHOST_TARGET|.claude/context/tier-architecture.md|up-nats" - "GHOST_TARGET|.claude/skills/agentgym-run/SKILL.md|agentgym-results" - "GHOST_TARGET|.claude/skills/agentgym-run/SKILL.md|agentgym-run" - "GHOST_TARGET|.claude/skills/agentgym-run/SKILL.md|agentgym-run-lightweight" - - "GHOST_TARGET|.claude/skills/google-workspace/SKILL.md|secrets-" - - "GHOST_TARGET|pmoves/docs/AGENTS/AGNOTE4482DnB.PHI.Orchestra.md|up-" - "GHOST_TARGET|pmoves/docs/AGENTS/AGNOTE4482PHI.t1.md|agentgym-up" - "GHOST_TARGET|pmoves/docs/AGENTS/AGNOTE4482PHI.t1.md|submodules" - "GHOST_TARGET|pmoves/docs/AGENTS/AGNOTE4482PHI.t1.md|tac-check" @@ -37,7 +50,6 @@ known_gaps: - "GHOST_TARGET|pmoves/docs/NEXT_STEPS.md|channel-monitor-discord-drop-smoke" - "GHOST_TARGET|pmoves/docs/NEXT_STEPS.md|channel-monitor-discord-gate-smoke" - "GHOST_TARGET|pmoves/docs/NEXT_STEPS.md|demo-content-published" - - "GHOST_TARGET|pmoves/docs/NEXT_STEPS.md|integrations-" - "GHOST_TARGET|pmoves/docs/NEXT_STEPS.md|jellyfin-verify-single" - "GHOST_TARGET|pmoves/docs/NEXT_STEPS.md|release" - "GHOST_TARGET|pmoves/docs/NEXT_STEPS.md|seed-repo-docs" @@ -175,7 +187,6 @@ known_gaps: - "GHOST_TARGET|pmoves/docs/PMOVESCHIT/CHIT_USER_GUIDE.md|secrets-chit-encode" - "GHOST_TARGET|pmoves/docs/PMOVESCHIT/PMOVES-CONCHexecution_guide.md|smoke-geometry-db" - "GHOST_TARGET|pmoves/docs/PMOVESCHIT/PMOVES-CONCHexecution_guide.md|smoke-hirag-v1" - - "GHOST_TARGET|pmoves/docs/SECRETS_PIPELINE_REFERENCE.md|up-" - "GHOST_TARGET|pmoves/docs/TAC/TAC_E2B_SANDBOX.md|build-e2b" - "GHOST_TARGET|pmoves/docs/TAC/TAC_E2B_SANDBOX.md|e2b-down" - "GHOST_TARGET|pmoves/docs/TAC/TAC_E2B_SANDBOX.md|e2b-health" @@ -214,8 +225,6 @@ known_gaps: - "GHOST_TARGET|pmoves/docs/handoffs/DARKXSIDE_E2B_DESKTOP_FANOUT_2026-08-06.md|local-build-base-template" - "GHOST_TARGET|pmoves/docs/handoffs/DARKXSIDE_E2B_DESKTOP_FANOUT_2026-08-06.md|local-infra" - "GHOST_TARGET|pmoves/docs/handoffs/DARKXSIDE_E2B_DESKTOP_FANOUT_2026-08-06.md|up-e2b-mcp" - - "GHOST_TARGET|pmoves/docs/handoffs/SECRET_ROTATION_RUNBOOK.md|up-" - - "GHOST_TARGET|pmoves/docs/handoffs/compose-overlay-defensive-networks-2026-05-18.md|overlay-up-" - "GHOST_TARGET|pmoves/docs/handoffs/creator-comfyui-selfhost-config-2026-06-24.md|comfyui-up" - "GHOST_TARGET|pmoves/docs/handoffs/juicefs-cross-node-storage-blocker-2026-08-04.md|juicefs-mount-pg" - "GHOST_TARGET|pmoves/docs/integrations/EXTERNAL_IMAGES_BUILD.md|docker-login-ghcr" @@ -244,7 +253,6 @@ known_gaps: - "GHOST_TARGET|pmoves/docs/operations/COMPLETE_BRING_UP_RUNBOOK.md|restore-data" - "GHOST_TARGET|pmoves/docs/operations/COMPLETE_BRING_UP_RUNBOOK.md|supabase-generate-keys" - "GHOST_TARGET|pmoves/docs/operations/COMPLETE_BRING_UP_RUNBOOK.md|test-smoke" - - "GHOST_TARGET|pmoves/docs/operations/COMPOSE_LAYERING_RUNBOOK.md|overlay-up-" - "GHOST_TARGET|pmoves/docs/operations/ENVIRONMENT_POLICY.md|jellyfin-hosts-generate" - "GHOST_TARGET|pmoves/docs/operations/ENVIRONMENT_POLICY.md|up-jellyfin-single" - "GHOST_TARGET|pmoves/docs/operations/FORDHAM_ROOM_LAUNCH_PLAN.md|fleet-deploy" @@ -252,7 +260,6 @@ known_gaps: - "GHOST_TARGET|pmoves/docs/operations/FORDHAM_ROOM_LAUNCH_PLAN.md|smoke-test-room" - "GHOST_TARGET|pmoves/docs/operations/FORDHAM_ROOM_LAUNCH_PLAN.md|validate-room-manifest" - "GHOST_TARGET|pmoves/docs/operations/JUICEFS_PHASE3_CUTOVER.md|juicefs-mirror" - - "GHOST_TARGET|pmoves/docs/operations/JUICEFS_PHASE3_CUTOVER.md|up-" - "GHOST_TARGET|pmoves/docs/operations/LOCAL_DEV.md|bootstrap" - "GHOST_TARGET|pmoves/docs/operations/LOCAL_DEV.md|demo-content-published" - "GHOST_TARGET|pmoves/docs/operations/LOCAL_DEV.md|discord-ping" @@ -342,7 +349,6 @@ known_gaps: - "GHOST_TARGET|pmoves/docs/operations/SMOKETESTS.md|yt-playlist-smoke" - "GHOST_TARGET|pmoves/docs/operations/SUBMODULE_BUILD_AND_MOUNT_GAP.md|submodules" - "GHOST_TARGET|pmoves/docs/operations/UPSTREAM_UPDATE_RUNBOOK.md|up-agent-zero" - - "GHOST_TARGET|pmoves/docs/operations/rto-rpo-targets.md|up-" - "GHOST_TARGET|pmoves/docs/pmoves-model-management-starter/README.md|health-agent-zero" - "GHOST_TARGET|pmoves/docs/pmoves-model-management-starter/README.md|smoke-archon" - "GHOST_TARGET|pmoves/docs/pmoves-model-management-starter/README.md|yt-emit-smoke" @@ -385,7 +391,8 @@ known_gaps: - "GHOST_TARGET|pmoves/docs/specs/p7-service-spec-2026-07-20.md|rooms-reload" - "GHOST_TARGET|pmoves/docs/specs/tts-pterm-gepeto-review-readmes-2026-08-01.md|tac-check" - "GHOST_TARGET|pmoves/docs/specs/tts-pterm-gepeto-review-readmes-2026-08-01.md|tts-test-all" - - "GHOST_TARGET|pmoves/docs/voice/VOICE_FABRIC_DEPLOYMENT.md|kokoro-" + - "UNKNOWN_HOST|.claude/PATTERNS.md|pmoves-kvm4-2" + - "UNKNOWN_HOST|.claude/commands/fleet/fix-relay.md|pmoves-kvm2" - "UNKNOWN_HOST|.claude/skills/ci-expedition/SKILL.md|pmoves-kvm4-1" - "UNKNOWN_HOST|deploy/runbooks/amd-r9700-install-day.md|pmoves-9850x3d-r9700" - "UNKNOWN_HOST|deploy/runbooks/fresh-install-fleet.md|pmoves-9850x3d-r9700" @@ -401,12 +408,14 @@ known_gaps: - "UNKNOWN_HOST|pmoves/docs/operations/TAILSCALE_EXIT_NODE_RUNBOOK.md|pmoves-kvm4-1" - "UNKNOWN_HOST|pmoves/docs/pilots/fordham-hill/06-pilot-observation.md|pmoves-kvm4-1" - "UNKNOWN_HOST|pmoves/docs/research/comprehensive-analysis/05_network_architecture.md|raw IP 192.168.8.1" - - "UNRUNNABLE_DOC|.claude/skills/ci-expedition/SKILL.md|'docker volume rm' in: docker volume rm" + - "UNRUNNABLE_DOC|.claude/PATTERNS.md|'rm -rf /' in: RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*" + - "UNRUNNABLE_DOC|.claude/README.md|'DROP DATABASE' in: DROP DATABASE" + - "UNRUNNABLE_DOC|.claude/README.md|'rm -rf /' in: rm -rf /" + - "UNRUNNABLE_DOC|.claude/commands/db/migrate.md|'DROP TABLE' in: DROP TABLE" - "UNRUNNABLE_DOC|pmoves/docs/NEO4J_INTEGRATION_GUIDE.md|'docker volume rm' in: docker volume rm pmoves_neo4jdata" - "UNRUNNABLE_DOC|pmoves/docs/NEO4J_SUBMODULE_INTEGRATION_COMPLETE.md|'docker volume rm' in: docker volume rm pmoves_neo4jdata" - "UNRUNNABLE_DOC|pmoves/docs/NEO4J_SUBMODULE_PROMOTION.md|'docker volume rm' in: docker volume rm pmoves_neo4jdata" - "UNRUNNABLE_DOC|pmoves/docs/handoffs/DOCKER_FLEET_PROD_AUDIT.md|'docker system prune -a' in: docker system prune -af --volumes" - - "UNRUNNABLE_DOC|pmoves/docs/handoffs/yt-oauth-legacy-stub-drop-2026-08-01.md|'DROP TABLE' in: DROP TABLE" - "UNRUNNABLE_DOC|pmoves/docs/handoffs/yt-oauth-legacy-stub-drop-2026-08-01.md|'DROP TABLE' in: DROP TABLE IF EXISTS public.yt_oauth_cookies;" - "UNRUNNABLE_DOC|pmoves/docs/operations/BRING_UP_WSL2.md|'docker system prune -a' in: docker system prune -a --volumes # Clean Docker (careful!)" - "UNRUNNABLE_DOC|pmoves/docs/operations/EDGE_TRAEFIK_SSO_RUNBOOK.md|'docker volume rm' in: docker volume rm _traefik-acme" diff --git a/pmoves/tools/tests/test_validate_command_anchors.py b/pmoves/tools/tests/test_validate_command_anchors.py index 95bbf14bf0..5042c680d1 100644 --- a/pmoves/tools/tests/test_validate_command_anchors.py +++ b/pmoves/tools/tests/test_validate_command_anchors.py @@ -168,3 +168,57 @@ def test_inline_span_regex_finds_prose_commands(): def test_inline_span_does_not_span_newlines(): assert vca.INLINE_SPAN_RE.findall("`a\nb`") == [] + + +# ── orientation coverage + guard self-check (#2494) ───────────────── + + +def test_always_loaded_orientation_files_are_scanned(): + """First contact must be verified. .claude/CLAUDE.md tells every agent that + `worktree-sitrep-strict` is authoritative; no such target exists.""" + docs = {d.as_posix() for d in vca.live_docs()} + for must in ("CLAUDE.md", "BOOTSTRAP.md", "PATTERNS.md", "AGENTS.md"): + assert any(d.endswith(must) for d in docs), f"{must} not scanned" + + +def test_learnings_are_excluded(): + """A learnings file records what was true in a past session. Log, not promise.""" + assert "learnings" in vca.DOC_EXCLUDE_PARTS + + +def test_guard_routing_table_is_checked(): + """The ratchet aimed one layer inward: where does a blocked agent get sent?""" + targets = vca.discover_targets() + findings = vca.scan_guard_roads(targets) + assert isinstance(findings, list) + for f in findings: + assert f["kind"] == "GHOST_ROAD" + assert f["scope"] == "guard" + + +def test_guard_road_placeholders_are_not_flagged(): + """`up-` is a placeholder; the trailing hyphen is the tell.""" + assert "up-" in vca.GUARD_ROAD_SKIP + + +def test_naming_a_road_on_the_line_exempts_it(): + """`.claude/PATTERNS.md` and AGENTS.md carry a blocked-command -> Known Road + table. Those are the cure; flagging them would punish the docs doing it right.""" + row = "| `docker volume " + "rm` | `make -C pmoves volume-reset SERVICE=...` | `/deploy:services` |" + assert vca.ROAD_IN_LINE_RE.search(row) + + +def test_describing_a_block_exempts_it(): + for line in ("# Blocks: dangerous ops, etc.", + "| Raw command (blocked) | Known Road |", + "- NEVER do this anywhere"): + assert vca.DESCRIBES_BLOCK_RE.search(line), line + + +def test_discriminators_carry_no_control_characters(): + """A literal backslash-b in a heredoc escape-interprets to 0x08 and silently + turns the word boundary into a backspace. That happened here once; the regex + then matched nothing and the exemption looked broken rather than absent.""" + for rx in (vca.DESCRIBES_BLOCK_RE, vca.ROAD_IN_LINE_RE, vca.GUARD_ROAD_RE): + assert chr(8) not in rx.pattern + assert chr(12) not in rx.pattern diff --git a/pmoves/tools/validate_command_anchors.py b/pmoves/tools/validate_command_anchors.py index 4b68a0b25f..8d8ba722a3 100644 --- a/pmoves/tools/validate_command_anchors.py +++ b/pmoves/tools/validate_command_anchors.py @@ -40,6 +40,9 @@ UNKNOWN_HOST a documented `ssh ` names a host absent from the fleet topology (this also catches raw IPs, which must never appear in committed docs) + GHOST_ROAD the damage-control guard offers a `make` target as the + "correct path" and no such target exists — a blocked agent is + routed into a wall at the moment it is least able to recover STALE_BASELINE a baselined key that no longer occurs — i.e. it was FIXED. Also a failure: leaving it in the file re-accepts the same defect if it returns, which is not "count only goes down". @@ -80,9 +83,36 @@ DOC_ROOTS = [ PMOVES / "docs", REPO_ROOT / ".claude" / "skills", + REPO_ROOT / ".claude" / "context", + REPO_ROOT / ".claude" / "commands", + REPO_ROOT / ".claude" / "agents", REPO_ROOT / "deploy" / "runbooks", ] -DOC_EXCLUDE_PARTS = {"archive", "_archive", "node_modules", "pmoves_all_in_one_v10"} +# The ALWAYS-LOADED orientation files. An agent entering this repo reads these +# before it reads anything else, so a dead reference here is the highest-cost +# kind there is: first contact is misdirection, the agent improvises, and the +# breakage gets blamed on the model. `.claude/CLAUDE.md` currently tells every +# agent that `make -C pmoves worktree-sitrep-strict` is "authoritative — prefer +# this"; no such target exists. +DOC_FILES = [ + REPO_ROOT / ".claude" / "CLAUDE.md", + REPO_ROOT / ".claude" / "BOOTSTRAP.md", + REPO_ROOT / ".claude" / "PATTERNS.md", + REPO_ROOT / ".claude" / "CATALOG.md", + REPO_ROOT / ".claude" / "PINOKIO_LAUNCHER_GUIDE.md", + REPO_ROOT / ".claude" / "README.md", + REPO_ROOT / "CLAUDE.md", + REPO_ROOT / "AGENTS.md", +] +# `.claude/learnings/` is deliberately excluded alongside archive/: a learnings +# file records what was true during a past session. It is a log, not a promise. +DOC_EXCLUDE_PARTS = {"archive", "_archive", "node_modules", "pmoves_all_in_one_v10", "learnings"} + +# The damage-control guard's routing table. Every `make -C pmoves ` it offers +# as a "correct path" is a promise made at the exact moment an agent is blocked +# and least able to recover — so a dead road here routes a well-behaved agent +# into a wall and then blames it for improvising. +GUARD_PATTERNS = REPO_ROOT / ".claude" / "hooks" / "damage-control" / "patterns.yaml" MAKEFILES = [PMOVES / "Makefile"] @@ -205,7 +235,7 @@ def blocked_patterns() -> List[str]: def live_docs() -> List[Path]: - out: List[Path] = [] + out: List[Path] = [p for p in DOC_FILES if p.is_file()] for root in DOC_ROOTS: if not root.is_dir(): continue @@ -246,7 +276,9 @@ def scan(targets: Dict[str, Path], scopes: Dict[str, str]) -> List[dict]: for block in FENCE_RE.findall(text): cited |= {m.group(1) for m in MAKE_FENCED_RE.finditer(block)} for t in sorted(cited): - if t in targets: + # `up-` / `overlay-up-` are placeholders, not targets. + # The trailing hyphen is the tell. + if t in targets or t.endswith("-"): continue findings.append({ "kind": "GHOST_TARGET", @@ -266,22 +298,30 @@ def scan(targets: Dict[str, Path], scopes: Dict[str, str]) -> List[dict]: # tool or the hook itself — both must name the patterns to work. if self_name in rel or "hooks/" in rel: continue - # Command-shaped lines (fenced blocks, $-prefixed, bare invocations) - # AND inline code spans in prose. A reader copies both. - candidates: List[str] = [] - for line in text.splitlines(): - stripped = line.strip() - if stripped.startswith(("$", "ssh ", "docker ", "make ", "sudo ")): - candidates.append(stripped) - candidates.extend(m.group(1) for m in INLINE_SPAN_RE.finditer(text)) - for cand in candidates: - if pat.lower() in cand.lower(): - findings.append({ - "kind": "UNRUNNABLE_DOC", - "doc": rel, - "detail": f"{pat!r} in: {cand[:80]}", - "scope": "docker", - }) + # Walk LINES, not spans. A span carries only the command; the + # discriminator lives on the line around it. Testing the span alone + # flagged `.claude/PATTERNS.md` and `AGENTS.md`, both of which carry + # a "| Raw command (blocked) | Known Road |" table — the two docs + # doing this exactly right. + for raw in text.splitlines(): + line = raw.strip() + if not line: + continue + # Documentation ABOUT the guard, not instruction THROUGH it: + # either it names the Known Road on the same line, or it frames + # the command as blocked/dangerous. + if ROAD_IN_LINE_RE.search(line) or DESCRIBES_BLOCK_RE.search(line): + continue + forms = [line] if line.startswith(("$", "ssh ", "docker ", "make ", "sudo ")) else [] + forms.extend(m.group(1) for m in INLINE_SPAN_RE.finditer(line)) + for cand in forms: + if pat.lower() in cand.lower(): + findings.append({ + "kind": "UNRUNNABLE_DOC", + "doc": rel, + "detail": f"{pat!r} in: {cand[:80]}", + "scope": "docker", + }) for m in SSH_CITE_RE.finditer(text): host = m.group(1) @@ -303,6 +343,43 @@ def scan(targets: Dict[str, Path], scopes: Dict[str, str]) -> List[dict]: return findings +# ── The guard's own routing table ─────────────────────────────────── + + +GUARD_ROAD_RE = re.compile(r"make\s+-C\s+pmoves\s+([a-z][a-z0-9-]{2,})") +# Placeholders, not targets: the guard writes `up-` to mean "the up- +# target for whatever service you meant". Flagging those would be noise. +GUARD_ROAD_SKIP = {"up-", "up-service"} +# Used to tell "here is the correct path" apart from "run this". +ROAD_IN_LINE_RE = re.compile(r"make\s+-C\s+pmoves\s+[a-z][a-z0-9-]{2,}|/deploy:|Known Road") +# "Blocks: X, Y" / "Raw command (blocked)" / an anti-pattern bullet is a +# description of the wall, not an instruction to walk into it. +DESCRIBES_BLOCK_RE = re.compile(r"\bBlocks?:|\bblocked\b|\bDangerous Operation\b|\bNEVER\b|\banti-pattern\b|\bmass deletion\b|Raw command|❌", re.I) + + +def scan_guard_roads(targets: Dict[str, Path]) -> List[dict]: + """A blocked agent is routed by patterns.yaml. Check where it gets sent. + + This is the ratchet aimed one layer inward: the same GHOST_TARGET question, + asked of the thing that answers the question for everyone else. + """ + if not GUARD_PATTERNS.is_file(): + return [] + rel = GUARD_PATTERNS.relative_to(REPO_ROOT).as_posix() + text = GUARD_PATTERNS.read_text(encoding="utf-8", errors="replace") + findings: List[dict] = [] + for t in sorted({m.group(1) for m in GUARD_ROAD_RE.finditer(text)}): + if t in targets or t in GUARD_ROAD_SKIP or t.endswith("-"): + continue + findings.append({ + "kind": "GHOST_ROAD", + "doc": rel, + "detail": f"guard offers `make -C pmoves {t}` as the correct path; no such target", + "scope": "guard", + }) + return findings + + # ── Baseline ──────────────────────────────────────────────────────── @@ -357,7 +434,7 @@ def main() -> int: bodies = target_bodies() scopes = {t: classify_scope(bodies.get(t, [])) for t in targets} - findings = scan(targets, scopes) + findings = scan(targets, scopes) + scan_guard_roads(targets) if args.write_baseline: write_baseline(findings)