b49ea488fb
The Stop scalar never dispatched on macOS or Linux (the PowerShell branch was always selected because check-complete.ps1 ships on every platform) and was a silent no-op anywhere CLAUDE_SKILL_DIR was unset (the :- install path fallback can never substitute because the probed variable is always a non-empty string). Both the legacy completion advisory and the v3 completion gate were dead in those environments. The scalar now selects targets by file existence and dispatches by platform, PowerShell only under MINGW/MSYS/CYGWIN, sh elsewhere. Windows output is unchanged. Patched in all 14 SKILL.md variants that carry the scalar. session-catchup probed a ~/.claude/projects name that Claude Code never writes for POSIX paths (leading dash stripped) or paths containing an underscore (replaced with a dash), so recovery after /clear silently found nothing there. The mapper now probes the exact spelling first, keeps both legacy spellings for stores created by older versions, and settles ambiguity via the cwd recorded in the newest session file. Propagated to every shipped copy including the older-generation root, .hermes, and .mastracode scripts. Also: the attestation SHA cache is keyed on the absolute plan path so two projects can no longer share a slot and report a false PLAN TAMPERED; resolve-plan-dir.ps1 reaches parity with the sh resolver (slug filter, task_plan.md requirement in the newest-dir scan, fail-closed containment); ledger-append.sh no longer truncates summaries mid-codepoint (UTF-8-safe trim via iconv with a pure-sh fallback). New long-run features, all opt-in or additive: structure-aware injection (PWF_INJECT=smart or an inject-smart .mode token) keeps the active phase, phase counts, and the last three decisions in the window late in long plans; a Next Step section in both plan templates plus a sixth reboot question; session-catchup annotates tool results (ok or FAILED with the first error line) instead of only listing attempts. Infrastructure: macos-latest joins the CI matrix, a BSD-userland simulation harness runs the script fleet without realpath, readlink, flock, or sha256sum on the Linux leg, and .gitattributes pins LF for scripts. SEO surfaces: llms.txt rewritten as a Q&A page, three problem-query docs pages, plugin.json and CITATION.cff keyword hygiene. Suite grows from 217 to 301 passed, 11 skipped, on Windows and both existing CI legs; default hook output stays byte-identical to v2.43 (legacy invariant proven by the existing invariant tests).
114 lines
4.2 KiB
Python
114 lines
4.2 KiB
Python
"""Guards for the v3.8.0 problem-query docs pages.
|
||
|
||
The three pages under docs/ answer high-volume search queries and carry
|
||
strict content rules: fixed H1, 60-120 line budget, the two-route Install
|
||
section, cross-links between all three, links back to README and
|
||
docs/installation.md, internal-v1 framing on any recovery number, no
|
||
em/en dashes, and no competitor method names. This test pins each rule
|
||
so later edits cannot silently break them.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
DOCS_DIR = REPO_ROOT / "docs"
|
||
|
||
# filename -> required exact H1 (first line of the file)
|
||
PAGES = {
|
||
"claude-code-lost-context-after-compaction.md": (
|
||
"# Claude Code lost context after compaction: how to recover and prevent it"
|
||
),
|
||
"agent-forgets-plan-after-clear.md": (
|
||
"# My coding agent forgets the plan after /clear: the file-based fix"
|
||
),
|
||
"long-running-agent-tasks.md": (
|
||
"# Long-running agent tasks: keeping a coding agent on track for hours"
|
||
),
|
||
}
|
||
|
||
PLUGIN_MARKETPLACE = "/plugin marketplace add OthmanAdi/planning-with-files"
|
||
PLUGIN_INSTALL = "/plugin install planning-with-files@planning-with-files"
|
||
NPX_ONE_LINER = (
|
||
"npx skills add OthmanAdi/planning-with-files --skill planning-with-files -g"
|
||
)
|
||
|
||
# Honesty rule for the release: competing planning methods are never named
|
||
# in SEO-facing docs pages.
|
||
COMPETITOR_NAMES = ("superpowers", "spec-kit", "memory-bank", "cline")
|
||
|
||
|
||
def read(name: str) -> str:
|
||
return (DOCS_DIR / name).read_text(encoding="utf-8")
|
||
|
||
|
||
class SeoDocsPagesTests(unittest.TestCase):
|
||
def test_pages_exist(self) -> None:
|
||
for name in PAGES:
|
||
self.assertTrue((DOCS_DIR / name).is_file(), name)
|
||
|
||
def test_line_budget_60_to_120(self) -> None:
|
||
for name in PAGES:
|
||
count = len(read(name).splitlines())
|
||
self.assertGreaterEqual(count, 60, f"{name}: {count} lines")
|
||
self.assertLessEqual(count, 120, f"{name}: {count} lines")
|
||
|
||
def test_exact_h1_on_first_line(self) -> None:
|
||
for name, h1 in PAGES.items():
|
||
first = read(name).splitlines()[0]
|
||
self.assertEqual(first, h1, name)
|
||
|
||
def test_install_section_with_both_routes(self) -> None:
|
||
for name in PAGES:
|
||
body = read(name)
|
||
self.assertIn("## Install", body, name)
|
||
install = body.split("## Install", 1)[1]
|
||
self.assertIn(PLUGIN_MARKETPLACE, install, name)
|
||
self.assertIn(PLUGIN_INSTALL, install, name)
|
||
self.assertIn(NPX_ONE_LINER, install, name)
|
||
|
||
def test_links_to_readme_and_installation_guide(self) -> None:
|
||
for name in PAGES:
|
||
body = read(name)
|
||
self.assertIn("../README.md", body, f"{name}: missing README link")
|
||
self.assertIn(
|
||
"(installation.md)", body, f"{name}: missing docs/installation.md link"
|
||
)
|
||
|
||
def test_pages_cross_link_each_other(self) -> None:
|
||
for name in PAGES:
|
||
body = read(name)
|
||
for other in PAGES:
|
||
if other == name:
|
||
continue
|
||
self.assertIn(other, body, f"{name} must link to {other}")
|
||
|
||
def test_no_em_or_en_dashes(self) -> None:
|
||
for name in PAGES:
|
||
body = read(name)
|
||
self.assertNotIn("—", body, f"{name}: em dash found")
|
||
self.assertNotIn("–", body, f"{name}: en dash found")
|
||
|
||
def test_recovery_number_keeps_internal_v1_framing(self) -> None:
|
||
# Wherever the 5.0 vs 13.3 turn numbers appear, the internal-v1
|
||
# framing must appear on the same page.
|
||
for name in PAGES:
|
||
body = read(name)
|
||
if "5.0 turns" in body or "13.3" in body:
|
||
low = body.lower()
|
||
self.assertIn("internal", low, name)
|
||
self.assertIn("v1", low, name)
|
||
self.assertIn("author-run", low, name)
|
||
|
||
def test_no_competitor_names(self) -> None:
|
||
for name in PAGES:
|
||
low = read(name).lower()
|
||
for competitor in COMPETITOR_NAMES:
|
||
self.assertNotIn(competitor, low, f"{name}: names {competitor}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|