diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93832f81..f17053e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,11 @@ jobs: make frontend-test make frontend-build + + - name: Docs quality gates (lint + relative link check) + run: | + make docs-check + - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 00000000..301225ec --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,23 @@ +# markdownlint-cli2 config +# Keep the ruleset intentionally tiny to avoid noisy churn. + +config: + default: false + MD009: true # no trailing spaces + MD010: true # no hard tabs + MD012: true # no multiple consecutive blank lines + MD047: true # single trailing newline + +globs: + - "**/*.md" + +ignores: + - "**/node_modules/**" + - "**/.next/**" + - "**/dist/**" + - "**/build/**" + - "**/.venv/**" + - "**/__pycache__/**" + - "**/.pytest_cache/**" + - "**/.mypy_cache/**" + - "**/coverage/**" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 323178a9..5c5303a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,6 @@ git checkout -b If you accidentally based your branch off another feature branch, fix it by cherry-picking the intended commits onto a clean branch and force-pushing the corrected branch (or opening a new PR). - ### Expectations - Keep PRs **small and focused** when possible. diff --git a/Makefile b/Makefile index 694edd15..b64228ff 100644 --- a/Makefile +++ b/Makefile @@ -122,3 +122,15 @@ backend-templates-sync: ## Sync templates to existing gateway agents (usage: mak .PHONY: check check: lint typecheck backend-coverage frontend-test build ## Run lint + typecheck + tests + coverage + build + + +.PHONY: docs-lint +docs-lint: frontend-tooling ## Lint markdown files (tiny ruleset; avoids noisy churn) + $(NODE_WRAP) npx markdownlint-cli2@0.15.0 --config .markdownlint-cli2.yaml "**/*.md" + +.PHONY: docs-link-check +docs-link-check: ## Check for broken relative links in markdown docs + python scripts/check_markdown_links.py + +.PHONY: docs-check +docs-check: docs-lint docs-link-check ## Run all docs quality gates diff --git a/backend/templates/AUTONOMY.md b/backend/templates/AUTONOMY.md index 0e22df5b..7cb418c8 100644 --- a/backend/templates/AUTONOMY.md +++ b/backend/templates/AUTONOMY.md @@ -31,4 +31,3 @@ This file defines how you decide when to act vs when to ask. ## Collaboration defaults - If you are idle/unassigned: pick 1 in-progress/review task owned by someone else and leave a concrete, helpful comment (context gaps, quality risks, validation ideas, edge cases, handoff clarity). - If you notice duplicate work: flag it and propose a merge/split so there is one clear DRI per deliverable. - diff --git a/backend/templates/SELF.md b/backend/templates/SELF.md index 1823bf61..9273927e 100644 --- a/backend/templates/SELF.md +++ b/backend/templates/SELF.md @@ -66,4 +66,3 @@ Notes: | Date | Change | |------|--------| | | | - diff --git a/docs/03-development.md b/docs/03-development.md new file mode 100644 index 00000000..4949c0c3 --- /dev/null +++ b/docs/03-development.md @@ -0,0 +1,3 @@ +# Development workflow + +Placeholder: see root `README.md` for current setup steps. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..80a9fbe9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Mission Control docs + +This folder is the starting point for Mission Control documentation. + +## Sections + +- [Development workflow](./03-development.md) +- [Testing guide](./testing/README.md) +- [Coverage policy](./coverage-policy.md) +- [Deployment](./deployment/README.md) +- [Production notes](./production/README.md) +- [Troubleshooting](./troubleshooting/README.md) +- [Gateway WebSocket protocol](./openclaw_gateway_ws.md) + +## Status + +These pages are minimal placeholders so repo-relative links stay healthy. The actual docs +information architecture will be defined in the Docs overhaul tasks. diff --git a/docs/coverage-policy.md b/docs/coverage-policy.md new file mode 100644 index 00000000..4476e2a8 --- /dev/null +++ b/docs/coverage-policy.md @@ -0,0 +1,3 @@ +# Coverage policy + +Placeholder: coverage policy is currently documented in the root `Makefile` (`backend-coverage`). diff --git a/docs/deployment/README.md b/docs/deployment/README.md new file mode 100644 index 00000000..cb39e267 --- /dev/null +++ b/docs/deployment/README.md @@ -0,0 +1,3 @@ +# Deployment guide + +Placeholder. diff --git a/docs/openclaw_gateway_ws.md b/docs/openclaw_gateway_ws.md new file mode 100644 index 00000000..5fc7b84e --- /dev/null +++ b/docs/openclaw_gateway_ws.md @@ -0,0 +1,3 @@ +# Gateway WebSocket protocol + +Placeholder. diff --git a/docs/production/README.md b/docs/production/README.md new file mode 100644 index 00000000..e461bfde --- /dev/null +++ b/docs/production/README.md @@ -0,0 +1,3 @@ +# Production notes + +Placeholder. diff --git a/docs/testing/README.md b/docs/testing/README.md new file mode 100644 index 00000000..80dcee45 --- /dev/null +++ b/docs/testing/README.md @@ -0,0 +1,3 @@ +# Testing guide + +Placeholder: see root `README.md` and `CONTRIBUTING.md` for current commands. diff --git a/docs/troubleshooting/README.md b/docs/troubleshooting/README.md new file mode 100644 index 00000000..1b897489 --- /dev/null +++ b/docs/troubleshooting/README.md @@ -0,0 +1,3 @@ +# Troubleshooting + +Placeholder. diff --git a/scripts/check_markdown_links.py b/scripts/check_markdown_links.py new file mode 100755 index 00000000..9123cd79 --- /dev/null +++ b/scripts/check_markdown_links.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Lightweight markdown link checker for repo docs. + +Checks *relative* links inside markdown files and fails CI if any targets are missing. + +Design goals: +- No external deps. +- Ignore http(s)/mailto links. +- Ignore pure anchors (#foo). +- Support links with anchors (./path.md#section) by checking only the path part. + +Limitations: +- Does not validate that anchors exist inside target files. +- Does not validate links generated dynamically or via HTML. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)") + + +def iter_md_files(root: Path) -> list[Path]: + """Return markdown files to check. + + Policy: + - Always check root contributor-facing markdown (`README.md`, `CONTRIBUTING.md`). + - If `docs/` exists, also check `docs/**/*.md`. + + Rationale: + - We want fast feedback on broken *relative* links in the most important entrypoints. + - We intentionally do **not** crawl external URLs. + """ + + files: list[Path] = [] + + for p in (root / "README.md", root / "CONTRIBUTING.md"): + if p.exists(): + files.append(p) + + docs = root / "docs" + if docs.exists(): + files.extend(sorted(docs.rglob("*.md"))) + + # de-dupe + stable order + return sorted({p.resolve() for p in files}) + + +def normalize_target(raw: str) -> str | None: + raw = raw.strip() + if not raw: + return None + if raw.startswith("http://") or raw.startswith("https://") or raw.startswith("mailto:"): + return None + if raw.startswith("#"): + return None + # strip query/fragment + raw = raw.split("#", 1)[0].split("?", 1)[0] + if not raw: + return None + return raw + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + md_files = iter_md_files(root) + + missing: list[tuple[Path, str]] = [] + + for md in md_files: + text = md.read_text(encoding="utf-8") + for m in LINK_RE.finditer(text): + target_raw = m.group(1) + target = normalize_target(target_raw) + if target is None: + continue + + # Skip common markdown reference-style quirks. + if target.startswith("<") and target.endswith(">"): + continue + + # Resolve relative to current file. + resolved = (md.parent / target).resolve() + if not resolved.exists(): + missing.append((md, target_raw)) + + if missing: + print("Broken relative links detected:\n") + for md, target in missing: + print(f"- {md.relative_to(root)} -> {target}") + print(f"\nTotal: {len(missing)}") + return 1 + + print(f"OK: checked {len(md_files)} markdown files") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())