Crafter.Build/tests/DependencyNotes/main.cpp

357 lines
22 KiB
C++
Raw Permalink Normal View History

// 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.
//
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.
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;
}
// 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());
}
}
// 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. -->";
}
int main() {
// The merge on its own: what goes where in a document, with no project.
const std::string original = "# app\n\nThe consumer's own text.\n";
const std::vector<DependencyNote> two = {
{ .name = "one", .text = "## one\n\nFirst." },
{ .name = "two", .text = "## two\n\nSecond." },
};
const std::string merged = Merge(original, two);
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");
const std::size_t first = merged.find("<!-- from one -->\n## one\n\nFirst.\n");
const std::size_t second = merged.find("<!-- from two -->\n## two\n\nSecond.\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);
{
const std::string moved = std::format("# app\n\n{}\n## After\n\nThe user's text after it.\n", section);
const std::vector<DependencyNote> changed = { { .name = "one", .text = "## one\n\nChanged." } };
const std::string rewritten = Merge(moved, changed);
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\nThe 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");
const std::string between = std::format("Para one.\n\n{}Para two.\n", section);
Check(Merge(between, {}) == "Para one.\n\nPara 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("{}```\nnever 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\nText.\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");
}
// Markers quoted in a code block are not the section's, including a block
// that quotes a fenced block of its own; and only real fences count as
// code blocks.
{
const std::string quoted = std::format("# app\n\n````markdown\n{}\n```\nnested\n```\n{}\n````\n", BeginMarker, EndMarker);
const std::string withNotes = Merge(quoted, two);
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");
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{}\nThe 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");
}
// 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.
Check(MergeFails(std::format("# app\n{}\ntext\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");
Check(MergeFails("# app\n\n```\nnever 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{}\ntext\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```\nnever closed\n", {}) == "# app\n\n```\nnever 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) } };
Check(MergeFails(original, markerNote).contains("marker-note"), "a note with a marker line of its own is refused");
const std::vector<DependencyNote> fenceNote = { { .name = "fence-note", .text = "## bad\n```\nnever closed" } };
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.
const fs::path home = fs::current_path();
const fs::path fixture = home / "tests" / "DependencyNotes" / "fixture";
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";
const fs::path projectFile = app / "project.cpp";
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);
// 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;
}
const std::string toolNote = "<!-- from tool -->\n## tool\n\nRun tool before committing.\n";
const std::string libNote = "<!-- from lib -->\n## lib\n\nCall lib through its module, never through its headers.\n";
// base's file opens and ends with blank lines, which the section drops.
const std::string baseNote = "<!-- from base -->\n## base\n\nbase 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);
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;
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");
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");
Check(!text.contains("Notes from app"), "app's own AGENTS.consumer.md stays out, even beside a dependency's project file");
Check(!text.contains("libtool"), "lib's host tooling stays out of its consumers' notes");
Check(!text.contains("testonly"), "a test's dependency stays out");
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");
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;
Write(libNotes, "## lib\n\nlib was rewritten.\n");
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("{}\nA paragraph added to the template.\n", templateText));
Check(UpdateAgentsMd(cfg, projectFile), "an edited template is picked up");
Check(Read(agents).starts_with(std::format("{}\nA 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\nText 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\nText 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");
// 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\nlib, 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\nlib, 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("{}\nAn 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\nAn 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");
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");
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");
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\nText.\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.
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", "");
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");
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\nNotes 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);
// 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");
(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");
(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");
(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");
// 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);
(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");
// 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);
return Failures > 0 ? 1 : 0;
}