test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std ;
import Crafter . Build ;
namespace fs = std : : filesystem ;
using namespace Crafter ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// Dependency notes: a consumer with an AGENTS.template.md beside its
// project.cpp has every build and test run write AGENTS.md from it, with the
// AGENTS.consumer.md each dependency ships merged into a marked section.
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
//
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// The template is the user's own file, committed and edited by hand, and
// AGENTS.md is build output that stays out of version control: notes kept in
// a committed AGENTS.md changed it under every diff. So what this guards
// against is the generated file leaking into the user's: the template
// written, an AGENTS.md someone wrote replaced (one committed before the
// template was, or one beside no template at all), one written although it
// is read-only, or replaced so that a hard link to it goes stale; and inside
// AGENTS.md, the template's text changed or the section misplaced, a write
// when nothing changed (an editor reloading), notes from a test's dependency
// or a dependency's own host tooling leaking into the consumer's, and `lint`,
// `run` or `test --list`, which promise not to touch the project, writing it
// anyway.
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
namespace {
std : : int32_t Failures = 0 ;
void Check ( bool cond , std : : string_view msg ) {
if ( ! cond ) {
std : : println ( std : : cerr , " FAIL: {} " , msg ) ;
+ + Failures ;
}
}
std : : string Read ( const fs : : path & file ) {
std : : ifstream in ( file , std : : ios : : binary ) ;
return std : : string ( ( std : : istreambuf_iterator < char > ( in ) ) , std : : istreambuf_iterator < char > ( ) ) ;
}
void Write ( const fs : : path & file , std : : string_view content ) {
std : : ofstream ( file , std : : ios : : binary | std : : ios : : trunc ) < < content ;
}
std : : size_t Count ( std : : string_view text , std : : string_view needle ) {
std : : size_t count = 0 ;
for ( std : : size_t pos = text . find ( needle ) ; pos ! = std : : string_view : : npos ; pos = text . find ( needle , pos + needle . size ( ) ) ) + + count ;
return count ;
}
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
// MergeDependencyNotes with a throw counted as a failure rather than
// ending the run, so the cases after it are still reported.
std : : string Merge ( std : : string_view doc , std : : span < const DependencyNote > notes ) {
try {
return MergeDependencyNotes ( doc , notes ) ;
} catch ( const std : : exception & e ) {
std : : println ( std : : cerr , " FAIL: MergeDependencyNotes threw: {} " , e . what ( ) ) ;
+ + Failures ;
return std : : format ( " <threw: {}> " , e . what ( ) ) ;
}
}
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
// Expect MergeDependencyNotes to throw; the message, or "" when it didn't.
std : : string MergeFails ( std : : string_view doc , std : : span < const DependencyNote > notes ) {
try {
( void ) MergeDependencyNotes ( doc , notes ) ;
} catch ( const std : : exception & e ) {
return e . what ( ) ;
}
return " " ;
}
std : : int32_t RunWith ( std : : vector < std : : string > args ) {
std : : vector < char * > argv ;
for ( std : : string & arg : args ) argv . push_back ( arg . data ( ) ) ;
return Run ( static_cast < std : : int32_t > ( argv . size ( ) ) , argv . data ( ) ) ;
}
// LoadProject compiles a project file against the installed module
// sources; without them the end-to-end half cannot run at all.
bool ModuleSourcesInstalled ( ) {
try {
return fs : : exists ( GetCrafterBuildHome ( ) / " Crafter.Build.cppm " ) ;
} catch ( const std : : exception & ) {
return false ;
}
}
constexpr std : : string_view BeginMarker = " <!-- crafter-build: dependency notes begin --> " ;
constexpr std : : string_view EndMarker = " <!-- crafter-build: dependency notes end --> " ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
constexpr std : : string_view GeneratedLine = " <!-- Written by crafter-build from AGENTS.template.md, and rewritten on every build. Edit that file, not this one. --> " ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
}
int main ( ) {
// The merge on its own: what goes where in a document, with no project.
const std : : string original = " # app \n \n The consumer's own text. \n " ;
const std : : vector < DependencyNote > two = {
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
{ . name = " one " , . text = " ## one \n \n First. " } ,
{ . name = " two " , . text = " ## two \n \n Second. " } ,
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
} ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : string merged = Merge ( original , two ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( merged . starts_with ( std : : format ( " {} \n {} \n " , original , BeginMarker ) ) , " the section is appended after one blank line, and the text before it is untouched " ) ;
Check ( merged . ends_with ( std : : format ( " {} \n " , EndMarker ) ) , " the section ends with its end marker " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : size_t first = merged . find ( " <!-- from one --> \n ## one \n \n First. \n " ) ;
const std : : size_t second = merged . find ( " <!-- from two --> \n ## two \n \n Second. \n " ) ;
Check ( first ! = std : : string : : npos & & second ! = std : : string : : npos & & first < second , " each note follows a line naming its dependency, in the order given " ) ;
Check ( Merge ( merged , two ) = = merged , " merging the same notes again changes nothing " ) ;
Check ( Merge ( merged , { } ) = = original , " removing the section gives the file its own bytes back " ) ;
Check ( Merge ( original , { } ) = = original , " a file with no section and no notes is left alone " ) ;
// A section the user moved is rewritten where it stands, and removing it
// keeps the paragraphs around it apart.
const std : : string section = merged . substr ( original . size ( ) + 1 ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
{
const std : : string moved = std : : format ( " # app \n \n {} \n ## After \n \n The user's text after it. \n " , section ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : vector < DependencyNote > changed = { { . name = " one " , . text = " ## one \n \n Changed. " } } ;
const std : : string rewritten = Merge ( moved , changed ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( rewritten . starts_with ( std : : format ( " # app \n \n {} \n " , BeginMarker ) ) , " the text before a moved section is kept " ) ;
Check ( rewritten . ends_with ( " \n ## After \n \n The user's text after it. \n " ) , " the text after a moved section is kept " ) ;
Check ( rewritten . contains ( " Changed. " ) & & ! rewritten . contains ( " First. " ) & & ! rewritten . contains ( " Second. " ) , " a rewritten section holds exactly the new notes " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : string between = std : : format ( " Para one. \n \n {}Para two. \n " , section ) ;
Check ( Merge ( between , { } ) = = " Para one. \n \n Para two. \n " , " removing a section from between two paragraphs keeps them apart " ) ;
// An unclosed block after the section does not stop it being rewritten
// in place, since the section is found before it.
const std : : string open = std : : format ( " {}``` \n never closed \n " , merged ) ;
Check ( MergeFails ( open , changed ) . empty ( ) & & Merge ( open , changed ) . contains ( " Changed. " ) , " a section before an unclosed code block is still rewritten " ) ;
}
// A CRLF file gets a CRLF section and keeps it.
{
const std : : string crlf = " # app \r \n \r \n Text. \r \n " ;
const std : : string withNotes = Merge ( crlf , two ) ;
Check ( withNotes . starts_with ( std : : format ( " {} \r \n {} \r \n " , crlf , BeginMarker ) ) & & Count ( withNotes , " \n " ) = = Count ( withNotes , " \r \n " ) , " a CRLF file gets a section with CRLF line ends " ) ;
Check ( Merge ( withNotes , two ) = = withNotes & & Merge ( withNotes , { } ) = = crlf , " a CRLF section is found again and removed cleanly " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
}
// Markers quoted in a code block are not the section's, including a block
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
// that quotes a fenced block of its own; and only real fences count as
// code blocks.
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
{
const std : : string quoted = std : : format ( " # app \n \n ````markdown \n {} \n ``` \n nested \n ``` \n {} \n ```` \n " , BeginMarker , EndMarker ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : string withNotes = Merge ( quoted , two ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( withNotes . starts_with ( std : : format ( " {} \n {} \n " , quoted , BeginMarker ) ) , " markers quoted in a code block are left alone, and a section is appended after them " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
Check ( Merge ( withNotes , two ) = = withNotes , " the real section is found past the quoted markers " ) ;
// ```make``` is inline code: read as a fence, it would open a block
// that hides everything after it.
const std : : string inlineCode = " # app \n \n ```make``` builds everything. \n " ;
Check ( MergeFails ( inlineCode , two ) . empty ( ) , " a backtick run with a backtick after it does not open a code block " ) ;
const std : : string once = Merge ( inlineCode , two ) ;
Check ( Merge ( once , two ) = = once & & Count ( once , BeginMarker ) = = 1 , " a section after inline code is found again " ) ;
// A fence inside a list item, closed at the item's indentation. Read
// without the list, the indented closer would open a block instead,
// and the quoted markers after it would be taken for the section.
const std : : string listed = std : : format ( " - ```bash \n x \n ``` \n \n ``` \n {} \n The user's quoted text. \n {} \n ``` \n " , BeginMarker , EndMarker ) ;
const std : : string afterList = Merge ( listed , two ) ;
Check ( afterList . starts_with ( std : : format ( " {} \n {} \n " , listed , BeginMarker ) ) , " a fence in a list item closes at the item's indentation " ) ;
Check ( Merge ( afterList , two ) = = afterList , " the section after a list item's fence is found again " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
}
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
// With notes to write, guessing which part of someone's file is generated
// would overwrite their text, and missing a section would add another on
// every build.
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( MergeFails ( std : : format ( " # app \n {} \n text \n " , BeginMarker ) , two ) . contains ( " line 2 " ) , " a begin marker with no end marker is refused, naming its line " ) ;
Check ( MergeFails ( std : : format ( " # app \n \n {} \n " , EndMarker ) , two ) . contains ( " line 3 " ) , " an end marker with no begin marker is refused " ) ;
Check ( MergeFails ( std : : format ( " {} \n {} \n " , EndMarker , BeginMarker ) , two ) . contains ( " line 1 " ) , " an end marker before the begin marker is refused " ) ;
Check ( MergeFails ( std : : format ( " {0} \n {1} \n {0} \n {1} \n " , BeginMarker , EndMarker ) , two ) . contains ( " line 3 " ) , " a second section is refused " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
Check ( MergeFails ( " # app \n \n ``` \n never closed \n " , two ) . contains ( " line 3 " ) , " appending to a document that ends inside a code block is refused " ) ;
// With none, leaving the file alone loses nothing, so it is not an error.
const std : : string broken = std : : format ( " # app \n {} \n text \n " , BeginMarker ) ;
Check ( MergeFails ( broken , { } ) . empty ( ) & & Merge ( broken , { } ) = = broken , " with no notes, unbalanced markers leave the file alone without an error " ) ;
Check ( Merge ( " # app \n \n ``` \n never closed \n " , { } ) = = " # app \n \n ``` \n never closed \n " , " with no notes, an unclosed code block is no error " ) ;
const std : : vector < DependencyNote > markerNote = { { . name = " marker-note " , . text = std : : format ( " ## bad \n {} " , EndMarker ) } } ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( MergeFails ( original , markerNote ) . contains ( " marker-note " ) , " a note with a marker line of its own is refused " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : vector < DependencyNote > fenceNote = { { . name = " fence-note " , . text = " ## bad \n ``` \n never closed " } } ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( MergeFails ( original , fenceNote ) . contains ( " fence-note " ) , " a note that leaves a code block open is refused " ) ;
if ( ! ModuleSourcesInstalled ( ) ) {
std : : println ( std : : cerr , " SKIP: no module sources to compile the fixture's project files against " ) ;
return Failures > 0 ? 1 : 77 ;
}
// Through LoadProject and the CLI. Staged, because each case rewrites
// AGENTS.md and every load drops a build/ tree beside its project file.
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const fs : : path home = fs : : current_path ( ) ;
const fs : : path fixture = home / " tests " / " DependencyNotes " / " fixture " ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
const fs : : path scratch = fs : : temp_directory_path ( ) / " crafter-build-dependency-notes " ;
std : : error_code ec ;
fs : : remove_all ( scratch , ec ) ;
fs : : create_directories ( scratch ) ;
fs : : copy ( fixture , scratch , fs : : copy_options : : recursive ) ;
const fs : : path app = scratch / " app " ;
const fs : : path agents = app / " AGENTS.md " ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
const fs : : path templateFile = app / " AGENTS.template.md " ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
const fs : : path projectFile = app / " project.cpp " ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const fs : : path libNotes = scratch / " lib " / " AGENTS.consumer.md " ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// Checked in under another name, so that this repository's tree holds no
// template a build could ever pick up.
const std : : string templateText = Read ( app / " AGENTS.template.md.in " ) ;
Write ( templateFile , templateText ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
// LocalProject resolves against the working directory, and app's paths
// are written for `cd app && crafter-build`.
fs : : current_path ( app ) ;
std : : array < std : : string_view , 0 > noArgs = { } ;
Configuration cfg ;
try {
cfg = LoadProject ( projectFile , noArgs ) ;
} catch ( const std : : exception & e ) {
std : : println ( std : : cerr , " FAIL: LoadProject threw: {} " , e . what ( ) ) ;
return 1 ;
}
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : string toolNote = " <!-- from tool --> \n ## tool \n \n Run tool before committing. \n " ;
const std : : string libNote = " <!-- from lib --> \n ## lib \n \n Call lib through its module, never through its headers. \n " ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
// base's file opens and ends with blank lines, which the section drops.
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
const std : : string baseNote = " <!-- from base --> \n ## base \n \n base counts from zero. \n " ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// How every AGENTS.md written from templateText starts.
const std : : string generated = std : : format ( " {} \n {} " , GeneratedLine , templateText ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( ! fs : : exists ( agents ) & & UpdateAgentsMd ( cfg , projectFile ) , " the first update creates AGENTS.md " ) ;
// Every case after this one reads or rewrites that file.
if ( ! fs : : exists ( agents ) ) return 1 ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
std : : string text = Read ( agents ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( text . starts_with ( std : : format ( " {} \n {} \n " , generated , BeginMarker ) ) , " AGENTS.md is the line saying a build wrote it, the template's text untouched, then the section " ) ;
Check ( Read ( templateFile ) = = templateText , " the template is never written " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
const std : : size_t tool = text . find ( toolNote ) ;
const std : : size_t lib = text . find ( libNote ) ;
const std : : size_t base = text . find ( baseNote ) ;
Check ( tool ! = std : : string : : npos & & lib ! = std : : string : : npos & & base ! = std : : string : : npos , std : : format ( " tool, lib and base each contribute their notes (got: \n {}) " , text ) ) ;
Check ( tool < lib & & lib < base , " host tooling first, then what app names, then what lib builds against " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
Check ( ! text . contains ( " Notes from app " ) , " app's own AGENTS.consumer.md stays out, even beside a dependency's project file " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( ! text . contains ( " libtool " ) , " lib's host tooling stays out of its consumers' notes " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
Check ( ! text . contains ( " testonly " ) , " a test's dependency stays out " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
const fs : : file_time_type written = fs : : last_write_time ( agents ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) , " an update with nothing changed writes nothing " ) ;
Check ( Read ( agents ) = = text & & fs : : last_write_time ( agents ) = = written , " the bytes and the mtime are left alone when nothing changed " ) ;
cfg . dependencyNotes = DependencyNotes : : Direct ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( UpdateAgentsMd ( cfg , projectFile ) , " switching to Direct rewrites AGENTS.md " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
text = Read ( agents ) ;
Check ( text . contains ( toolNote ) & & text . contains ( libNote ) & & ! text . contains ( " ## base " ) , " Direct keeps what app names and drops what only lib does " ) ;
cfg . dependencyNotes = DependencyNotes : : Transitive ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
Write ( libNotes , " ## lib \n \n lib was rewritten. \n " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
Check ( UpdateAgentsMd ( cfg , projectFile ) , " a dependency's edited notes are picked up " ) ;
text = Read ( agents ) ;
Check ( text . contains ( " lib was rewritten. " ) & & ! text . contains ( " never through its headers " ) & & Count ( text , BeginMarker ) = = 1 , " edited notes replace the old ones, in the one section " ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// AGENTS.md follows the template: an edit made there shows up here, and
// the template's marker lines say where the notes go.
Write ( templateFile , std : : format ( " {} \n A paragraph added to the template. \n " , templateText ) ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " an edited template is picked up " ) ;
Check ( Read ( agents ) . starts_with ( std : : format ( " {} \n A paragraph added to the template. \n \n {} \n " , generated , BeginMarker ) ) , " the template's new text goes in ahead of the section " ) ;
Write ( templateFile , std : : format ( " # app \n \n {} \n {} \n \n ## After \n \n Text after the notes. \n " , BeginMarker , EndMarker ) ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " a template that places the section is picked up " ) ;
text = Read ( agents ) ;
Check ( text . starts_with ( std : : format ( " {} \n # app \n \n {} \n " , GeneratedLine , BeginMarker ) ) & & text . contains ( toolNote ) & & text . ends_with ( std : : format ( " {} \n \n ## After \n \n Text after the notes. \n " , EndMarker ) ) , " the section goes where the template's markers are " ) ;
Write ( templateFile , templateText ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) & & Read ( agents ) . starts_with ( generated ) , " AGENTS.md follows the template back " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
// A CLAUDE.md hard-linked to AGENTS.md has to keep reading the same file.
fs : : create_hard_link ( agents , app / " CLAUDE.md " ) ;
Write ( libNotes , " ## lib \n \n lib, through a link. \n " ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " a hard-linked AGENTS.md is updated " ) ;
Check ( Read ( app / " CLAUDE.md " ) = = Read ( agents ) & & Read ( agents ) . contains ( " lib, through a link. " ) & & fs : : hard_link_count ( agents ) = = 2 , " the update is visible through every link to the file " ) ;
fs : : remove ( app / " CLAUDE.md " ) ;
// A read-only AGENTS.md is a lock someone set on purpose. Root writes
// through the permission bits, so the check needs an ordinary user.
fs : : permissions ( agents , fs : : perms : : owner_read | fs : : perms : : group_read | fs : : perms : : others_read ) ;
if ( ! std : : ofstream ( agents , std : : ios : : app ) . is_open ( ) ) {
const std : : string locked = Read ( agents ) ;
Write ( libNotes , " ## lib \n \n lib, locked out. \n " ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) & & Read ( agents ) = = locked , " a read-only AGENTS.md is left alone " ) ;
}
fs : : permissions ( agents , fs : : perms : : owner_write , fs : : perm_options : : add ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// A build's own AGENTS.md is output, an edit made in it included; any
// other text in its place is someone's, and only an empty file is not.
( void ) UpdateAgentsMd ( cfg , projectFile ) ;
const std : : string settled = Read ( agents ) ;
Write ( agents , std : : format ( " {} \n An edit made in AGENTS.md itself. \n " , GeneratedLine ) ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) & & Read ( agents ) = = settled , " an edit made in AGENTS.md is replaced by the next build " ) ;
const std : : string handWritten = " # app \n \n An AGENTS.md committed before the template was. \n " ;
Write ( agents , handWritten ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) & & Read ( agents ) = = handWritten , " an AGENTS.md no build wrote is never replaced " ) ;
Write ( agents , " " ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) & & Read ( agents ) = = settled , " an empty AGENTS.md, all a write cut short leaves of a new one, is replaced " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
cfg . dependencyNotes = DependencyNotes : : None ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " None rewrites AGENTS.md " ) ;
Check ( Read ( agents ) = = generated , " under None, AGENTS.md holds the template's text alone " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
cfg . dependencyNotes = DependencyNotes : : Transitive ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// Linked to the template, AGENTS.md would have every build write the
// file that is committed.
fs : : remove ( agents ) ;
fs : : create_symlink ( " AGENTS.template.md " , agents ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) & & Read ( templateFile ) = = templateText , " an AGENTS.md that links to the template is not written " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
fs : : remove ( agents ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// A template the notes cannot be placed in leaves no AGENTS.md behind,
// not even an empty one.
Write ( templateFile , std : : format ( " # app \n \n {} \n " , BeginMarker ) ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) & & ! fs : : exists ( agents ) , " a template with a begin marker and no end marker writes no AGENTS.md " ) ;
// A CRLF template makes a CRLF AGENTS.md, the line a build adds included.
Write ( templateFile , " # app \r \n \r \n Text. \r \n " ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " a CRLF template is written " ) ;
text = Read ( agents ) ;
Check ( text . starts_with ( std : : format ( " {} \r \n # app \r \n " , GeneratedLine ) ) & & Count ( text , " \n " ) = = Count ( text , " \r \n " ) , " AGENTS.md from a CRLF template ends every line in CRLF " ) ;
Write ( templateFile , templateText ) ;
fs : : remove ( agents ) ;
// A layout that keeps the file elsewhere and links it, which a write that
// replaced the link with a copy would quietly undo.
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
fs : : create_directories ( app / " docs " ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Write ( app / " docs " / " agents.md " , " " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
fs : : create_symlink ( " docs/agents.md " , agents ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( UpdateAgentsMd ( cfg , projectFile ) , " an AGENTS.md that is a symlink is written " ) ;
Check ( fs : : is_symlink ( agents ) & & Read ( app / " docs " / " agents.md " ) . contains ( toolNote ) , " the write goes through the link, which stays a link " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
fs : : remove ( agents ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
// Without a template, AGENTS.md is someone's own: never created, and
// never written, not even to take out a section an earlier crafter-build
// kept in it, which only gets a warning.
fs : : remove ( templateFile ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) & & ! fs : : exists ( agents ) , " a project without a template gets no AGENTS.md " ) ;
const std : : vector < DependencyNote > oldNotes = { { . name = " lib " , . text = " ## lib \n \n Notes a committed AGENTS.md still holds. " } } ;
const std : : string committed = Merge ( templateText , oldNotes ) ;
Write ( agents , committed ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) & & Read ( agents ) = = committed , " without a template, an AGENTS.md holding a section is left alone " ) ;
fs : : remove ( agents ) ;
Write ( templateFile , templateText ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
// The CLI: only a build or a test run writes it.
( void ) RunWith ( { " crafter-build " , " -q " , " lint " } ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( ! fs : : exists ( agents ) , " lint writes no AGENTS.md " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
( void ) RunWith ( { " crafter-build " , " -q " , " run " } ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( ! fs : : exists ( agents ) , " run writes no AGENTS.md " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
( void ) RunWith ( { " crafter-build " , " -q " , " test " , " --list " } ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( ! fs : : exists ( agents ) , " test --list writes no AGENTS.md " ) ;
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
( void ) RunWith ( { " crafter-build " , " -q " , " test " } ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( Read ( agents ) . contains ( toolNote ) , " a test run writes AGENTS.md " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
// app has no sources, so this build fails at the link, after the update.
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
fs : : remove ( agents ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
( void ) RunWith ( { " crafter-build " , " -q " } ) ;
feat(agents): write AGENTS.md from a tracked AGENTS.template.md
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:53:42 +01:00
Check ( Read ( agents ) . contains ( toolNote ) , " a build writes AGENTS.md before compiling, so one that fails still does " ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
test(agents): the cases the review found
Each fixed finding gets a check that fails without its fix: inline code
and a list item's fence, a section removed from between two paragraphs,
a section before an unclosed fence, a CRLF file, no error when there is
nothing to write, a hard-linked and a read-only AGENTS.md, a dependency
whose project file sits in the consumer's own directory, a test's
dependency, and `test --list`. The read-only case needs an ordinary
user, since root writes through the permission bits, and is skipped
under root.
The merge cases go through a helper that counts a throw as a failure,
so one regression no longer ends the run before the cases after it. A
passing run removes its staged copy, whose builds leave a std PCM per
configuration in a tmpfs.
Reverting the inline-code rule, the own-notes exclusion, the skipping
of tests, the `test --list` guard or the paragraph-preserving removal
each fails its check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:02:36 +01:00
// The builds above leave a std PCM per configuration in a tmpfs; a run
// that failed keeps them for a look.
fs : : current_path ( home ) ;
if ( Failures = = 0 ) fs : : remove_all ( scratch , ec ) ;
test(agents): what a consumer's AGENTS.md gets, and what stays put
The merge on its own: the section is appended after one blank line,
merging again changes nothing, a moved section is rewritten where it
stands, and removing it gives the file its own bytes back. Markers
inside a fenced block, one quoting a fenced block of its own, are not
the section's. Unbalanced markers, a document that ends inside a code
block and notes that would break the section are refused, naming the
line.
End to end, through LoadProject on a staged fixture: app names lib
and has tool as host tooling; lib builds against base and has libtool
as its own. Transitive gives tool, lib and base in that order, and
neither app's own notes nor libtool's. A second update writes nothing,
mtime included. Direct drops base, edited notes replace the old ones
in the one section, and None restores the original bytes. A project
without an AGENTS.md does not get one, and a symlinked AGENTS.md is
written through the link, which stays a link. Through Run in process,
lint and run leave the file alone, while `test --list` and a build
that fails at the link both refresh it.
The fixture's AGENTS.md is checked in as AGENTS.md.in: an AGENTS.md in
this tree would read as instructions for the agents working on it.
Moving the CLI hook above the run and lint branches, or letting Direct
walk past the first loaded project, fails four of the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:18:21 +01:00
return Failures > 0 ? 1 : 0 ;
}