Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions epythet/agentic_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@
)
#: The kinds a check reports on, in display order.
KINDS = ("skills", "subagents", "instruction_files", "agent_docs", "section")
#: The snippet names the section is rendered from.
#: 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.
SECTION_SNIPPET = "agentic-readme-section"
DOCS_ONLY_SECTION_SNIPPET = "agentic-readme-section-docs-only"
HUMOR_SNIPPET = "agentic-readme-humor"
INSTRUCTION_SNIPPET = "agentic-readme-instruction"
#: The opener used when ``humor`` is off.
Expand Down Expand Up @@ -599,7 +602,8 @@ def render_section(
name = config.name if config is not None else artifacts.project_dir.name
repo_stub = repo_stub_for(config.repo_url) if config is not None else ""
site_url = site_url_for(config.repo_url) if config is not None else ""
template = snippets(SECTION_SNIPPET)
snippet_name = section_snippet_for(artifacts)
template = snippets(snippet_name)
fields = dict(
marker_start=MARKER_START,
marker_end=MARKER_END,
Expand All @@ -618,18 +622,39 @@ def render_section(
rendered = template.format(**fields)
except Exception as e: # str.format raises Key/Index/Value/Attribute/TypeError
raise SectionError(
f"snippet {SECTION_SNIPPET!r} does not format: {e!r}; the fields are "
f"snippet {snippet_name!r} does not format: {e!r}; the fields are "
f"{sorted(SECTION_FIELDS)} and literal braces must be doubled ({{{{ and }}}})"
) from e
rendered = rendered.strip("\n") + "\n"
if marker_span(rendered, strict=False) != (0, len(rendered)):
raise SectionError(
f"snippet {SECTION_SNIPPET!r} must start with {{marker_start}} and end with "
f"snippet {snippet_name!r} must start with {{marker_start}} and end with "
"{marker_end}, each on its own line, or the section cannot be updated in place"
)
return rendered


def ships_tooling(artifacts: AIArtifacts) -> bool:
"""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 bool(
artifacts.skills or artifacts.subagents or artifacts.instruction_files
)


def section_snippet_for(artifacts: AIArtifacts) -> str:
"""The name of the section snippet ``artifacts`` calls for.

:data:`SECTION_SNIPPET` when the project ships tooling, else the shorter
:data:`DOCS_ONLY_SECTION_SNIPPET`, which makes no "ships tooling" claim.
"""
return SECTION_SNIPPET if ships_tooling(artifacts) else DOCS_ONLY_SECTION_SNIPPET


def _for_humans_intro(name: str, policy: ReadmePolicy, pool_text: str) -> str:
"""The opener: a stable pick from the humour pool when ``humor`` is on, else neutral."""
lines = pool_lines(pool_text) if policy.humor else []
Expand Down
4 changes: 4 additions & 0 deletions epythet/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ def _resolve_repo_stub(repo):
#: still become ``nargs="*"`` (``--ignore a b``), not a single value.
CONVENTION = dataclasses.replace(cw.ARGH, resolve_hints=True)

#: ``--ignore a --ignore b`` accumulates on every command that takes it (argparse
#: would keep only the last flag); ``validate`` and ``repair`` declare the same.
quickstart._cw = {"params": {"ignore": {"action": "extend", "nargs": "*"}}}


def mk_epythet_parser(**parser_kwargs):
"""The full ``epythet`` parser: the flat commands, the tool commands, the ``ledger`` group."""
Expand Down
5 changes: 3 additions & 2 deletions epythet/data/skills/epythet-agentic-readme/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ agentic_first = true

## Changing the wording: snippets

Three snippets render the section. The user's copy in `<config dir>/snippets/<name>.md` wins over the packaged default in `epythet/data/snippets/`:
Four snippets render the section. The user's copy in `<config dir>/snippets/<name>.md` wins over the packaged default in `epythet/data/snippets/`:

| Snippet | What it is |
|---|---|
| `agentic-readme-section` | the section template (`str.format` fields: `{marker_start}`, `{marker_end}`, `{heading}`, `{name}`, `{repo_stub}`, `{site_url}`, `{skills_block}`, `{subagents_block}`, `{instructions_block}`, `{docs_block}`, `{for_humans_intro}`, `{humans_link}`; literal braces doubled) |
| `agentic-readme-section` | the section template for a project that ships skills, subagents or instruction files (`str.format` fields: `{marker_start}`, `{marker_end}`, `{heading}`, `{name}`, `{repo_stub}`, `{site_url}`, `{skills_block}`, `{subagents_block}`, `{instructions_block}`, `{docs_block}`, `{for_humans_intro}`, `{humans_link}`; literal braces doubled) |
| `agentic-readme-section-docs-only` | the shorter template used when the project's only agentic aspect is its published agent-readable documentation (`llms.txt`, `<package>.md`); same fields, no "ships tooling" claim |
| `agentic-readme-humor` | the pool of openers for the "for humans" sentence, one per line, `#` comments allowed |
| `agentic-readme-instruction` | what step 2 tells the agent when the policy is `add` |

Expand Down
13 changes: 7 additions & 6 deletions epythet/data/skills/epythet-docstring-style/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,21 @@ What epythet's normalizer repairs at build time (so existing code renders, not s

**Tier 3, complex class**: everything in Tier 2 on the class docstring (not `__init__`); `Attributes:` with invariants; a lifecycle sketch (construct, configure, use, tear down) as a doctest; state invariants (what mutates, what is safe to reuse); methods at Tier 1 or 2. Target: 40 to 80 lines on the class.

**Module docstring** (required on every non-underscore module): one line on purpose; two to four sentences of intent and how the module relates to the package; a curated `Main entry points:` block naming the 2 to 5 things to start with (a curation, not an inventory); one minimal doctest.
**Module docstring** (required on every non-underscore module): one line on purpose; two to four sentences of intent and how the module relates to the package; a curated `Main entry points:` line followed by a blank line and a bullet list naming the 2 to 5 things to start with (a curation, not an inventory); one minimal doctest. The blank line matters: `Main entry points:` directly over an indented block is an RST definition list, which `epythet validate` reports as DR014.

```python
"""<One line: what this module is for.>

<2-4 sentences: the problem it solves, the mental model, its place in the package.>

Main entry points:
<name>: <one clause>
<name>: <one clause>

>>> from pkg.module import main_thing
>>> main_thing([1, 2, 3])
6
- ``<name>``: <one clause>
- ``<name>``: <one clause>

>>> from pkg.module import main_thing
>>> main_thing([1, 2, 3])
6
"""
```

Expand Down
2 changes: 1 addition & 1 deletion epythet/data/snippets/agentic-readme-instruction.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Add the agentic aspects to this README. With `agentic_first` on, agents get their section before the humans get theirs; otherwise it closes the README.

Do not hand-write the section. Run `epythet ai-readme-check <project_dir> --write` so it lands between epythet's marker comments and a later run updates it in place. To change the wording, change the snippets, not the README: `epythet snippets init`, then edit `agentic-readme-section.md` and `agentic-readme-humor.md` in your snippets folder.
Do not hand-write the section. Run `epythet ai-readme-check <project_dir> --write` so it lands between epythet's marker comments and a later run updates it in place. To change the wording, change the snippets, not the README: `epythet snippets init`, then edit `agentic-readme-section.md` (a project that ships skills, subagents or instruction files), `agentic-readme-section-docs-only.md` (a project whose only agentic aspect is its agent-readable documentation) and `agentic-readme-humor.md` in your snippets folder.

Then read the result as a stranger would. The humour is light and stays on the writer's side of the joke. No em-dashes, no "not X but Y", no closing paragraph that restates the section. Every link resolves. If the README already covers the same artifacts under a heading of its own, keep that heading only if it says something the generated section does not; otherwise remove it, so the two never disagree.
7 changes: 7 additions & 0 deletions epythet/data/snippets/agentic-readme-section-docs-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{marker_start}
{heading} For AI agents

`{name}` publishes its documentation in forms made for coding agents. If you are one, start here.
{docs_block}
{for_humans_intro}, the rest of this README is written for you, starting at {humans_link}.
{marker_end}
19 changes: 19 additions & 0 deletions epythet/ledger/rules/rendering/DR014.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,22 @@ def good_real_definition_list(): # ok: DR014
term
definition of the term
"""


def bad_entry_points_over_indented_block(): # ruleid: DR014
"""Do a thing.

Main entry points:
thing: the one to start with
other: the second one
"""


def good_entry_points_blank_line_then_list(): # ok: DR014
"""Do a thing.

Main entry points:

- ``thing``: the one to start with
- ``other``: the second one
"""
Loading
Loading