Merge
dotagents._merge
Managed-block merge for init's AGENTS.md and the harnesses' own entry files.
init must never clobber a user-customized AGENTS.md. Content owned by dotagents
is delimited by literal marker lines and treated as a block within the file:
create if missing, prepend if present-without-markers, refresh-in-place if the
markers are already there. Detection is by marker presence only, never by prose
matching, so it survives user reformatting.
A marker counts only when it is a LINE of its own (surrounding whitespace
allowed): the base AGENTS.md itself mentions the marker names in prose, and
a substring match would treat that mention as a block.
BEGIN_MARKER = '<!-- dotagents:begin -->'
module-attribute
CONTEXT_BEGIN_MARKER = '<!-- dotagents:context:begin -->'
module-attribute
CONTEXT_END_MARKER = '<!-- dotagents:context:end -->'
module-attribute
END_MARKER = '<!-- dotagents:end -->'
module-attribute
find_block(text, begin_marker=BEGIN_MARKER, end_marker=END_MARKER)
(start, end) character span of the managed block in text -- from
the first BEGIN marker line through the first END marker line after it,
ignoring marker lines inside fenced code -- or None when there is no
block. A BEGIN with no END after it is not a block (the caller decides
whether that is an error).
Source code in src/dotagents/_merge.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
merge_block(target, block_source_text, *, force=False, dry_run=False, backup_root=None, begin_marker=BEGIN_MARKER, end_marker=END_MARKER, append=False)
Merge block_source_text (a fully marker-wrapped skeleton file's
contents) into target, returning the branch taken:
"created" / "block-inserted" / "block-refreshed" / "skipped (present)"
(the block is already current) and, with force, "unchanged" /
"replaced (--force, backed up)" / "replaced (--force)".
Never overwrites content outside the markers unless force is True (then
the whole file is replaced, after being backed up under backup_root if the
target pre-exists -- at a path mirroring the target's, and never over an
earlier backup of the same file; "backed up" is reported only when a copy
was made).
begin_marker/end_marker default to the Markdown/HTML comment pair; pass a
different pair for other comment syntaxes (e.g. #-prefixed markers for TOML).
append puts the block at the END of an existing file rather than the start.
Required for TOML: a [table] header captures every key line that follows it,
so prepending a table would silently swallow the user's existing top-level keys
into our table.
Files are written LF-only (:func:dotagents._fs.write_text_lf). A target
carrying a BEGIN marker line with no END after it is malformed and refused
(SystemExit) rather than gaining a second block.
Source code in src/dotagents/_merge.py
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
merge_context_block(target, context_text, *, dry_run=False)
Write an assembled context into target as a managed
dotagents:context block: created if the file is absent, refreshed in
place if the block is there, appended after the user's own content
otherwise. Never a raw overwrite: the target may be a context SOURCE (a
harness's own AGENTS.md), and overwriting it would re-inline the
previous run's output.
Marker lines inside context_text (a store's AGENTS.md carries its
own dotagents:begin/end) are dropped: embedded, a later include
merge into the same file took them for its block and overwrote the
context between them.
Source code in src/dotagents/_merge.py
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | |
merge_include_line(target, include_line, *, force=False, dry_run=False, backup_root=None)
Managed-block merge of a one-line @<path> include into a harness's
entry file (~/.claude/CLAUDE.md, <project>/.claude/CLAUDE.md,
~/.gemini/GEMINI.md), so the harness actually loads the store's
AGENTS.md.
If the file already carries include_line ANYWHERE (a hand-written include,
inside or outside a managed block), it is left untouched (branch
"skipped (present)") -- the include is the point, not the markers. Otherwise
the marker-wrapped line is merged like any block, APPENDED so an existing
file's own content stays first.
Source code in src/dotagents/_merge.py
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
remove_block(target, *, dry_run=False, begin_marker=BEGIN_MARKER, end_marker=END_MARKER)
Delete the managed block (markers included) from target, with the one
blank line merge_block(append=True) put before it. Everything else stays.
Returns "removed" or "absent" (no file, or no block in it).
Source code in src/dotagents/_merge.py
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
timestamped_backup_root(dest)
<dest>/install_backup/<timestamp>, ONE per store per process: every
adapter of an init --force run backs up into the same root, so
:func:_backup's never-overwrite rule sees the earlier copies.
Source code in src/dotagents/_merge.py
340 341 342 343 344 345 346 347 | |