diff --git a/.agents/skills/docs-lint/lint.py b/.agents/skills/docs-lint/lint.py index dc1261d..7de8e49 100644 --- a/.agents/skills/docs-lint/lint.py +++ b/.agents/skills/docs-lint/lint.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 """Lint committed docs against .agents/shared/writing-style.md. -Checks two mechanical rules: +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 +import os, re, sys, glob BANNED = [ "pivotal", "crucial", "vital", "groundbreaking", "transformative", "testament", @@ -33,9 +34,125 @@ def iter_md(paths): if f.endswith(".md"): yield os.path.join(root, f) +def check_plans(): + """Check plan status consistency: active plans with 'Done' status, files + missing from index, dangling index entries, done files with wrong status.""" + REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + plans_dir = os.path.join(REPO, "plans") + done_dir = os.path.join(REPO, "plans", "done") + index_path = os.path.join(plans_dir, "index.md") + + if not os.path.exists(index_path): + return 0 + + violations = 0 + STATUS_RE = re.compile(r'^\*\*Status:\*\*\s*(.+)', re.I) + + # Parse index.md for active and done entries + active_files = set() + done_files = set() + current_section = None + with open(index_path) as f: + for line in f: + if line.startswith("## Active"): + current_section = "active" + continue + if line.startswith("## Done"): + current_section = "done" + continue + if current_section == "active": + m = re.search(r'\]\(([^)]+)\)', line) + if m: + active_files.add(m.group(1)) + elif current_section == "done": + m = re.search(r'\]\(([^)]+)\)', line) + if m: + done_files.add(m.group(1)) + + # Active plans on disk (not in done/, not index.md) + disk_active = set() + for f in glob.glob(os.path.join(plans_dir, "*.md")): + name = os.path.basename(f) + if name == "index.md": + continue + disk_active.add(name) + + # Done plans on disk + disk_done = set() + if os.path.isdir(done_dir): + for f in glob.glob(os.path.join(done_dir, "*.md")): + disk_done.add("done/" + os.path.basename(f)) + + # Check 1: active plans on disk whose internal status is Done/Implemented/Complete + for name in disk_active: + fpath = os.path.join(plans_dir, name) + with open(fpath) as f: + for line_num, line in enumerate(f, 1): + if line_num > 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))): diff --git a/plans/2026-07-08-liveness-drift-and-ux-cohesion.md b/plans/2026-07-08-liveness-drift-and-ux-cohesion.md index 674066f..c322ff1 100644 --- a/plans/2026-07-08-liveness-drift-and-ux-cohesion.md +++ b/plans/2026-07-08-liveness-drift-and-ux-cohesion.md @@ -1,8 +1,6 @@ # 2026-07-08 — Liveness, drift, and UX cohesion -**Status:** Code complete for Phases 1–4 core scope; not yet deployed to the -live containers (pending explicit go-ahead — see below). Phase 5 partially -covered by pre-existing endpoints; full CRUD UI deferred. +**Status:** In Progress — Phases 1–4 code complete; not yet deployed. Phase 5 deferred. - **Phase 1 (drift/staleness):** done. Health/metrics/events misattribution fix, staleness sweep, `/entities` health+freshness, dashboard/fleet-health diff --git a/plans/2026-07-08-signal-triggers.md b/plans/done/2026-07-08-signal-triggers.md similarity index 99% rename from plans/2026-07-08-signal-triggers.md rename to plans/done/2026-07-08-signal-triggers.md index 0ec7b2a..dda7614 100644 --- a/plans/2026-07-08-signal-triggers.md +++ b/plans/done/2026-07-08-signal-triggers.md @@ -1,6 +1,6 @@ # 2026-07-08 — Signal triggers: host health checks -**Status:** Implemented (Phases 1-5 complete) +**Status:** Done — Phases 1-5 complete ## Goal diff --git a/plans/index.md b/plans/index.md index 057fd16..595232a 100644 --- a/plans/index.md +++ b/plans/index.md @@ -13,7 +13,9 @@ went sideways, open an investigation. | 2026-07-08 | [Oikos gaps, broken things, and improvements](2026-07-08-oikos-gaps-and-improvements.md) | Planned | | 2026-07-08 | [Control room web UI](2026-07-08-control-room-webui.md) | In Progress | | 2026-07-08 | [Nomos resident agent (renames Hermes)](2026-07-08-nomos-resident-agent.md) | In Progress | +| 2026-07-08 | [Liveness, drift, and UX cohesion](2026-07-08-liveness-drift-and-ux-cohesion.md) | In Progress | | 2026-07-09 | [Chat sessions: reliability, cost, and session-management fixes](2026-07-09-chat-sessions-improvements.md) | Planned | +| 2026-07-09 | [Session execution, UX, and learning improvements](2026-07-09-session-execution-and-ux-fixes.md) | Planned | ## Done @@ -33,6 +35,7 @@ See [`done/`](done/) for executed plans: | 2026-07-07 | [Client lifecycle in Go — enrollment through deprecation](done/2026-07-07-client-lifecycle-in-go.md) | | 2026-07-08 | [Fix MCP analysis tools](done/2026-07-08-fix-mcp-analysis-tools.md) | | 2026-07-06 | [Consolidate Oikos control plane onto mac-mini](done/2026-07-06-consolidate-oikos-control-plane-onto-mac-mini.md) | +| 2026-07-08 | [Signal triggers: host health checks](done/2026-07-08-signal-triggers.md) | ## Conventions