From da476147b5c83a15957da7477ac807c5686e100f Mon Sep 17 00:00:00 2001 From: 0xQuinto Date: Tue, 8 Sep 2026 06:40:55 -0500 Subject: [PATCH 1/3] feat(poteto-mode): add lint-skills.py, a structural check for skills Checks every skill's frontmatter, that name matches its directory, that relative links and backticked paths under the skill resolve, and that bold principle references name a skill that exists. Counts dashes and arrows per skill as a prose signal without blocking. --self-test plants six defects and requires each to be caught. The authoring-a-skill playbook now points at it. --- .../playbooks/authoring-a-skill.md | 2 +- .../skills/poteto-mode/scripts/lint-skills.py | 204 ++++++++++++++++++ 2 files changed, 205 insertions(+), 1 deletion(-) create mode 100755 pstack/skills/poteto-mode/scripts/lint-skills.py diff --git a/pstack/skills/poteto-mode/playbooks/authoring-a-skill.md b/pstack/skills/poteto-mode/playbooks/authoring-a-skill.md index 8c5a55a44..05bfe3b42 100644 --- a/pstack/skills/poteto-mode/playbooks/authoring-a-skill.md +++ b/pstack/skills/poteto-mode/playbooks/authoring-a-skill.md @@ -3,7 +3,7 @@ **You own the skill's voice.** 1. Use the **create-skill** skill (Cursor's built-in for authoring SKILL.md files). -2. Validate the skill: frontmatter has `name` and `description`, referenced files exist, cross-skill links resolve. +2. Validate the skill with `scripts/lint-skills.py ` (in the poteto-mode skill): frontmatter has `name` and `description`, `name` matches the directory, referenced files exist, principle references resolve. `--dashes` counts the dashes and arrows unslop bans. 3. Test cases if structural. Skip if subjective. 4. Run **Opening a PR**. diff --git a/pstack/skills/poteto-mode/scripts/lint-skills.py b/pstack/skills/poteto-mode/scripts/lint-skills.py new file mode 100755 index 000000000..46c06d8dc --- /dev/null +++ b/pstack/skills/poteto-mode/scripts/lint-skills.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Structural lint for pstack skills. + +For every `//SKILL.md`: the frontmatter parses and carries +`name` and `description`; `name` slugified (lowercase, non-alphanumerics to +hyphens) equals the directory name, so a display name like "Poteto Mode" +passes for poteto-mode; relative +markdown links resolve from the skill directory; a backticked path whose +first segment is a subdirectory of the skill (`references/x.md`, +`scripts/log.sh`, `playbooks/feature.md`) exists, templates and globs +(``, `*`) excepted; a bold `**principle-*`** +reference names a skill that exists. Em dashes, en dashes and arrows are +counted per skill and reported, never blocking. + +Usage: lint-skills.py [...] [--dashes] [--self-test] + No path: the `skills/` directory this script ships in. + --dashes: print the dash and arrow count per skill, highest first. + --self-test: plant one defect per check in a temp tree and require each + to be caught, plus a clean fixture that passes. +Exit: 0 clean, 1 findings, 2 usage or self-test red. +""" +from __future__ import annotations + +import os +import re +import sys +import tempfile +from typing import Dict, List, Optional, Tuple + +HERE = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) +LINK_RE = re.compile(r"\]\(([^)\s]+)\)") +CODE_RE = re.compile(r"`([^`\n]+)`") +BOLD_RE = re.compile(r"\*\*(principle-[a-z0-9-]+)\*\*") +FIELD_RE = re.compile(r"^([A-Za-z_-]+):\s*(.*)$") +DASHES = ("—", "–", "→", "←", "⇒") + + +def slug(name: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", name.strip().lower()).strip("-") + + +def frontmatter(text: str) -> Tuple[Optional[Dict[str, str]], str, str]: + if not text.startswith("---\n"): + return None, "no YAML frontmatter", text + end = text.find("\n---", 4) + if end == -1: + return None, "frontmatter never closes", text + fields: Dict[str, str] = {} + for line in text[4:end].splitlines(): + m = FIELD_RE.match(line) + if m: + fields[m.group(1)] = m.group(2).strip().strip('"').strip("'") + body = text[end + 4:] + return fields, "", body + + +def skill_dirs(path: str) -> List[str]: + if os.path.isfile(os.path.join(path, "SKILL.md")): + return [os.path.abspath(path)] + out = [] + for name in sorted(os.listdir(path)): + d = os.path.join(path, name) + if not name.startswith(".") and os.path.isdir(d): + out.append(os.path.abspath(d)) + return out + + +def lint_skill(skill_dir: str, known: set) -> Tuple[List[str], int]: + name = os.path.basename(skill_dir) + skill_md = os.path.join(skill_dir, "SKILL.md") + if not os.path.isfile(skill_md): + return ["%s: SKILL.md missing" % name], 0 + with open(skill_md, encoding="utf-8") as fh: + text = fh.read() + errors: List[str] = [] + fm, err, body = frontmatter(text) + if fm is None: + errors.append("%s: %s" % (name, err)) + else: + if slug(fm.get("name", "")) != name: + errors.append("%s: frontmatter name %r does not match the directory" % (name, fm.get("name", ""))) + if not fm.get("description"): + errors.append("%s: frontmatter description missing" % name) + subdirs = set(n for n in os.listdir(skill_dir) if os.path.isdir(os.path.join(skill_dir, n))) + for m in LINK_RE.finditer(body): + raw = m.group(1) + if "://" in raw or raw.startswith("#") or raw.startswith("mailto:"): + continue + target = raw.split("#")[0] + if target and not os.path.exists(os.path.normpath(os.path.join(skill_dir, target))): + errors.append("%s: dead link %s" % (name, raw)) + for m in CODE_RE.finditer(body): + token = m.group(1).strip() + if " " in token or "/" not in token or any(ch in token for ch in "<>*"): + continue + if token.split("/")[0] not in subdirs: + continue + if not os.path.exists(os.path.join(skill_dir, token.rstrip("/"))): + errors.append("%s: `%s` does not exist" % (name, token)) + for m in BOLD_RE.finditer(body): + if m.group(1) not in known: + errors.append("%s: **%s** names no skill" % (name, m.group(1))) + dashes = sum(body.count(ch) for ch in DASHES) + return errors, dashes + + +def run(paths: List[str], show_dashes: bool) -> int: + dirs: List[str] = [] + for p in paths: + if not os.path.isdir(p): + print("✗ lint-skills: %s is not a directory" % p) + return 2 + dirs.extend(skill_dirs(p)) + known = set(os.path.basename(d) for d in dirs) + for p in paths: + parent = os.path.dirname(os.path.abspath(p)) if os.path.isfile(os.path.join(p, "SKILL.md")) else p + known.update(n for n in os.listdir(parent) if os.path.isdir(os.path.join(parent, n))) + findings: List[str] = [] + counts: List[Tuple[int, str]] = [] + for d in dirs: + errs, dashes = lint_skill(d, known) + findings.extend(errs) + counts.append((dashes, os.path.basename(d))) + if show_dashes: + for dashes, name in sorted(counts, reverse=True): + print("%4d %s" % (dashes, name)) + for f in findings: + print("✗ %s" % f) + total = sum(c for c, _ in counts) + if findings: + print("✗ lint-skills: %d findings in %d skills" % (len(findings), len(dirs))) + return 1 + print("✓ lint-skills: %d skills clean, %d dashes/arrows in bodies" % (len(dirs), total)) + return 0 + + +def _write(path: str, text: str) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + fh.write(text) + + +def _fm(name: str, description: str = "fixture") -> str: + desc = ("description: %s\n" % description) if description else "" + return "---\nname: %s\n%s---\n" % (name, desc) + + +def self_test() -> int: + fails = 0 + with tempfile.TemporaryDirectory() as tmp: + root = os.path.join(tmp, "skills") + _write(os.path.join(root, "principle-good", "SKILL.md"), _fm("principle-good") + "# ok\n") + _write(os.path.join(root, "display-name", "SKILL.md"), _fm("Display Name") + "# ok\n") + _write( + os.path.join(root, "good", "SKILL.md"), + _fm("good") + + "See [ref](references/a.md), `scripts/run.sh`, **principle-good**.\n" + + "Templates and globs are not paths: `references/.md`, `references/*.md`.\n", + ) + _write(os.path.join(root, "good", "references", "a.md"), "a\n") + _write(os.path.join(root, "good", "scripts", "run.sh"), "#!/bin/sh\n") + planted = { + "bad-name": (_fm("other") + "# x\n", "does not match the directory"), + "no-desc": (_fm("no-desc", "") + "# x\n", "description missing"), + "no-front": ("# no frontmatter here\n", "no YAML frontmatter"), + "dead-link": (_fm("dead-link") + "see [gone](./missing.md)\n", "dead link ./missing.md"), + "bad-ref": (_fm("bad-ref") + "apply **principle-nope** here\n", "**principle-nope** names no skill"), + "bad-path": (_fm("bad-path") + "run `references/missing.md`\n", "`references/missing.md` does not exist"), + } + for skill, (text, _) in planted.items(): + _write(os.path.join(root, skill, "SKILL.md"), text) + _write(os.path.join(root, "bad-path", "references", "present.md"), "x\n") + known = set(os.listdir(root)) + for skill, (_, fragment) in planted.items(): + errs, _ = lint_skill(os.path.join(root, skill), known) + if not any(fragment in e for e in errs): + print("✗ self-test %s: expected a finding containing %r, got %s" % (skill, fragment, errs or "clean")) + fails += 1 + for skill in ("good", "principle-good", "display-name"): + errs, _ = lint_skill(os.path.join(root, skill), known) + if errs: + print("✗ self-test %s: expected clean, got %s" % (skill, errs)) + fails += 1 + if fails: + print("✗ lint-skills --self-test: %d failures" % fails) + return 2 + print("✓ lint-skills --self-test: %d planted defects caught, 3 clean fixtures pass" % len(planted)) + return 0 + + +def main(argv: List[str]) -> int: + if "--self-test" in argv: + return self_test() + show_dashes = "--dashes" in argv + paths = [a for a in argv if not a.startswith("--")] + if any(a.startswith("--") and a not in ("--dashes",) for a in argv): + print(__doc__) + return 2 + return run(paths or [DEFAULT_ROOT], show_dashes) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) From 1687f775813fadda128548e5cfcb6d85bd09f499 Mon Sep 17 00:00:00 2001 From: 0xQuinto Date: Tue, 8 Sep 2026 06:40:55 -0500 Subject: [PATCH 2/3] refactor(maintain-verification-skill): coverage table as the report, harness gaps block The pass now ends in a coverage table with closed vocabularies per column (source, live, evidence, disposition) and derives clean, changed, or blocked from it. A harness gap left open makes the pass blocked instead of a footnote. Locate searches .claude, .cursor and .agents skill homes by frontmatter name and features/README.md instead of one path. A map check the skill ships runs before hand hygiene. Readers get file paths, not contents, run on the swarm workers model, and return the prerequisites their recipe needs, which is what makes an unreachable feature reportable with its cause. The live pass runs on the runtime the repo pins, uses a recipe driver when the skill ships one, and reruns the map check and doctor after the last edit. Run notes route to a show-me-your-work log. Dashes and arrows in the body go from 16 to 0. Shaped by a real run on a nine-feature map: the previous report format let a pass claim every feature was exercised without per-feature proof and park a harness false negative as a footnote. --- .../maintain-verification-skill/SKILL.md | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/pstack/skills/maintain-verification-skill/SKILL.md b/pstack/skills/maintain-verification-skill/SKILL.md index a2680b91f..af9584cc0 100644 --- a/pstack/skills/maintain-verification-skill/SKILL.md +++ b/pstack/skills/maintain-verification-skill/SKILL.md @@ -1,39 +1,58 @@ --- name: maintain-verification-skill -description: "Periodic pass that keeps a project's verification skill and feature map honest: parallel source readers per feature, one live session driving every feature, at most one PR of proven corrections. Use for /maintain-verification-skill or \"audit the verify skill\"." +description: "Periodic pass that keeps a project's verification skill and feature map honest: parallel source readers per feature, one live session driving every feature, a coverage table as the report, at most one PR of proven corrections. Use for /maintain-verification-skill or \"audit the verify skill\"." disable-model-invocation: true --- # Maintain a verification skill -A feature map rots the moment the app changes. This skill is the upkeep loop for a skill generated by `/create-verification-skill` (or any project-local verification skill with a feature map). The unit of rigor is the feature, not every sentence: cover every feature file from source and exercise every feature live, without terminalising every bullet. +A feature map rots the moment the app changes. This skill is the upkeep loop for a skill generated by `/create-verification-skill`, or for any project-local verification skill with a feature map. The unit of rigor is the feature. Cover every feature file from source, drive every feature live, and report both in one table. Not every bullet needs its own drive. ## Outcomes -Pick one, and say which: +The pass ends in exactly one outcome. The coverage table decides which one, not your impression of the run. -- **clean** — every feature got source and live coverage; nothing worth shipping. No branch, no PR. -- **changed** — one PR ships proven doc, harness, or map corrections. -- **blocked** — coverage could not finish or a proven fix could not ship safely. Say exactly what blocked it. +- **blocked.** A feature has no `live` result, a `disposition` says `harness-gap open`, or a proven fix could not ship. Say what blocked it and which features it affects. No PR. +- **changed.** The verification skill's directory has a diff of proven corrections. One PR. +- **clean.** Every feature has source and live coverage and the skill's directory has no diff. No branch, no PR. + +Product gaps never change the outcome. They are the app's problem and go under the table. ## Edit scope -Only edit the verification skill's own directory (its SKILL.md, features/, and any harness scripts it owns). Never edit product code during a run: a behavior the map describes that the app no longer does is either doc drift (fix the map) or a product regression (report it, don't paper over it in docs). +Edit only the verification skill's own directory: its `SKILL.md`, `features/`, and any helper scripts it owns. Never edit product code during a run. A behavior the map describes that the app no longer does is either doc drift, which you fix in the map, or a product regression, which you report. Do not paper a regression over in the docs. + +## The pass + +1. **Locate the target.** An argument to the skill names it. Otherwise search `.claude/skills`, `.cursor/skills`, and `.agents/skills` for a directory whose `SKILL.md` frontmatter `name` starts with `verify-` and that has `features/README.md`. One match is the target. Several matches, ask which. No match, stop and point at `/create-verification-skill`. Never invent a target. + +2. **Check the map mechanically.** If the skill ships a structural check of its own map (a `map-check` or `check-map` helper named in its body), run it first and fix what it reports. If it ships none, hand-check the README against its sibling files for missing, extra, duplicate, or dead entries, and treat the missing check as a harness gap worth one small helper. No generated inventory. + +3. **Source wave.** Launch one read-only subagent per feature file, all at once, on the `swarm workers` model. Give each the path of its feature file and the path of the skill's `SKILL.md`, not their contents. Each answers "how does this user-facing feature work?" from source, flags likely doc drift with `file:line` citations, and returns one live-verification recipe. Readers never drive the app and never edit files. Return shape: feature summary, source entry points, drift with citations or `none`, one recipe, and the prerequisites the recipe needs (runtime version, installed deps, fixtures, secrets by name, network, external services). The prerequisites are what let you report a feature as `unreachable` with its concrete cause. + +4. **Reconcile.** Every feature file has a returned summary before you continue. Merge overlapping recipes into as few app states as practical. Spot-check cited drift. Do not re-prove clean claims. Sweep recent churn (`git log` since the skill's last commit) for user-facing surfaces missing from the map. Name a concrete source path before calling a feature missing. + +5. **Live pass.** Required even when source looks clean. You drive; readers never do. Follow the verification skill's own Launch section for the instance model. Servers and UIs get one long-lived instance driven serially. Short-lived CLIs get a fresh isolated session per drive. Run on the runtime the repo pins (`engines`, the CI image), not whatever the shell has. When the skill ships a helper that drives its recipes and asserts their observables, run it and drive by hand only what it cannot reach; when it ships none and the map is stable, one such helper is the cheapest harness fix you can make. Drive every feature at least once. Hold three invariants for the whole pass, whatever fails: + - **Doctor before you drive.** Run the skill's doctor before the first drive, on each fresh session where sessions are the unit, and after any failed drive. When doctor cannot see the failure (a wedged UI on a healthy process), reset to a known state or relaunch. Never hope. An instance the doctor will not own is foreign. Do not drive it. + - **Evidence survives every cleanup.** After each cleanup, check the evidence at its named location. Do not assume. + - **Nothing a drive started outlives its usefulness.** Clean failed-iteration residue whether the session is stuck, exited, or shared. For a shared instance, clean the residue, not the instance. -## Pass + A doctor failure caused by skill drift is drift. Fix it under edit scope, restart only what the fix invalidated, and retry once before calling the pass blocked. A feature you cannot reach is `unreachable` only with the concrete prerequisite (auth, entitlement, OS, external state) and the route you tried. A prerequisite the map omits is drift. Re-drive every harness fix live before it ships. Tear down after the last drive of the run, re-drives included. Evidence stays. -0. **Locate the target.** Find the verification skill to maintain: the project-local skill whose body has launch/drive sections and a feature map (usually `.cursor/skills/verify-*/`). Several candidates → ask which one; none → stop and point at `/create-verification-skill` instead of inventing a target. +6. **Triage.** Every anomaly gets a row and one disposition. A wrong or missing user-POV description is doc drift. Fix it. Working behavior the harness cannot drive is a harness gap. Fix it. Helper rules are the same as at generation, scripts executable and their invocation shown in the skill body. A harness gap you leave open makes the pass blocked, not a footnote in the PR. App behavior that is broken is a product gap. Report it with the command and the observed result, and keep it out of the PR. -1. **Index hygiene.** Read the feature map README and glob its sibling files. Fix missing, extra, duplicate, or dead entries. Lightweight; no generated inventory. +7. **Report and ship.** Write the coverage table below and derive the outcome from it. For changed, re-read every changed file, run the map check and the doctor once more after the last edit (an edit to the skill can break its own checks), then open one PR whose body carries the table. For clean or blocked, open no PR. Reply with the table and the outcome either way. -2. **Source wave.** One read-only subagent per feature file, launched concurrently. Each explains "how does this user-facing feature work?" from source, flags likely doc drift with citations, and returns one concise live-verification recipe. Children never drive the app and never edit files. Return shape: feature summary / source entry points / likely drift or none / one recipe. +## The coverage table -3. **Reconcile.** Every feature file has a returned summary. Merge overlapping recipes into as few app states as practical. Spot-check cited drift; don't re-prove clean claims. Sweep recent churn for user-facing surfaces missing from the map — require a concrete source path before calling one missing. +One row per feature file. Closed vocabularies per column. The table is the proof. A sentence like "all features exercised" is not. -4. **Live pass.** Required even when source looks clean. The coordinator owns all driving; follow the verification skill's own launch model — one long-lived instance driven serially for servers and UIs, or a fresh isolated session per drive for short-lived CLIs (the skill's Launch section decides, not this one). Exercise every feature at least once, and hold three invariants the whole pass, whatever the failure: (1) never drive an instance you haven't health-checked since it last did something surprising — doctor before first drive, doctor on each fresh session where sessions are the unit, doctor again after any failed drive, and where doctor can't see the failure (a wedged UI state on a healthy process), reset to a known state or relaunch rather than hoping; (2) evidence captured so far survives every cleanup, checked at its named location, not assumed; (3) nothing a drive started outlives that drive's usefulness — failed-iteration residue is cleaned whether the session is stuck, exited, or shared (for a shared instance, clean the residue, not the instance). A doctor failure caused by skill drift is drift: fix it under edit scope and retry once — restart whatever the fix invalidated, nothing more — before calling the pass `blocked`. A feature that can't be reached is `verified-unreachable` only with the concrete prerequisite (auth, entitlement, OS, external state) and the route attempted; if the map omits that prerequisite, that's drift. Any harness fix from triage gets re-driven live before it ships. Final teardown happens after the last drive of the run — including those re-proofs — so nothing outlives the run (evidence stays, per the skill). +| feature | source | live | evidence | disposition | +|---|---|---|---|---| +| the feature file | `clean`, or `drift: ` | `pass`, `fail: `, or `unreachable: , ` | path of the captured proof | `none`, `doc-drift fixed`, `harness-gap fixed`, `harness-gap open`, or `product-gap reported` | -5. **Triage.** Wrong or missing user-POV description → doc drift, fix it. Working behavior the harness can't drive → harness gap, fix it; a harness fix follows the same helpers rule as generation (scripts executable, invocation documented in the skill body). App behavior that's actually broken → product gap; record it for the user, keep it out of this PR. +Under the table, list each product gap as command, expected, observed, and give the map check's final result. -6. **Ship or stop.** For changed: one PR of proven corrections, re-read every changed file first. For clean or blocked: no PR, report the outcome and the coverage honestly. +## Run notes -Keep concise run notes (features covered, unreachable prerequisites, confirmed drift, outcome) in a scratch location; don't commit them. +Keep the run's decisions in a show-me-your-work log at `.audit/maintain-.tsv`, one row per drive and per triage call. Do not commit it. From f4588ce223f7dee992a36e56d58c723da15b8e88 Mon Sep 17 00:00:00 2001 From: 0xQuinto Date: Tue, 8 Sep 2026 08:47:38 -0500 Subject: [PATCH 3/3] fix(maintain-verification-skill): split the two semicolon-joined clauses (Bugbot) --- pstack/skills/maintain-verification-skill/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pstack/skills/maintain-verification-skill/SKILL.md b/pstack/skills/maintain-verification-skill/SKILL.md index af9584cc0..e7cbc62ae 100644 --- a/pstack/skills/maintain-verification-skill/SKILL.md +++ b/pstack/skills/maintain-verification-skill/SKILL.md @@ -32,7 +32,7 @@ Edit only the verification skill's own directory: its `SKILL.md`, `features/`, a 4. **Reconcile.** Every feature file has a returned summary before you continue. Merge overlapping recipes into as few app states as practical. Spot-check cited drift. Do not re-prove clean claims. Sweep recent churn (`git log` since the skill's last commit) for user-facing surfaces missing from the map. Name a concrete source path before calling a feature missing. -5. **Live pass.** Required even when source looks clean. You drive; readers never do. Follow the verification skill's own Launch section for the instance model. Servers and UIs get one long-lived instance driven serially. Short-lived CLIs get a fresh isolated session per drive. Run on the runtime the repo pins (`engines`, the CI image), not whatever the shell has. When the skill ships a helper that drives its recipes and asserts their observables, run it and drive by hand only what it cannot reach; when it ships none and the map is stable, one such helper is the cheapest harness fix you can make. Drive every feature at least once. Hold three invariants for the whole pass, whatever fails: +5. **Live pass.** Required even when source looks clean. You drive. Readers never do. Follow the verification skill's own Launch section for the instance model. Servers and UIs get one long-lived instance driven serially. Short-lived CLIs get a fresh isolated session per drive. Run on the runtime the repo pins (`engines`, the CI image), not whatever the shell has. When the skill ships a helper that drives its recipes and asserts their observables, run it and drive by hand only what it cannot reach. When it ships none and the map is stable, one such helper is the cheapest harness fix you can make. Drive every feature at least once. Hold three invariants for the whole pass, whatever fails: - **Doctor before you drive.** Run the skill's doctor before the first drive, on each fresh session where sessions are the unit, and after any failed drive. When doctor cannot see the failure (a wedged UI on a healthy process), reset to a known state or relaunch. Never hope. An instance the doctor will not own is foreign. Do not drive it. - **Evidence survives every cleanup.** After each cleanup, check the evidence at its named location. Do not assume. - **Nothing a drive started outlives its usefulness.** Clean failed-iteration residue whether the session is stuck, exited, or shared. For a shared instance, clean the residue, not the instance.