Skip to content

feat(framework): validate the framework's files deterministically #1149

Description

@alexsoyes

Problem

The framework's structure is held by rules a model reads, not by checks. PR #601 said it: "R6, R7, R8, R11, R12, R13, R14, R15, R16 and R18 are all testable — […] but nothing runs them in CI." So a skill drifts, a description gets truncated, and each drift costs tokens in every session. The generator's review catches some of it, with a model, after the fact.

Spotify's Shunt moved the same idea from advice to enforcement: "The rules were advisory, not enforced. Claude could ignore them." Its hook measures, then answers.

Measured on next, 2026-11-05:

What Now
SKILL.md 52 files, longest 131 lines
Action files 146 files, longest 65 lines; 2 lack ## Input or ## Process
Descriptions 173 to 355 characters, median 218, 11,806 in total
Skill named apart from its folder 1, fixed by #1148
Skill folder names up to 22 characters, all prefixed NN-
Action file names up to 32 characters; 6 have no NN- prefix (answer.md, forget.md, frame.md, identity.md, start.md, stop.md)
Reference file names up to 35 characters; 3 have a NN- prefix, the others none

Scope

One validator, deterministic: no model, no network, same input gives the same output.

Checks Rule Example of a failure
Frontmatter name equals the folder; description present, bounded, opening on its distinct intent, naming no command plugins/aidd-x/skills/05-foo/SKILL.md:2 V1 name "foo" differs from folder "05-foo"
Description length Per skill, and in total across the plugins, since Codex shortens the list once it exceeds its budget …/SKILL.md:3 V2 description is 355 characters, limit 240
Markdown structure Required sections, in order, per file kind …/actions/02-plan.md:14 V3 "## Process" before "## Input"
Steps Numbered, each one action …/actions/02-plan.md:21 V4 step 3 is not a numbered line
File size Maximum lines per file kind …/SKILL.md:1 V5 180 lines, limit 150: move routes to actions
File names Maximum length and required prefix, by a pattern per file kind …/actions/start.md V8 name does not match ^\d{2}-[a-z0-9-]+\.md$
Forbidden mentions Text a file must not hold, per file kind, each with its reason. Memory is loaded automatically, so a skill naming a memory file duplicates it …/SKILL.md:12 V9 names "aidd_docs/memory/": memory is already loaded, state the rule without the path
Links Every link resolves, on scripts/check-markdown-links.js, which already covers Markdown links, @ includes and src:, and not paths in backticks …/actions/02-plan.md:9 V10 link "../references/foo.md" does not exist
Folder size Maximum files per folder …/actions V6 14 files, limit 12: split the skill
Execution budget Lines an agent loads for one route: SKILL.md plus one action and its references (#418) …/SKILL.md → actions/02-plan.md V7 loads 420 lines, limit 300
Other artifacts Agents, rules and plugin.json hold to the same frontmatter rules …/agents/reviewer.md:2 V1 no description

The limits above are proposals, to set from the measures; the first run records what already exceeds them.

Configuration: every limit and name pattern lives in JSON config, mapped to the files it applies to; no check hardcodes one.

Example config, an excerpt. Limits and section order are proposals:

{
  "kinds": {
    "skill-folder": {
      "match": ["plugins/*/skills/*/"],
      "rules": { "V8": { "maxLength": 24, "pattern": "^\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$" } }
    },
    "skill": {
      "match": ["plugins/*/skills/*/SKILL.md"],
      "rules": {
        "V1": { "required": ["name", "description"], "nameEquals": "parent" },
        "V2": { "maxChars": 240, "maxTotalChars": 12000 },
        "V5": { "maxLines": 70 }
      }
    },
    "action": {
      "match": ["plugins/*/skills/*/actions/**/*.md"],
      "rules": {
        "V3": { "required": ["Output", "Process"], "forbidden": ["Test", "Tests"], "order": ["Input", "Output", "Process", "Rules"] },
        "V4": { "section": "Process" },
        "V5": { "maxLines": 70 },
        "V8": { "maxLength": 32, "pattern": "^\\d{2}-[a-z0-9]+(-[a-z0-9]+)*\\.md$" },
        "V9": { "forbidden": [{ "text": "aidd_docs/memory/", "why": "memory is already loaded, state the rule without the path" }] },
        "V10": { "forms": ["markdown", "include", "src"] }
      }
    },
    "reference": {
      "match": ["plugins/*/skills/*/references/**"],
      "rules": {
        "V5": { "maxLines": 125 },
        "V8": { "maxLength": 36, "pattern": "^(\\d{2}-)?[a-z0-9]+(-[a-z0-9]+)*\\.(md|json)$" }
      }
    },
    "agent": {
      "match": ["plugins/*/agents/*.md"],
      "rules": {
        "V1": { "required": ["name", "description"], "nameEquals": "stem" },
        "V8": { "maxLength": 16, "pattern": "^[a-z]+(-[a-z0-9]+)*\\.md$" }
      }
    }
  }
}

Example report, from a prototype run on next with a config like this one:

plugins/aidd-vcs/skills/05-resolve-conflict/SKILL.md:2 V1 name "resolve-conflict" differs from "05-resolve-conflict"
plugins/aidd-refine/skills/05-review-conversation/SKILL.md:3 V2 description is 354 characters, limit 240: open on the distinct intent, cut the rest
plugins/aidd-dev/skills/05-review/actions/02-review.md:19 V3 "## Output" after "## Process": order is Input, Output, Process, Rules
plugins/aidd-context/skills/00-onboard/actions/01-scan.md:22 V3 "## Test" is forbidden: move the invariant to Rules
plugins/aidd-dev/skills/09-for-sure/actions/02-auto-accept.md:15 V4 not a numbered step: "Apply these rules in order to "
plugins/aidd-orchestrator/skills/00-async-dev/SKILL.md:1 V5 131 lines, limit 70: move detail to a reference
plugins/aidd-telemetry/skills/01-usage/actions/frame.md V8 name does not match ^\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$: rename it

Example of a forbidden mention, a real line from the feat/review-comment-faq branch:

- Reach the pull or merge request through the VCS tool `aidd_docs/memory/vcs.md` names, else the one the remote points to.

plugins/aidd-vcs/skills/06-answer-comments/SKILL.md:26 V9 names "aidd_docs/memory/": memory is already loaded, state the rule without the path

Example of a broken config, which stops the run with exit 2 instead of silently disabling a check:

config script: unknown rule "V9"
config agent.V8: unknown key "maxLenght"
config agent.V8.maxLength: expected integer
config hook: glob "plugins/*/hook/**" matches nothing
config plugins/aidd-context/hooks/hooks.json: 0 kinds, expected 1

Expected action shape:

# Plan

## Input
- The request to plan.

## Process
1. Read the request.
2. Write the phases.

## Output
- `plan.md`, one section per phase.

Generation: a generate-skill-action script writes this skeleton, so a new file starts valid and the generator's prose stops describing a shape a script can produce.

Where it runs, one engine for all three:

  1. By hand: node scripts/validate-framework.cjs [path...], exit 1 on an error.
  2. Write hook: hands the model the lines its own write broke, shaped <file>:<line> <rule> <message>, like #1146's memory hook. It reuses that engine rather than adding a second one.
  3. Pre-commit and CI: block.

Existing drift: a baseline lists today's violations, and may only shrink, so the validator can block from day one. An entry that no longer fires fails the run.

Absorbed from the issues this one closes

  • #985: remove the Test / Tests sections from every skill, and give constraints one skill-level Rules section plus an optional action-level one. Every meaningful verification step keeps a home. The validator then refuses a ## Test section. The generator's R11 and R13 change with it.
  • #794: rewrite the descriptions it lists (aidd-telemetry, 12-cook, 10-todo, 08-three-amigos, 01-sdlc, 07-refactor, 02-challenge, 09-for-sure) so the distinct intent comes first and survives truncation, and align R3 and its template. Its table and acceptance criteria stay the reference. Note: its measures predate today's above.

To brainstorm

To settle before building:

  • Mapping: how a config maps rules to files: by file kind, by glob, or both.
  • Files: one JSON for everything, or one per family of rules.
  • Prefix: which kinds require NN-. Today skills do, references almost never, agents never, and 6 actions don't.
  • Size: whether a name's limit covers the file name alone or the whole path (longest today: 95 characters).
  • Overrides: whether a plugin can tighten or loosen the shared config.
  • Mentions: which ones stay legitimate. On next, 45 lines in 17 files name aidd_docs/memory, 38 of them in aidd-context, which writes that memory; 07-refactor also records into it. A script can't tell reading from writing, so these need an exception per path or the baseline.
  • Links: whether to also check paths in backticks, which may be relative to the file, to the plugin or to the user's project.

Acceptance criteria

  • make validate runs every check above, and exits 0 on next with the baseline.
  • Changing a limit or a name pattern takes a JSON edit, no code.
  • Each rule has a fixture that breaks it and a test that sees it fail, so a rule that stops firing fails the suite.
  • Two runs on the same tree give byte-identical output; no check reads the network or calls a model.
  • Every error names the file, the line where it has one, the rule, and what to do.
  • A write breaking a rule gets its error back in the same turn on each host the memory hook covers; the hook stays silent otherwise and never fails the tool call.
  • A new skill and action from the generator pass with no edit.
  • No shipped skill or action holds a Test section, and the invariants those sections carried have a home.
  • The descriptions docs(framework): fiabiliser les descriptions des skills sous troncature Codex #794 lists are rewritten; a plain request for a task list does not select 10-todo.
  • aidd-orchestrator's validator agent (#983) runs the validator first, and judges only what a script cannot.

Prior art

  • #1146: memory-file linter and write hook, rules M1 to M7 (a prose line over 25 words, a file over 400). The first slice, and the engine to extend.
  • #1148: a skill's name equals its folder, held by a test. Moves into the validator.
  • #1142: make check-hosts checks what each tool really loads. The installed side of this issue's source side.
  • #250: "il doit être déterministe", the pre-commit and CI model for orthogonality.
  • aidd#46: a validation script reporting file:line, wired to a blocking lefthook.
  • Already mechanical, to keep or fold in: scripts/check-skill-argument-hints.mjs (R4), scripts/lib/architecture-scan.js (router lists every action), lefthook.yml skill-frontmatter (presence only), scripts/check-markdown-links.js.
  • Related: #1017 (generator aligned with Anthropic's guidance), #978 (tool compatibility).

Out of scope

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions