#!/usr/bin/env python3 """Lint committed docs against .agents/shared/writing-style.md. Checks: 1. Banned vocabulary (significance puffers, analytical verbs, poetic nouns, promotional adjectives, opening crutches). 2. Broken relative markdown links. 3. Plan status consistency (status vs location vs index). Prose-voice rules are not machine-checkable; this covers the parts that are. Run from the repo root: python3 .agents/skills/docs-lint/lint.py [paths...] Exit 1 if any violation is found. """ import os, re, sys, glob BANNED = [ "pivotal", "crucial", "vital", "groundbreaking", "transformative", "testament", "paramount", "invaluable", "delve", "leverage", "utilize", "facilitate", "foster", "showcase", "underscore", "streamline", "harness", "tapestry", "realm", "paradigm", "nexus", "cornerstone", "robust", "seamless", "innovative", "cutting-edge", "meticulous", "holistic", "comprehensive", "in today's world", "it's worth noting", "it is important to note", ] BAN_RE = re.compile(r'(? 5: break m = STATUS_RE.match(line) if m: status = m.group(1).strip().lower() done_keywords = ["done", "implemented", "complete", "completed"] if any(status.startswith(kw) for kw in done_keywords): print(f"{fpath}:{line_num}: status '{m.group(1).strip()}' — file is in plans/ but appears done; move to done/") violations += 1 break # Check 2: active plans on disk not in index for name in sorted(disk_active): if name not in active_files: fpath = os.path.join(plans_dir, name) print(f"{fpath}:1: not listed in plans/index.md Active table") violations += 1 # Check 3: done plans on disk not in index for name in sorted(disk_done): if name not in done_files: fpath = os.path.join(REPO, "plans", name) print(f"{fpath}:1: not listed in plans/index.md Done table") violations += 1 # Check 4: index entries with no file on disk for name in sorted(active_files): if name not in disk_active: print(f"plans/index.md: active entry '{name}' — file not found on disk") violations += 1 for name in sorted(done_files): if name not in disk_done: print(f"plans/index.md: done entry '{name}' — file not found on disk") violations += 1 # Check 5: files in done/ whose internal status doesn't say Done for name in disk_done: fpath = os.path.join(REPO, "plans", name) with open(fpath) as f: found_status = False for line_num, line in enumerate(f, 1): if line_num > 5: break m = STATUS_RE.match(line) if m: found_status = True status = m.group(1).strip().lower() if not status.startswith("done"): print(f"{fpath}:{line_num}: status '{m.group(1).strip()}' — file is in done/ but status is not 'Done'") violations += 1 break if not found_status: print(f"{fpath}:1: file is in done/ but has no Status header") violations += 1 return violations def main(argv): paths = argv or ["knowledge", ".agents", "operations", "investigations", "plans"] violations = 0 if "plans" in paths or any(p.startswith("plans") for p in paths): violations += check_plans() # The style guide and this skill enumerate the banned words by definition. ban_exempt = ("shared/writing-style.md", "skills/docs-lint/") for f in sorted(set(iter_md(paths))): check_banned = not any(x in f for x in ban_exempt) fence = False with open(f) as fh: for ln, line in enumerate(fh, 1): if line.lstrip().startswith("```"): fence = not fence; continue if fence: continue if check_banned: for m in BAN_RE.finditer(line): print(f"{f}:{ln}: banned word '{m.group(1)}'") violations += 1 for m in LINK.finditer(line): link = m.group(1) if re.match(r'^(https?:|mailto:|#|/)', link): continue path = re.split(r'[#?]', link)[0] if not path: continue tgt = os.path.normpath(os.path.join(os.path.dirname(f), path)) if not os.path.exists(tgt): print(f"{f}:{ln}: broken link -> {link}") violations += 1 print(f"\n{violations} violation(s)") return 1 if violations else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))