epythet.agentic_readme

Check that a README documents a project’s agentic aspects; render and place the section.

A project that ships skills, subagents or instruction files, and whose site publishes agent-readable documentation (llms.txt, <package>.md), should say so in its README: that is where an agent arriving at the repository looks first. check_readme() reuses epythet.ai_artifacts.discover_artifacts() to learn what exists and reads the README to see whether each kind is mentioned (a gh skill install line, a skill or agent name, CLAUDE.md, llms.txt, a heading about agents). The result is a ReadmeReport: one KindCheck per kind, each ok, warn or n/a. The check is a heuristic: a mention counts whatever the sentence around it says.

render_section() produces the README section from the effective snippets (epythet.userconfig: the user’s agentic-readme-section.md and agentic-readme-humor.md over the packaged defaults) and the effective ReadmePolicy (the user’s config.toml, with a project’s [tool.epythet.readme] keys on top so a committed README does not depend on who ran the tool). place_section() puts it between two marker comments so a later run updates it in place: before the first heading after the title when agentic_first is on, at the end otherwise. The “for humans” line links to the heading that follows the section.

>>> readme = "# pkg\n\nTagline.\n\n# Install\n\npip install pkg\n"
>>> start, end, heading, level = place_section(readme, agentic_first=True)
>>> readme[start:end], heading.title, level
('', 'Install', 1)
>>> humans_link_for(heading)
'[Install](#install)'

Module Attributes

MARKER_START

The comments that delimit the generated section in a README (each on its own line).

README_NAMES

README filenames, in order of preference.

KINDS

The kinds a check reports on, in display order.

SECTION_SNIPPET

The snippet names the section is rendered from.

NEUTRAL_INTRO

The opener used when humor is off.

MAX_BLURB

Longest blurb (first sentence of a description) shown per skill or agent.

HEADLINE_SUFFIXES

Skill name suffixes that make a skill the one named in the install line.

SECTION_FIELDS

The fields a section snippet may use.

Functions

ai_readme_check(project_dir, *[, format, ...])

Report whether the README documents the project's agentic aspects; draft or write the section.

blurb(description, *[, max_length])

The first sentence of a skill or agent description, short enough for a table cell.

check_readme(project_dir, *[, config, ...])

Which agentic aspects the project has, and whether its README mentions each.

draft_section(project_dir, *[, user_config, ...])

Render the section for a project as it would be placed: (section, readme_text, start, end).

find_readme(project_dir)

The project's README, by the usual names (None when there is none).

github_anchor(title)

GitHub's anchor for a heading title.

headings_of(text)

Every ATX heading outside fenced code blocks.

headline_skill(skills)

The skill named in the install line: a *-setup-like one if any, else the first installable.

humans_link_for(heading)

[Title](#anchor) for the heading after the section, or a plain fallback.

instruction_text(*[, snippets])

The instruction the skill hands an agent when the policy is add.

load(project_dir, *[, config, artifacts, ...])

Resolve a project once; each argument given is used instead of being loaded.

load_project(project_dir)

(config, artifacts) for a project; config is None for a non-Python tree.

marker_span(text, *[, strict])

The character span of the marked section (None when there is none).

place_section(text, *, agentic_first)

Where the section goes in text: (start, end, next_heading, level).

read_readme(path)

(text, newline): the README with \n line ends, and the style to write back.

render_section(artifacts, config, *, policy)

The README section for artifacts, from the effective snippets and policy.

section_snippet_for(artifacts)

The name of the section snippet artifacts calls for.

ships_tooling(artifacts)

Whether the project ships anything an agent installs or reads as instructions.

splice_section(text, section, *, start, end)

text with section in place of text[start:end], blank lines kept sane.

write_section(project_dir, *[, user_config])

Add or update the marked section in the project's README; returns (path, outcome).

Classes

Heading(line, level, title)

A Markdown ATX heading: its line index, level and title text.

KindCheck(kind, present, documented[, ...])

One artifact kind: whether the project has it and whether the README covers it.

Project(root, config, artifacts, readme, ...)

What every entry point needs once: config, artifacts, README, effective policy.

ReadmeReport(project_dir, readme, checks, ...)

The outcome of check_readme() for one project.

Exceptions

SectionError

The README or a snippet is in a state the tool will not write over.

epythet.agentic_readme.HEADLINE_SUFFIXES = ('-setup', '-quickstart', '-start')

Skill name suffixes that make a skill the one named in the install line.

class epythet.agentic_readme.Heading(line, level, title)[source]

Bases: object

A Markdown ATX heading: its line index, level and title text.

epythet.agentic_readme.KINDS = ('skills', 'subagents', 'instruction_files', 'agent_docs', 'section')

The kinds a check reports on, in display order.

class epythet.agentic_readme.KindCheck(kind, present, documented, items=(), evidence='')[source]

Bases: object

One artifact kind: whether the project has it and whether the README covers it.

property status: str

n/a when absent from the project, else ok or warn.

epythet.agentic_readme.MARKER_START = '<!-- epythet:agentic-readme:start -->'

The comments that delimit the generated section in a README (each on its own line).

epythet.agentic_readme.MAX_BLURB = 140

Longest blurb (first sentence of a description) shown per skill or agent.

epythet.agentic_readme.NEUTRAL_INTRO = 'If you are a human'

The opener used when humor is off.

class epythet.agentic_readme.Project(root, config, artifacts, readme, user_config, policy, project_overrides)[source]

Bases: object

What every entry point needs once: config, artifacts, README, effective policy.

epythet.agentic_readme.README_NAMES = ('README.md', 'readme.md', 'README.markdown', 'README.rst', 'README.txt', 'README')

README filenames, in order of preference.

class epythet.agentic_readme.ReadmeReport(project_dir, readme, checks, policy, user_config, project_overrides)[source]

Bases: object

The outcome of check_readme() for one project.

policy is the effective ReadmePolicy; user_config and project_overrides are where it came from.

property status: str

warn when anything present is undocumented, else ok.

table()[source]

The plain-text listing (the default CLI output).

Return type:

str

to_dict()[source]

A JSON-ready view (--format json).

Return type:

dict

property warnings: tuple[KindCheck, ...]

The kinds present in the project but missing from the README.

epythet.agentic_readme.SECTION_FIELDS = frozenset({'docs_block', 'for_humans_intro', 'heading', 'humans_link', 'instructions_block', 'marker_end', 'marker_start', 'name', 'repo_stub', 'site_url', 'skills_block', 'subagents_block'})

The fields a section snippet may use.

epythet.agentic_readme.SECTION_SNIPPET = 'agentic-readme-section'

The snippet names the section is rendered from. A project whose only agentic aspect is its agent-readable documentation gets the shorter docs-only variant: it ships no tooling, so the section must not say it does.

exception epythet.agentic_readme.SectionError[source]

Bases: ValueError

The README or a snippet is in a state the tool will not write over.

Raised for unpaired or repeated markers, a README that is not UTF-8 or not Markdown, and a section snippet that fails to format or drops the markers.

epythet.agentic_readme.ai_readme_check(project_dir, *, format='table', fail_on='', draft=False, write=False)[source]

Report whether the README documents the project’s agentic aspects; draft or write the section.

Reuses epythet ai-artifacts discovery (skills, subagents, instruction files) plus the agent-readable outputs the site publishes, and looks for each in the README: a gh skill install line or skill name, a subagent name, CLAUDE.md / AGENTS.md, llms.txt / <package>.md, and a heading about agents (or epythet’s own section markers). The effective policy (~/.config/epythet/config.toml [readme], overridden by the project’s [tool.epythet.readme]) is part of the output so a skill can read it. --write is explicit: it writes whatever the policy says.

Parameters:
  • project_dir – the project root

  • format (str) – table (human) or json

  • fail_on (str) – warn to exit 1 when anything present is undocumented (default: exit 0)

  • draft (bool) – print the README section rendered from the effective snippets and policy, without writing

  • write (bool) – add or update the section in README.md between epythet’s markers

epythet.agentic_readme.blurb(description, *, max_length=140)[source]

The first sentence of a skill or agent description, short enough for a table cell.

Return type:

str

>>> blurb("Find and fix things. Use when asked to fix.")
'find and fix things'
>>> blurb("Do the thing and then some: a, b, c, " + "and more " * 30)
'do the thing and then some'
epythet.agentic_readme.check_readme(project_dir, *, config=None, artifacts=None, user_config=None)[source]

Which agentic aspects the project has, and whether its README mentions each.

Parameters:
  • config – the DocsConfig (loaded when omitted)

  • artifacts (AIArtifacts | None) – discovery result (computed when omitted)

  • user_config (UserConfig | None) – the user’s policy (read from the config dir when omitted)

Return type:

ReadmeReport

epythet.agentic_readme.draft_section(project_dir, *, user_config=None, config=None, artifacts=None)[source]

Render the section for a project as it would be placed: (section, readme_text, start, end).

readme_text is the current README ("" when none exists); start and end delimit the span the section replaces.

Raises:

SectionError – on unpaired markers, a non-UTF-8 README, or a broken snippet

Return type:

tuple[str, str, int, int]

epythet.agentic_readme.find_readme(project_dir)[source]

The project’s README, by the usual names (None when there is none).

Return type:

Path | None

epythet.agentic_readme.github_anchor(title)[source]

GitHub’s anchor for a heading title.

Return type:

str

>>> github_anchor("For AI agents"), github_anchor("What it *fixes*: `x`")
('for-ai-agents', 'what-it-fixes-x')
>>> github_anchor("my_function and [links](https://x)")
'my_function-and-links'
epythet.agentic_readme.headings_of(text)[source]

Every ATX heading outside fenced code blocks.

Return type:

list[Heading]

>>> [h.title for h in headings_of("# A\n```\n# not one\n```\n## B\n")]
['A', 'B']
epythet.agentic_readme.headline_skill(skills)[source]

The skill named in the install line: a *-setup-like one if any, else the first installable.

[Title](#anchor) for the heading after the section, or a plain fallback.

Return type:

str

epythet.agentic_readme.instruction_text(*, snippets=<function snippet_text>)[source]

The instruction the skill hands an agent when the policy is add.

Return type:

str

epythet.agentic_readme.load(project_dir, *, config=None, artifacts=None, user_config=None)[source]

Resolve a project once; each argument given is used instead of being loaded.

Return type:

Project

epythet.agentic_readme.load_project(project_dir)[source]

(config, artifacts) for a project; config is None for a non-Python tree.

A tree without pyproject.toml or setup.cfg is inspected without a config. A tree that has one but cannot be loaded raises ConfigError: a broken [tool.epythet] must not silently change what gets written.

epythet.agentic_readme.marker_span(text, *, strict=True)[source]

The character span of the marked section (None when there is none).

Markers count only on their own line outside fenced code, so a README that shows them in an example is not mistaken for one that has the section.

Parameters:

strict (bool) – raise SectionError on an unpaired or repeated marker (False: report such a README as having no section)

Return type:

tuple[int, int] | None

epythet.agentic_readme.place_section(text, *, agentic_first)[source]

Where the section goes in text: (start, end, next_heading, level).

text[start:end] is the span to replace: the existing marked section when there is one (its position is kept, wherever the author moved it), else an empty span right before the first heading after the title (agentic_first) or at the end of the file. next_heading is the heading that follows the span (the “for humans” target) and level the heading level the section should use to sit among its siblings. Headings inside the existing section are ignored, so rewriting never changes the level.

Raises:

SectionError – on unpaired markers

Return type:

tuple[int, int, Heading | None, int]

epythet.agentic_readme.read_readme(path)[source]

(text, newline): the README with \n line ends, and the style to write back.

Raises:

SectionError – when the file is not UTF-8

Return type:

tuple[str, str]

epythet.agentic_readme.render_section(artifacts, config, *, policy, level=1, humans_link='the top of the page', snippets=<function snippet_text>, agent='claude-code')[source]

The README section for artifacts, from the effective snippets and policy.

Parameters:
  • level (int) – heading level (1 renders # For AI agents)

  • humans_link (str) – what the “for humans” sentence points at

  • snippets (Callable[[str], str]) – name -> text resolver (the seam tests use to inject text)

  • agent (str) – the host named in the gh skill install line

Raises:

SectionError – when the section snippet fails to format or drops a marker

Return type:

str

epythet.agentic_readme.section_snippet_for(artifacts)[source]

The name of the section snippet artifacts calls for.

SECTION_SNIPPET when the project ships tooling, else the shorter DOCS_ONLY_SECTION_SNIPPET, which makes no “ships tooling” claim.

Return type:

str

epythet.agentic_readme.ships_tooling(artifacts)[source]

Whether the project ships anything an agent installs or reads as instructions.

Skills, subagents and instruction files count; published agent-readable documentation (llms.txt, <package>.md) does not, because it is a view of the docs rather than tooling.

Return type:

bool

epythet.agentic_readme.splice_section(text, section, *, start, end)[source]

text with section in place of text[start:end], blank lines kept sane.

Return type:

str

epythet.agentic_readme.write_section(project_dir, *, user_config=None)[source]

Add or update the marked section in the project’s README; returns (path, outcome).

outcome is added, updated or unchanged. The README must be Markdown (README.md); a missing README is created with the section alone. Line endings are kept as found (CRLF stays CRLF).

Raises:

SectionError – when there is nothing agentic to document, the README is not Markdown or not UTF-8, it has unpaired markers, or the snippet is broken

Return type:

tuple[Path, str]