Download the PHP package icebear/yac without Composer
On this page you can find all versions of the php package icebear/yac. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package yac
YAC: Yeet Agent Comments
YAC keeps comments written by coding agents out of your PHP source code.
Agents often leave explanatory comments, implementation notes and reasoning in the files they touch. Some are useful, many are noise, and afterwards nobody can tell them apart from human comments. YAC puts agent notes in a separate layer: JSON sidecars under .yac/ that mirror your source tree. Each note is attached to a statement or declaration by its code, not by a line number, so it survives edits elsewhere in the file.
YAC never decides whether a note is useful. It tracks ownership, location and lifecycle. Humans decide what survives:
Agents that cannot run the CLI can still write inline @yac PHPDoc comments. yac extract moves them into sidecars:
There are no heuristics, classifiers or LLM calls. Everything is deterministic.
Installation
Requirements: PHP 8.2 or newer. Runtime dependencies are symfony/console and nikic/php-parser.
yac init creates .yac/ and adds /.yac/ to .gitignore. Use --no-gitignore if your team wants to commit the notes. Both policies are supported: committed notes become shared, machine-oriented project context. .yac/.gitignore always excludes .lock and /.cache/ (reserved for derived data such as a future SQLite index; sources inside a top-level .cache/ directory therefore cannot be annotated in a committed .yac/). Re-running yac init adds entries missing from older installs.
The project root is the nearest directory, walking up from the working directory, that contains .yac/ or .git; the search never goes above a Git repository root. Commands that do not need to write anything work without .yac/, and add and extract create it on first use.
Quick start
Commands
Every command that takes paths defaults to the current working directory. Paths are relative to the working directory and are matched in their on-disk letter case; an empty path is rejected. A path that does not exist and holds no annotations is reported, and the command exits with 2. Discovery skips .git/, vendor/, node_modules/, .yac/, gitignored files and nested Git repositories (submodules), and only scans .php files.
| Command | What it does |
|---|---|
init [--no-gitignore] |
Create .yac/ with its own .gitignore. Add /.yac/ to .gitignore unless --no-gitignore is given. Running it again only adds missing ignore entries. |
add <file> --anchor=<code> [--comment=<text>] [--scope=<scope>] [--line=<n>] [--format=json] |
Attach a note. The comment is read from stdin when --comment is omitted and stdin is piped. --scope and --line pick one of several matches; --line is never stored. |
list [paths] [--format=json] |
List annotations with their current line and status. |
show <id> [--format=json] |
Show one annotation. |
context <file>... [--stdin] [--format=json] |
Agent-oriented notes for the given files (files only; use list for directories). With --stdin, the single given file is resolved against the text read from stdin instead of the file on disk, so editors can show notes for unsaved changes; the file does not have to exist yet. --stdin with more than one file exits 2 (--stdin takes exactly one file.), before stdin is read. Stdin that is a terminal, or that delivers nothing within 5 seconds, exits 2 (--stdin was given, but nothing could be read from stdin.); stdin that is closed straight away is read as an empty file. |
status [paths] [--format=json] |
Count resolved, orphaned and ambiguous annotations and explain every problem, including invalid sidecars and duplicate IDs. |
check [paths] [--no-annotations] [--allow-inline] [--format=json] |
CI gate (see below). |
remove <id>... [--dry-run] |
Delete annotations. All IDs are looked up first, so one bad ID changes nothing. |
yeet [paths] [--dry-run] |
Delete all annotations for the given paths. The PHP source is not touched. |
promote <id> [--dry-run] [--diff] |
Turn an annotation into a human PHPDoc comment above its anchor and delete the annotation. |
extract [paths] [--dry-run] [--diff] |
Move inline @yac PHPDoc comments into sidecars and remove them from the source. |
inject [paths] [--dry-run] [--diff] |
Move annotations back into the source as inline @yac comments, for agents that cannot run YAC. Each block carries its ID (@yac yac_…), so inject then extract restores the source byte for byte and keeps the IDs. Files that check and extract never scan (not .php, or inside vendor/ and friends) are refused. |
instructions |
Print the agent comment policy below. |
IDs look like yac_01JB8M3Z4X9K7Q2W5E6R8T0Y1V (ULIDs). Any unique prefix of at least yac_ plus 4 characters works, in any letter case.
JSON output
Every --format=json document starts with "schema": 1. Statuses are snake_case: resolved, orphaned, ambiguous, missing_source, unparseable_source, invalid_anchor. Commands that can warn include a warnings list with the same text they print to stderr.
| Command | Fields |
|---|---|
add |
id, source, line, scope |
list |
annotations (list), annotation_count, warnings |
show |
one annotation: id, source, line, status, scope, before, anchor, after, comment, matches (candidate lines), problems |
context |
files (list of source + annotations), warnings |
status |
file_count, annotation_count, counts (per status), problems (unresolved annotations), invalid_sidecars (source, reason), duplicate_ids (id, sources), warnings |
check |
findings (type, source, line, id, detail), finding_count, annotation_count, warnings |
Agent instructions
Paste this into your AGENTS.md, CLAUDE.md or equivalent (vendor/bin/yac instructions prints it too):
How anchoring works
An anchor is PHP code that must match the start of a statement or declaration, compared token by token with whitespace and comments ignored:
- Simple statements are anchored by their full text, e.g.
$entity = $repository->find($id);. - Declarations and control structures are anchored by their header:
final class UserService,public function updateUser(int $id): void,if ($user->isActive()). Leading attributes may be included or left out:public function show(matches a method preceded by#[Route(...)]. - An anchor may be a token-level prefix:
public function updateUser(works too. - Template echo tags work as written:
<?= $title ?>.
add and extract also record the scope (UserService::updateUser; User::$name::get for a property hook, User::__construct::$name::get for a hook on a promoted constructor property) and the neighbouring statements (before and after, cut before the first { so a closure body does not become part of the context). An annotation without a scope belongs to the top level of the file and never matches code inside a class or function; closures do not open a scope.
When the anchor text alone matches several statements in its scope at the time the note is stored, the sidecar records "context": "required". Such a note only resolves while at least one stored neighbour still matches, so deleting the annotated statement orphans the note instead of moving it to an identical statement elsewhere.
When an annotation is resolved against the current source:
| Outcome | When |
|---|---|
resolved |
Exactly one match (siblings break ties between identical statements). A prefix anchor, or one marked "context": "required", also needs at least one stored neighbour to still match. |
orphaned |
No match: the anchor code was edited or deleted, or its scope was renamed. |
ambiguous |
Several matches that the stored context cannot tell apart. |
Resolution is conservative by design. YAC never relocates a note by similarity, never searches outside the stored scope and never guesses: a wrong attachment is worse than an orphan. Moving code, adding lines above it, or reformatting it keeps annotations resolved. Line numbers are never stored; they are computed on display.
An inline comment belongs to YAC only when @yac is the first thing in the PHPDoc and the block contains no other tag (including namespaced annotations such as @\ORM\Column). A human PHPDoc that merely mentions @yac, or one that mixes @yac with @param/@return, is left alone. The rest of the tag line may hold the note's ID (@yac yac_01JB…), which inject writes and extract reuses when it is still free.
Inline comments are anchored as follows during extract:
- A comment followed by code on its line introduces that code.
- A trailing comment (
$x = load(); /** @yac ... */) describes the statement that ends right before it, when that statement also starts on the comment's line; if several do (if ($a) return; /** @yac ... */), the outermost one. A comment that follows the closing brace of another branch (} else { /** @yac ... */) is not attached to anything before it. - A comment on its own line introduces the next statement. Human comments in between are skipped.
When a header is shared by identical statements with identical neighbours, extract lengthens the anchor into the statement body until it is unique (if ($a) { return 3). A comment YAC cannot anchor safely stays inline and is reported (for example one before a closing brace).
Sidecar format
src/Service/UserService.php → .yac/src/Service/UserService.php.yac:
Serialization is deterministic: fixed key order, annotations sorted by ID, a trailing newline, and no rewrite when nothing changed. Keys this version does not know are kept and written back after the known ones, sorted, so newer tools can add fields. A sidecar that becomes empty is deleted. All writes are atomic and guarded by a lock (YAC_LOCK_TIMEOUT_MS overrides the 10 second timeout); extract re-reads every file once it holds the lock, so edits made while it waited are kept.
Notes must be valid UTF-8, must not contain */, and no line may start with a PHPDoc tag (@return in a promoted note would change what PHPStan reads).
Source safety
Commands that change PHP source (extract, promote, inject) only splice byte ranges. They never reformat or regenerate code. Before any write, the result is re-tokenized: if anything other than whitespace and doc comments changed, nothing is written. Human // and /* */ comments are protected by the same check. Writes go through a temporary file and an atomic rename that keeps file permissions. --diff prints a unified diff that applies cleanly with git apply.
CI
check reports:
- invalid sidecars;
- missing or unparseable source files (a file whose letter case changed counts as missing, because Linux CI would not find it);
- duplicate IDs;
- orphaned, ambiguous and invalid annotations;
- inline
@yaccomments that have not been extracted (skip this with--allow-inline).
Valid annotations are not a problem. Use --no-annotations to also fail when any annotation exists, for example before a release if your team treats .yac/ as temporary. Invalid sidecars are reported as findings and make check exit with 2, because the annotations in them could not be checked.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success, or healthy. |
1 |
A problem you can act on: check findings, an unknown or ambiguous ID, an anchor that is not found or not unique, a source file add or promote cannot read or parse, a refused promotion, comments extract left inline, annotations inject skipped. |
2 |
The command could not do its job: an I/O error, lock timeout, invalid sidecar (also in status), invalid usage (a missing or malformed option, an empty path, a directory passed to context), or an incomplete scan (unreadable or binary files, unreadable directories, missing paths). |
Development
This repository uses YAC for its own agent notes: see AGENTS.md and .yac/.
Non-goals
YAC deliberately does not detect unmarked AI comments, rewrite or score comments, call LLMs, relocate notes by similarity, attribute notes to models, integrate with Git or GitHub, or run as a daemon. The PhpStorm plugin renders notes next to the code and runs the actions from the editor through this CLI (context --stdin for unsaved changes); the CLI and the sidecar format remain the canonical implementation.
License
MIT, see LICENSE.