Skip to content
Open
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
52 changes: 52 additions & 0 deletions .agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Agent documentation

Working notes about how hls4ml is built, written to be read by an AI coding assistant and by anyone new to
the codebase. [AGENTS.md](../AGENTS.md) in the repository root is the short version that agents read first;
these documents are the detail it points to.

They are not a substitute for [CONTRIBUTING.md](../CONTRIBUTING.md), which states the rules contributions
have to follow. Where the two appear to disagree, CONTRIBUTING.md wins.

## The documents

| Document | Read it when |
| --- | --- |
| [running-hls4ml.md](running-hls4ml.md) | you need to convert a model, run `predict()`, or check that a change works |
| [architecture-map.md](architecture-map.md) | before changing anything in the Python tree — what each stage owns and which file to open |
| [frontends.md](frontends.md) | adding support for a layer or operator, or a model fails to parse |
| [optimizer-passes.md](optimizer-passes.md) | changing the graph, adding a Strategy, a layer initializer or a config attribute |
| [kernels.md](kernels.md) | writing or modifying the C++ compute kernels |
| [precision-and-debugging.md](precision-and-debugging.md) | choosing fixed-point types, or the numerical result is wrong |
| [evaluating-implementations.md](evaluating-implementations.md) | claiming one implementation is faster or smaller than another |
| [new-backend.md](new-backend.md) | standing up a backend for a new toolchain |
| [toolchain-access.md](toolchain-access.md) | synthesizing, choosing a tool version, or a build cannot find its tool |
| [contributing-changes.md](contributing-changes.md) | shaping a change, and again before opening a pull request |
| [reporting-issues.md](reporting-issues.md) | reporting a bug, a performance problem, or proposing a feature |
| [local-setup.template.md](local-setup.template.md) | recording how your own machine is set up |

`frontends.md` ends with an end-to-end checklist for adding a layer, which sequences the others.

## Using these with your assistant

Each document carries a short front matter block with a `name`, a `description` saying when it applies, and
`globs` listing the paths it covers. Different assistants consume that differently, so the files are kept in
one neutral place and adapted rather than duplicated:

```
python .agents/agent_adapters.py --list # what can be generated
python .agents/agent_adapters.py claude cursor # generate those views
```

Generated views are ignored by git. Nothing prevents you from pointing your assistant at `.agents/` directly —
the files are plain Markdown and the front matter is harmless.

## Keeping them true

These describe mechanisms that change. Two rules keep them from rotting:

- A change to the machinery a document describes updates that document in the same pull request.
- `test/pytest/test_agent_docs.py` checks that every repository path mentioned in these files still exists.
It runs in the normal test suite; a renamed module makes it fail.

Statements should be checkable. Prefer naming the file that proves a claim over asserting it, and do not
record numbers from a specific machine or a specific project — the point is what stays true.
152 changes: 152 additions & 0 deletions .agents/agent_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Generate assistant-specific views of the documents in .agents/.

The documents in .agents/ are the single source of truth. Assistants disagree about where such files live and
what metadata they carry, so this script writes copies in the layout each one expects. Generated output is
git-ignored; edit .agents/ and regenerate.

python .agents/agent_adapters.py --list
python .agents/agent_adapters.py claude cursor
python .agents/agent_adapters.py --all --clean

Adding an adapter is a matter of writing one function and adding it to ADAPTERS. Please do not add
assistant-specific content to the source documents themselves.
"""

from __future__ import annotations

import argparse
import re
import shutil
from pathlib import Path

SOURCE = Path(__file__).resolve().parent
REPO = SOURCE.parent
SKIP = {'README.md', 'local-setup.template.md'}


def read_docs() -> list[dict]:
"""Return the parsed front matter and body of every source document."""
docs = []
for path in sorted(SOURCE.glob('*.md')):
if path.name in SKIP:
continue
text = path.read_text()
match = re.match(r'^---\n(.*?)\n---\n(.*)$', text, re.S)
if not match:
raise SystemExit(f'{path}: missing front matter')
front, body = match.group(1), match.group(2)

name = re.search(r'^name:\s*(.+)$', front, re.M)
description = re.search(r'^description:\s*>-\n((?:\s{2,}.*\n?)+)', front, re.M)
globs = re.findall(r'^\s+-\s*"(.+)"$', front, re.M)
if not name or not description:
raise SystemExit(f'{path}: front matter needs a name and a description')

docs.append(
{
'stem': path.stem,
'name': name.group(1).strip(),
'description': ' '.join(line.strip() for line in description.group(1).splitlines()),
'globs': globs,
'body': body.lstrip('\n'),
'path': path,
}
)
return docs


def adapt_claude(docs: list[dict]) -> Path:
"""Claude Code: .claude/skills/<name>/SKILL.md, dispatched by description."""
out = REPO / '.claude' / 'skills'
for doc in docs:
directory = out / doc['name']
directory.mkdir(parents=True, exist_ok=True)
body = doc['body'].replace('](', '](../../../.agents/')
front = f'---\nname: {doc["name"]}\ndescription: {doc["description"]}\n---\n\n'
(directory / 'SKILL.md').write_text(front + body)
return out


def adapt_cursor(docs: list[dict]) -> Path:
"""Cursor: .cursor/rules/<name>.mdc, activated by glob."""
out = REPO / '.cursor' / 'rules'
out.mkdir(parents=True, exist_ok=True)
for doc in docs:
globs = ', '.join(doc['globs'])
front = f'---\ndescription: {doc["description"]}\nglobs: {globs}\nalwaysApply: false\n---\n\n'
(out / f'{doc["stem"]}.mdc').write_text(front + doc['body'])
return out


def adapt_copilot(docs: list[dict]) -> Path:
"""GitHub Copilot: .github/instructions/<name>.instructions.md, applied by path."""
out = REPO / '.github' / 'instructions'
out.mkdir(parents=True, exist_ok=True)
for doc in docs:
apply_to = ','.join(doc['globs']) or '**'
front = f"---\napplyTo: '{apply_to}'\n---\n\n"
(out / f'{doc["stem"]}.instructions.md').write_text(front + f'<!-- {doc["description"]} -->\n\n' + doc['body'])
return out


def adapt_plain(docs: list[dict]) -> Path:
"""A single concatenated file, for assistants that take one document."""
out = REPO / 'agent-docs.md'
parts = ['# hls4ml agent documentation\n', '<!-- Generated from .agents/ by .agents/agent_adapters.py -->\n']
for doc in docs:
parts.append(f'\n\n---\n\n<!-- when: {doc["description"]} -->\n\n{doc["body"]}')
out.write_text(''.join(parts))
return out


ADAPTERS = {
'claude': adapt_claude,
'cursor': adapt_cursor,
'copilot': adapt_copilot,
'plain': adapt_plain,
}


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('adapters', nargs='*', metavar='ADAPTER', help=f'any of: {", ".join(ADAPTERS)}')
parser.add_argument('--all', action='store_true', help='generate every view')
parser.add_argument('--list', action='store_true', help='list the available views and exit')
parser.add_argument('--clean', action='store_true', help='remove generated output first')
args = parser.parse_args()

if args.list:
for name, func in ADAPTERS.items():
print(f'{name:10s} {func.__doc__.splitlines()[0]}')
return

selected = list(ADAPTERS) if args.all else args.adapters
if not selected:
parser.error('name at least one adapter, or pass --all (see --list)')
unknown = [name for name in selected if name not in ADAPTERS]
if unknown:
parser.error(f'unknown adapter(s): {", ".join(unknown)} (see --list)')

docs = read_docs()
for name in selected:
if args.clean:
target = (
REPO
/ {
'claude': '.claude/skills',
'cursor': '.cursor/rules',
'copilot': '.github/instructions',
'plain': 'agent-docs.md',
}[name]
)
if target.is_dir():
shutil.rmtree(target)
elif target.exists():
target.unlink()
written = ADAPTERS[name](docs)
print(f'{name}: wrote {len(docs)} documents to {written.relative_to(REPO)}')


if __name__ == '__main__':
main()
Loading
Loading