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 ;
// Dependency notes: every dependency that ships an AGENTS.consumer.md beside
// its project.cpp has the file kept in a marked section of its consumer's
// AGENTS.md, refreshed by each build and test run.
//
// AGENTS.md is the user's own file, committed and edited by hand, so what
// this guards against is everything else moving: a section appended again on
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
// every build, text outside the section rewritten, lost or run together, a
// write when nothing changed (a dirty tree, an editor reloading), an
// AGENTS.md created in a project that has none, one written although it is
// read-only, or replaced so that a hard link to it goes stale, 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 --> " ;
}
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 " ;
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 " ;
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
// Checked in under another name: an AGENTS.md in this repository's tree
// would read as instructions for the agents working on it.
const std : : string agentsOriginal = Read ( app / " AGENTS.md.in " ) ;
Write ( agents , agentsOriginal ) ;
// 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 " ;
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 ) , " the first update writes AGENTS.md " ) ;
std : : string text = Read ( agents ) ;
Check ( text . starts_with ( std : : format ( " {} \n {} \n " , agentsOriginal , BeginMarker ) ) , " the section follows the consumer's own text, which is untouched " ) ;
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 ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " switching to Direct rewrites the section " ) ;
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 " ) ;
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 ) ;
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 : : None ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " None removes the section " ) ;
Check ( Read ( agents ) = = agentsOriginal , " removing the section gives AGENTS.md its own bytes back " ) ;
cfg . dependencyNotes = DependencyNotes : : Transitive ;
fs : : remove ( agents ) ;
Check ( ! UpdateAgentsMd ( cfg , projectFile ) & & ! fs : : exists ( agents ) , " a project without an AGENTS.md does not get one " ) ;
// A common layout keeps the file elsewhere and links it, which a write
// that replaced the link with a copy would quietly undo.
fs : : create_directories ( app / " docs " ) ;
Write ( app / " docs " / " agents.md " , agentsOriginal ) ;
fs : : create_symlink ( " docs/agents.md " , agents ) ;
Check ( UpdateAgentsMd ( cfg , projectFile ) , " an AGENTS.md that is a symlink is updated " ) ;
Check ( fs : : is_symlink ( agents ) & & Read ( app / " docs " / " agents.md " ) . contains ( toolNote ) , " the update goes through the link, which stays a link " ) ;
fs : : remove ( agents ) ;
// The CLI: only a build or a test run writes it.
Write ( agents , agentsOriginal ) ;
( void ) RunWith ( { " crafter-build " , " -q " , " lint " } ) ;
Check ( Read ( agents ) = = agentsOriginal , " lint leaves AGENTS.md alone " ) ;
( void ) RunWith ( { " crafter-build " , " -q " , " run " } ) ;
Check ( Read ( agents ) = = agentsOriginal , " run leaves AGENTS.md alone " ) ;
( void ) RunWith ( { " crafter-build " , " -q " , " test " , " --list " } ) ;
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 ( Read ( agents ) = = agentsOriginal , " test --list leaves AGENTS.md alone " ) ;
( void ) RunWith ( { " crafter-build " , " -q " , " test " } ) ;
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 ( Read ( agents ) . contains ( toolNote ) , " a test run refreshes AGENTS.md " ) ;
// app has no sources, so this build fails at the link, after the update.
Write ( agents , agentsOriginal ) ;
( void ) RunWith ( { " crafter-build " , " -q " } ) ;
Check ( Read ( agents ) . contains ( toolNote ) , " a build refreshes AGENTS.md before compiling, so one that fails still does " ) ;
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 ;
}