context-loader 1.2.0__tar.gz → 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: context-loader
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: Render deterministic local Git context for any executor
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -219,14 +219,14 @@ Python standard library.
219
219
 
220
220
  ## Open-source quick start
221
221
 
222
- Current stable release: **1.2.0**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
222
+ Current stable release: **1.3.1**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
223
223
 
224
224
  ```bash
225
- uv tool install 'context-loader==1.2.0'
225
+ uv tool install 'context-loader==1.3.1'
226
226
  ```
227
227
 
228
228
  ```bash
229
- pip install 'context-loader==1.2.0'
229
+ pip install 'context-loader==1.3.1'
230
230
  ```
231
231
 
232
232
  ```bash
@@ -239,21 +239,21 @@ project-context --repo /path/to/repo --format json
239
239
 
240
240
  ## Install
241
241
 
242
- Source version: `1.2.0`. Install its matching published release or an exact source commit.
242
+ Source version: `1.3.1`. Install its matching published release or an exact source commit.
243
243
 
244
244
  Install via `uv`:
245
245
 
246
246
  ```bash
247
- uv tool install 'context-loader==1.2.0'
247
+ uv tool install 'context-loader==1.3.1'
248
248
  ```
249
249
 
250
250
  Install via `pip`:
251
251
 
252
252
  ```bash
253
- pip install 'context-loader==1.2.0'
253
+ pip install 'context-loader==1.3.1'
254
254
  ```
255
255
 
256
- For development or source-based installs tracking current repository (`1.2.0`):
256
+ For development or source-based installs tracking current repository (`1.3.1`):
257
257
 
258
258
  ```bash
259
259
  uv tool install git+https://github.com/xuanheng-tech/context-loader.git
@@ -264,8 +264,8 @@ The repository also retains `./project-context` as a direct development entry po
264
264
  ### Distributions
265
265
 
266
266
  The sole distribution is **`context-loader`**, containing the `context_loader` runtime
267
- and the `project-context` console script. Version `1.0.0` breaks the CLI name and Markdown
268
- heading contract. The JSON schema is unchanged. Terminal and any executor call this same
267
+ and the `project-context` console script. Version `1.0.0` broke the CLI name and Markdown
268
+ heading contract while leaving the then-current JSON schema unchanged. Terminal and any executor call this same
269
269
  entry point with explicit repository and focus arguments; no provider adapter or private
270
270
  session state is involved.
271
271
 
@@ -338,16 +338,17 @@ The output has no generated timestamp, AI summary, architecture inference, or di
338
338
  ## JSON Output
339
339
 
340
340
  `--format json` writes exactly one separator-compact UTF-8 JSON document plus one trailing newline
341
- to stdout. Keys are serialized in sorted order with `ensure_ascii=False`. `--format json-compact`
342
- uses the identical serialization; only the field set differs, as described under “Compact model
343
- consumption”. The declared contract for `--format json` is:
341
+ to stdout, within the JSON document bound under “Limits”. Keys are serialized in sorted order
342
+ with `ensure_ascii=False`. `--format json-compact` uses the identical serialization; only the field
343
+ set differs, as described under “Compact model consumption”. The declared contract for
344
+ `--format json` is:
344
345
 
345
346
  ```json
346
347
  {
347
- "schema_version": 1,
348
+ "schema_version": 3,
348
349
  "tool": {
349
350
  "name": "context-loader",
350
- "version": "1.2.0"
351
+ "version": "1.3.1"
351
352
  },
352
353
  "repository": {
353
354
  "requested_path": "/canonical/requested/path",
@@ -377,6 +378,11 @@ consumption”. The declared contract for `--format json` is:
377
378
  "context": "the same assembled Markdown context",
378
379
  "context_sha256": "sha256-hex",
379
380
  "statuses": [],
381
+ "nested_context": {
382
+ "files": ["docs/AGENTS.md"],
383
+ "list_truncated": false,
384
+ "scan_truncated": false
385
+ },
380
386
  "warnings": []
381
387
  }
382
388
  ```
@@ -389,31 +395,65 @@ context, in assembly order, after the existing newline normalization and truncat
389
395
  distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
390
396
  only `repository` sources and does not add any global-file discovery.
391
397
 
392
- `statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, and truncated working-tree or declared-command listings. Each entry has stable `code`, `subject_kind`, and `subject` fields. `sources`, `context`, and `schema_version` remain unchanged; callers can ignore `statuses` safely.
398
+ `statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown — plus the nested-context truncation flags, which exist only in machine form: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, truncated working-tree or declared-command listings, and truncated nested-`AGENTS.md` presence reports. Each entry has stable `code`, `subject_kind`, and `subject` fields. The two `nested_agents_*` codes mirror the `nested_context` truncation booleans for consumers that branch only on `statuses`; the full `nested_context` object remains a first-class field, never folded into `statuses`. Callers can ignore `statuses` safely.
393
399
 
394
400
  The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
395
401
  contain heading, heading level, and fixed selection reasons; it never contains the original focus or
396
- target path. Existing source fields and schema version 1 remain unchanged.
402
+ target path. Existing per-source fields remain unchanged; the document's exact key
403
+ set (now including `nested_context`) is named by its `schema_version`, described below.
404
+
405
+ `nested_context` is always present and existence-only: `files` lists repository-relative paths of
406
+ non-directory `AGENTS.md` entries under subdirectories, found by a bounded scan that enumerates
407
+ directory entries, never opens or reads a candidate file and never traverses a symlink; it carries
408
+ no sizes, no mtimes and no contents. A symlinked (even dangling) `AGENTS.md` is listed, because its
409
+ existence comes from the directory entry itself; the scan never resolves what it points at. Any
410
+ `.git`, `.venv`, `venv`, `node_modules` or `site-packages` directory is skipped at every depth:
411
+ package-manager and interpreter-managed trees are not authored repository instructions.
412
+ `list_truncated` means more matching entries exist beyond the report caps: 32 paths, and a 4 KiB
413
+ budget metered on the emitted array itself — each path's escaped, JSON-serialized string plus its
414
+ separator, with the array's brackets charged to the first entry — so the emitted array never costs
415
+ more than the budget it declares. Because quoting is counted, an array already near the cap can
416
+ report one path fewer than an older release did, and then says so through `list_truncated`.
417
+ `scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or a directory
418
+ or entry could not be read, so absence of a path is not proof of absence of the file. Nested paths
419
+ are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
420
+ the filesystem could not decode never breaks the document;
421
+ see “Limits” for the root paths that are emitted verbatim. The corresponding
422
+ `nested_agents_list_truncated` and `nested_agents_scan_truncated` status entries mirror both flags.
423
+ Presence is not instruction: whether a nested file applies, and its text, remain the caller's
424
+ judgment; Markdown output is unchanged.
397
425
 
398
426
  ### Compact model consumption (`--format json-compact`)
399
427
 
400
- `--format json-compact` emits a `schema_version` 2 document: exactly the version-1 document with
428
+ `--format json-compact` emits a `schema_version` 4 document: exactly the version-3 document with
401
429
  `sources[*].content` omitted. Every other field — `context`, `context_sha256`, `statuses`,
402
- `warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`, `path`,
403
- `content_sha256` and optional `selection` — is identical to `--format json` for the same arguments.
430
+ `nested_context`, `warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`,
431
+ `path`, `content_sha256` and optional `selection` — is identical to `--format json` for the same
432
+ arguments.
433
+ `sources` is provenance and index metadata, not the place a body must be fetched from: every omitted
434
+ body is already inside `context` verbatim, so consumers should read `context` for rule text and
435
+ reopen a source file only when the rendered body was truncated or selected away — never to recover a
436
+ body that the document already carried.
404
437
  The projection exists because the selected source bodies already occur verbatim inside `context`,
405
438
  so carrying them again in `sources` duplicates a large share of the document bytes (measured up to
406
439
  ~44% on real worktrees, scaling with how much of `context` those bodies occupy) for a model
407
440
  consumer without adding information. Nothing is lost: each omitted body remains inside `context`,
408
441
  `content_sha256` still fingerprints that rendered body, and `path` plus `canonical_root` locate the
409
442
  underlying file for a bounded manual re-read (whose raw bytes may differ from the rendered body as
410
- defined above). `schema_version` is an exact document selector, not an upgrade marker: version 2 is
411
- a field-set projection of version 1, so consumers must branch on the value and must not apply
412
- `>=`-superset reasoning. Default Markdown and `--format json` output bytes, exit codes and
413
- determinism are unchanged.
414
-
415
- The JSON schema version and package version are independent: `--format json` currently emits
416
- `schema_version` `1` and `--format json-compact` emits `2`, while `tool.version` is `1.2.0`.
443
+ defined above). `schema_version` is an exact document selector, not an upgrade marker: version 4 is
444
+ a field-set projection of version 3, so consumers must branch on the value and must not apply
445
+ `>=`-superset reasoning. Introducing this compact format does
446
+ not otherwise change default Markdown bytes, exit codes or determinism.
447
+
448
+ One historical exception is acknowledged: releases 1.0.0 and 1.1.0 both emitted `schema_version` 1
449
+ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward each emitted number
450
+ names exactly one key set, and the `nested_context` renumber below enforces that rule going forward.
451
+
452
+ The JSON schema version and package version are independent: this build emits `schema_version` `3`
453
+ for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.1`. Version 1
454
+ and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
455
+ `nested_context` changes the default document's key set, version 3 names the current full-document
456
+ key set and version 4 names the compact projection.
417
457
  Callers must depend only on fields declared above.
418
458
  The document contains no generated time or random identifier, so unchanged input produces identical
419
459
  JSON bytes. On failure, stdout remains empty and stderr contains only a short diagnostic.
@@ -428,7 +468,9 @@ Resolving an instruction hierarchy stays with the calling agent harness. That in
428
468
  shared or user-level instruction file outside the repository, nested or scoped `AGENTS.md` files
429
469
  under subdirectories, and any include or import directive written inside an instruction file:
430
470
  such a directive is transported as literal text and is never followed. `--path` selects sections
431
- of the repository-root `AGENTS.md` only; it never changes which files are read.
471
+ of the repository-root `AGENTS.md` only; it never changes which files are read. The JSON-only
472
+ `nested_context` field reports the existence of nested `AGENTS.md` paths without reading them;
473
+ deciding whether any of them applies stays with the calling agent harness exactly as before.
432
474
 
433
475
  A successful run therefore proves that the bounded root context was collected and rendered. It
434
476
  does not prove that every instruction applicable to a task has been loaded.
@@ -461,7 +503,23 @@ scan that ends before EOF still reports the characters it could not select.
461
503
 
462
504
  ## Limits
463
505
 
464
- - Final stdout: 98,304 bytes
506
+ Final output bounds differ per format. The two stdout bounds below are enforced on the bytes written
507
+ to stdout and the nested report cap on the bytes the document emits for that array; every other
508
+ bullet is a per-component, read or capture bound, as each one states:
509
+
510
+ - Markdown stdout: 98,304 bytes. A section that cannot fit is replaced by an omission notice, and so
511
+ is every later section, so the limit holds without cutting a section mid-body.
512
+ - JSON and JSON-compact stdout: 8,388,608 bytes (8 MiB) for the serialized document, including its
513
+ trailing newline. This bound is fail-closed: if a repository's evidence cannot fit, the tool exits
514
+ 1, writes nothing to stdout and prints `error: JSON output exceeded the 8388608 byte document
515
+ limit`. It never emits a truncated document. The component budgets below keep legitimate
516
+ documents far inside it — the largest escape-dense repository audited for this release emitted
517
+ 263 KiB, of which 248 KiB was escaped `statuses` subjects — so it is a final backstop rather
518
+ than an expected truncation point.
519
+ - Component budgets below are metered on the Markdown `context` content, and that same string is the
520
+ JSON `context` value; because JSON escaping expands quotes, backslashes and control characters,
521
+ the serialized bytes can be several times larger. The JSON document also repeats the source
522
+ bodies, so no single component figure bounds JSON stdout.
465
523
  - `AGENTS.md`: 256-KiB bounded source scan; selected source plus selection audit remains at most
466
524
  16 KiB, including a 4-KiB maximum small head
467
525
  - `README.md`: 16 KiB
@@ -469,8 +527,14 @@ scan that ends before EOF still reports the characters it could not select.
469
527
  - All entry-file bodies: 24 KiB
470
528
  - Declared commands: 8 KiB
471
529
  - Directory tree: 12 KiB, 300 entries, and depth 2
530
+ - Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, at most 32 reported paths and
531
+ a 4 KiB budget metered on the emitted array; file contents are never read
472
532
  - Working-tree changes: 100 paths and 4 KiB
473
533
  - Recent commits: 8
534
+ - JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
535
+ by the entry counts above, while `repository` and `sources[*].path` are emitted verbatim, so a
536
+ repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
537
+ still renders
474
538
  - Git subprocess output: 16 MiB (bounded while reading)
475
539
  - Candidate file validation: 16 MiB hard read bound
476
540
 
@@ -507,7 +571,8 @@ or candidate-file content.
507
571
 
508
572
  ## Not Included
509
573
 
510
- Version 1.0.0 does not provide AI summaries, project-type detection, nested `AGENTS.md` handling,
574
+ Version 1.3.1 does not provide AI summaries, project-type detection, loading or transport of
575
+ nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
511
576
  Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
512
577
  network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
513
578
 
@@ -521,8 +586,9 @@ just check
521
586
 
522
587
  ## Version maintenance
523
588
 
524
- The versions in `pyproject.toml` and `context_loader/__init__.py`, the matching `CHANGELOG.md`
525
- section, required tests, and the README current-stable declaration plus `context-loader==X.Y.Z`
589
+ The versions in `pyproject.toml` and `context_loader/__init__.py`, the `tool_version` in
590
+ `tool_cli_contract.json`, the matching `CHANGELOG.md` section, required tests, and the README
591
+ current-stable declaration plus `context-loader==X.Y.Z`
526
592
  install pins must change in the same release-preparation batch. `CHANGELOG.md` is the
527
593
  authoritative version-change record. `scripts/release.py` rejects README stable/install
528
594
  pins that disagree with the intended package version; historical changelog entries are
@@ -8,14 +8,14 @@ Python standard library.
8
8
 
9
9
  ## Open-source quick start
10
10
 
11
- Current stable release: **1.2.0**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
11
+ Current stable release: **1.3.1**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
12
12
 
13
13
  ```bash
14
- uv tool install 'context-loader==1.2.0'
14
+ uv tool install 'context-loader==1.3.1'
15
15
  ```
16
16
 
17
17
  ```bash
18
- pip install 'context-loader==1.2.0'
18
+ pip install 'context-loader==1.3.1'
19
19
  ```
20
20
 
21
21
  ```bash
@@ -28,21 +28,21 @@ project-context --repo /path/to/repo --format json
28
28
 
29
29
  ## Install
30
30
 
31
- Source version: `1.2.0`. Install its matching published release or an exact source commit.
31
+ Source version: `1.3.1`. Install its matching published release or an exact source commit.
32
32
 
33
33
  Install via `uv`:
34
34
 
35
35
  ```bash
36
- uv tool install 'context-loader==1.2.0'
36
+ uv tool install 'context-loader==1.3.1'
37
37
  ```
38
38
 
39
39
  Install via `pip`:
40
40
 
41
41
  ```bash
42
- pip install 'context-loader==1.2.0'
42
+ pip install 'context-loader==1.3.1'
43
43
  ```
44
44
 
45
- For development or source-based installs tracking current repository (`1.2.0`):
45
+ For development or source-based installs tracking current repository (`1.3.1`):
46
46
 
47
47
  ```bash
48
48
  uv tool install git+https://github.com/xuanheng-tech/context-loader.git
@@ -53,8 +53,8 @@ The repository also retains `./project-context` as a direct development entry po
53
53
  ### Distributions
54
54
 
55
55
  The sole distribution is **`context-loader`**, containing the `context_loader` runtime
56
- and the `project-context` console script. Version `1.0.0` breaks the CLI name and Markdown
57
- heading contract. The JSON schema is unchanged. Terminal and any executor call this same
56
+ and the `project-context` console script. Version `1.0.0` broke the CLI name and Markdown
57
+ heading contract while leaving the then-current JSON schema unchanged. Terminal and any executor call this same
58
58
  entry point with explicit repository and focus arguments; no provider adapter or private
59
59
  session state is involved.
60
60
 
@@ -127,16 +127,17 @@ The output has no generated timestamp, AI summary, architecture inference, or di
127
127
  ## JSON Output
128
128
 
129
129
  `--format json` writes exactly one separator-compact UTF-8 JSON document plus one trailing newline
130
- to stdout. Keys are serialized in sorted order with `ensure_ascii=False`. `--format json-compact`
131
- uses the identical serialization; only the field set differs, as described under “Compact model
132
- consumption”. The declared contract for `--format json` is:
130
+ to stdout, within the JSON document bound under “Limits”. Keys are serialized in sorted order
131
+ with `ensure_ascii=False`. `--format json-compact` uses the identical serialization; only the field
132
+ set differs, as described under “Compact model consumption”. The declared contract for
133
+ `--format json` is:
133
134
 
134
135
  ```json
135
136
  {
136
- "schema_version": 1,
137
+ "schema_version": 3,
137
138
  "tool": {
138
139
  "name": "context-loader",
139
- "version": "1.2.0"
140
+ "version": "1.3.1"
140
141
  },
141
142
  "repository": {
142
143
  "requested_path": "/canonical/requested/path",
@@ -166,6 +167,11 @@ consumption”. The declared contract for `--format json` is:
166
167
  "context": "the same assembled Markdown context",
167
168
  "context_sha256": "sha256-hex",
168
169
  "statuses": [],
170
+ "nested_context": {
171
+ "files": ["docs/AGENTS.md"],
172
+ "list_truncated": false,
173
+ "scan_truncated": false
174
+ },
169
175
  "warnings": []
170
176
  }
171
177
  ```
@@ -178,31 +184,65 @@ context, in assembly order, after the existing newline normalization and truncat
178
184
  distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
179
185
  only `repository` sources and does not add any global-file discovery.
180
186
 
181
- `statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, and truncated working-tree or declared-command listings. Each entry has stable `code`, `subject_kind`, and `subject` fields. `sources`, `context`, and `schema_version` remain unchanged; callers can ignore `statuses` safely.
187
+ `statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown — plus the nested-context truncation flags, which exist only in machine form: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, truncated working-tree or declared-command listings, and truncated nested-`AGENTS.md` presence reports. Each entry has stable `code`, `subject_kind`, and `subject` fields. The two `nested_agents_*` codes mirror the `nested_context` truncation booleans for consumers that branch only on `statuses`; the full `nested_context` object remains a first-class field, never folded into `statuses`. Callers can ignore `statuses` safely.
182
188
 
183
189
  The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
184
190
  contain heading, heading level, and fixed selection reasons; it never contains the original focus or
185
- target path. Existing source fields and schema version 1 remain unchanged.
191
+ target path. Existing per-source fields remain unchanged; the document's exact key
192
+ set (now including `nested_context`) is named by its `schema_version`, described below.
193
+
194
+ `nested_context` is always present and existence-only: `files` lists repository-relative paths of
195
+ non-directory `AGENTS.md` entries under subdirectories, found by a bounded scan that enumerates
196
+ directory entries, never opens or reads a candidate file and never traverses a symlink; it carries
197
+ no sizes, no mtimes and no contents. A symlinked (even dangling) `AGENTS.md` is listed, because its
198
+ existence comes from the directory entry itself; the scan never resolves what it points at. Any
199
+ `.git`, `.venv`, `venv`, `node_modules` or `site-packages` directory is skipped at every depth:
200
+ package-manager and interpreter-managed trees are not authored repository instructions.
201
+ `list_truncated` means more matching entries exist beyond the report caps: 32 paths, and a 4 KiB
202
+ budget metered on the emitted array itself — each path's escaped, JSON-serialized string plus its
203
+ separator, with the array's brackets charged to the first entry — so the emitted array never costs
204
+ more than the budget it declares. Because quoting is counted, an array already near the cap can
205
+ report one path fewer than an older release did, and then says so through `list_truncated`.
206
+ `scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or a directory
207
+ or entry could not be read, so absence of a path is not proof of absence of the file. Nested paths
208
+ are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
209
+ the filesystem could not decode never breaks the document;
210
+ see “Limits” for the root paths that are emitted verbatim. The corresponding
211
+ `nested_agents_list_truncated` and `nested_agents_scan_truncated` status entries mirror both flags.
212
+ Presence is not instruction: whether a nested file applies, and its text, remain the caller's
213
+ judgment; Markdown output is unchanged.
186
214
 
187
215
  ### Compact model consumption (`--format json-compact`)
188
216
 
189
- `--format json-compact` emits a `schema_version` 2 document: exactly the version-1 document with
217
+ `--format json-compact` emits a `schema_version` 4 document: exactly the version-3 document with
190
218
  `sources[*].content` omitted. Every other field — `context`, `context_sha256`, `statuses`,
191
- `warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`, `path`,
192
- `content_sha256` and optional `selection` — is identical to `--format json` for the same arguments.
219
+ `nested_context`, `warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`,
220
+ `path`, `content_sha256` and optional `selection` — is identical to `--format json` for the same
221
+ arguments.
222
+ `sources` is provenance and index metadata, not the place a body must be fetched from: every omitted
223
+ body is already inside `context` verbatim, so consumers should read `context` for rule text and
224
+ reopen a source file only when the rendered body was truncated or selected away — never to recover a
225
+ body that the document already carried.
193
226
  The projection exists because the selected source bodies already occur verbatim inside `context`,
194
227
  so carrying them again in `sources` duplicates a large share of the document bytes (measured up to
195
228
  ~44% on real worktrees, scaling with how much of `context` those bodies occupy) for a model
196
229
  consumer without adding information. Nothing is lost: each omitted body remains inside `context`,
197
230
  `content_sha256` still fingerprints that rendered body, and `path` plus `canonical_root` locate the
198
231
  underlying file for a bounded manual re-read (whose raw bytes may differ from the rendered body as
199
- defined above). `schema_version` is an exact document selector, not an upgrade marker: version 2 is
200
- a field-set projection of version 1, so consumers must branch on the value and must not apply
201
- `>=`-superset reasoning. Default Markdown and `--format json` output bytes, exit codes and
202
- determinism are unchanged.
203
-
204
- The JSON schema version and package version are independent: `--format json` currently emits
205
- `schema_version` `1` and `--format json-compact` emits `2`, while `tool.version` is `1.2.0`.
232
+ defined above). `schema_version` is an exact document selector, not an upgrade marker: version 4 is
233
+ a field-set projection of version 3, so consumers must branch on the value and must not apply
234
+ `>=`-superset reasoning. Introducing this compact format does
235
+ not otherwise change default Markdown bytes, exit codes or determinism.
236
+
237
+ One historical exception is acknowledged: releases 1.0.0 and 1.1.0 both emitted `schema_version` 1
238
+ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward each emitted number
239
+ names exactly one key set, and the `nested_context` renumber below enforces that rule going forward.
240
+
241
+ The JSON schema version and package version are independent: this build emits `schema_version` `3`
242
+ for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.1`. Version 1
243
+ and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
244
+ `nested_context` changes the default document's key set, version 3 names the current full-document
245
+ key set and version 4 names the compact projection.
206
246
  Callers must depend only on fields declared above.
207
247
  The document contains no generated time or random identifier, so unchanged input produces identical
208
248
  JSON bytes. On failure, stdout remains empty and stderr contains only a short diagnostic.
@@ -217,7 +257,9 @@ Resolving an instruction hierarchy stays with the calling agent harness. That in
217
257
  shared or user-level instruction file outside the repository, nested or scoped `AGENTS.md` files
218
258
  under subdirectories, and any include or import directive written inside an instruction file:
219
259
  such a directive is transported as literal text and is never followed. `--path` selects sections
220
- of the repository-root `AGENTS.md` only; it never changes which files are read.
260
+ of the repository-root `AGENTS.md` only; it never changes which files are read. The JSON-only
261
+ `nested_context` field reports the existence of nested `AGENTS.md` paths without reading them;
262
+ deciding whether any of them applies stays with the calling agent harness exactly as before.
221
263
 
222
264
  A successful run therefore proves that the bounded root context was collected and rendered. It
223
265
  does not prove that every instruction applicable to a task has been loaded.
@@ -250,7 +292,23 @@ scan that ends before EOF still reports the characters it could not select.
250
292
 
251
293
  ## Limits
252
294
 
253
- - Final stdout: 98,304 bytes
295
+ Final output bounds differ per format. The two stdout bounds below are enforced on the bytes written
296
+ to stdout and the nested report cap on the bytes the document emits for that array; every other
297
+ bullet is a per-component, read or capture bound, as each one states:
298
+
299
+ - Markdown stdout: 98,304 bytes. A section that cannot fit is replaced by an omission notice, and so
300
+ is every later section, so the limit holds without cutting a section mid-body.
301
+ - JSON and JSON-compact stdout: 8,388,608 bytes (8 MiB) for the serialized document, including its
302
+ trailing newline. This bound is fail-closed: if a repository's evidence cannot fit, the tool exits
303
+ 1, writes nothing to stdout and prints `error: JSON output exceeded the 8388608 byte document
304
+ limit`. It never emits a truncated document. The component budgets below keep legitimate
305
+ documents far inside it — the largest escape-dense repository audited for this release emitted
306
+ 263 KiB, of which 248 KiB was escaped `statuses` subjects — so it is a final backstop rather
307
+ than an expected truncation point.
308
+ - Component budgets below are metered on the Markdown `context` content, and that same string is the
309
+ JSON `context` value; because JSON escaping expands quotes, backslashes and control characters,
310
+ the serialized bytes can be several times larger. The JSON document also repeats the source
311
+ bodies, so no single component figure bounds JSON stdout.
254
312
  - `AGENTS.md`: 256-KiB bounded source scan; selected source plus selection audit remains at most
255
313
  16 KiB, including a 4-KiB maximum small head
256
314
  - `README.md`: 16 KiB
@@ -258,8 +316,14 @@ scan that ends before EOF still reports the characters it could not select.
258
316
  - All entry-file bodies: 24 KiB
259
317
  - Declared commands: 8 KiB
260
318
  - Directory tree: 12 KiB, 300 entries, and depth 2
319
+ - Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, at most 32 reported paths and
320
+ a 4 KiB budget metered on the emitted array; file contents are never read
261
321
  - Working-tree changes: 100 paths and 4 KiB
262
322
  - Recent commits: 8
323
+ - JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
324
+ by the entry counts above, while `repository` and `sources[*].path` are emitted verbatim, so a
325
+ repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
326
+ still renders
263
327
  - Git subprocess output: 16 MiB (bounded while reading)
264
328
  - Candidate file validation: 16 MiB hard read bound
265
329
 
@@ -296,7 +360,8 @@ or candidate-file content.
296
360
 
297
361
  ## Not Included
298
362
 
299
- Version 1.0.0 does not provide AI summaries, project-type detection, nested `AGENTS.md` handling,
363
+ Version 1.3.1 does not provide AI summaries, project-type detection, loading or transport of
364
+ nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
300
365
  Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
301
366
  network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
302
367
 
@@ -310,8 +375,9 @@ just check
310
375
 
311
376
  ## Version maintenance
312
377
 
313
- The versions in `pyproject.toml` and `context_loader/__init__.py`, the matching `CHANGELOG.md`
314
- section, required tests, and the README current-stable declaration plus `context-loader==X.Y.Z`
378
+ The versions in `pyproject.toml` and `context_loader/__init__.py`, the `tool_version` in
379
+ `tool_cli_contract.json`, the matching `CHANGELOG.md` section, required tests, and the README
380
+ current-stable declaration plus `context-loader==X.Y.Z`
315
381
  install pins must change in the same release-preparation batch. `CHANGELOG.md` is the
316
382
  authoritative version-change record. `scripts/release.py` rejects README stable/install
317
383
  pins that disagree with the intended package version; historical changelog entries are
@@ -1,3 +1,3 @@
1
1
  """Project Context Loader."""
2
2
 
3
- __version__ = "1.2.0"
3
+ __version__ = "1.3.1"
@@ -20,14 +20,22 @@ from .collect import (
20
20
  AgentsSelectionAudit,
21
21
  AgentsSelectionInputError,
22
22
  CollectedFile,
23
+ NestedContextPresence,
23
24
  ProjectContext,
24
25
  collect_project_context,
26
+ display_text,
25
27
  )
26
28
  from .git import ContextLoaderError, collect_repository
27
29
  from .render import render_markdown_with_details, rendered_source_contents
28
30
 
29
- JSON_SCHEMA_VERSION = 1
30
- COMPACT_JSON_SCHEMA_VERSION = 2
31
+ # Each value names one exact document shape and is never reused: 1 and 2 are the
32
+ # shapes published in release 1.2.0, so adding the nested_context object to both
33
+ # formats moved the default document to 3 and the compact projection to 4.
34
+ JSON_SCHEMA_VERSION = 3
35
+ COMPACT_JSON_SCHEMA_VERSION = 4
36
+ # Both machine formats share this final bound on the serialized document: output is
37
+ # either valid JSON within it or a fail-closed error, never a truncated document.
38
+ JSON_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024
31
39
  TOOL_NAME = "context-loader"
32
40
 
33
41
  _STATUS_CODE_BY_MESSAGE = {
@@ -81,6 +89,7 @@ class ProjectContextResult:
81
89
  context_sha256: str
82
90
  warnings: tuple[str, ...]
83
91
  statuses: tuple[ContextStatus, ...]
92
+ nested_context: NestedContextPresence
84
93
 
85
94
 
86
95
  def _text_sha256(content: str) -> str:
@@ -185,11 +194,16 @@ def _build_statuses(
185
194
  for entry in project.directory_tree.entries:
186
195
  if entry.kind == "unreadable_directory":
187
196
  subject = entry.path if entry.path else "."
188
- statuses.append(_status("unreadable", "tree_entry", subject))
197
+ statuses.append(_status("unreadable", "tree_entry", display_text(subject)))
189
198
 
190
199
  for title in omitted_sections:
191
200
  statuses.append(_status("section_omitted", "section", title))
192
201
 
202
+ if project.nested_context.list_truncated:
203
+ statuses.append(_status("nested_agents_list_truncated", "nested_context", "AGENTS.md"))
204
+ if project.nested_context.scan_truncated:
205
+ statuses.append(_status("nested_agents_scan_truncated", "nested_context", "AGENTS.md"))
206
+
193
207
  deduped: list[ContextStatus] = []
194
208
  seen: set[tuple[str, str, str]] = set()
195
209
  for status in statuses:
@@ -239,6 +253,7 @@ def load_project_context(
239
253
  context_sha256=hashlib.sha256(rendered.output).hexdigest(),
240
254
  warnings=(),
241
255
  statuses=statuses,
256
+ nested_context=project.nested_context,
242
257
  )
243
258
 
244
259
 
@@ -294,6 +309,9 @@ def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes
294
309
  ``compact`` projects away the duplicated source bodies only at this
295
310
  boundary: statuses and context were computed from the full model at load
296
311
  time, so both documents agree on everything except ``sources[*].content``.
312
+
313
+ The bound is on the final serialized document: a repository whose evidence
314
+ cannot fit fails closed with no output rather than emitting a partial file.
297
315
  """
298
316
  document = {
299
317
  "schema_version": (COMPACT_JSON_SCHEMA_VERSION if compact else result.schema_version),
@@ -309,6 +327,13 @@ def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes
309
327
  _source_document(source, include_content=not compact) for source in result.sources
310
328
  ],
311
329
  "statuses": [_status_document(status) for status in result.statuses],
330
+ "nested_context": {
331
+ # Filesystem names arrive undecoded (surrogate-escaped); sanitize exactly like
332
+ # tree/change paths so no raw byte can reach the UTF-8 document.
333
+ "files": [display_text(path) for path in result.nested_context.files],
334
+ "list_truncated": result.nested_context.list_truncated,
335
+ "scan_truncated": result.nested_context.scan_truncated,
336
+ },
312
337
  "context": result.context,
313
338
  "context_sha256": result.context_sha256,
314
339
  "warnings": list(result.warnings),
@@ -319,4 +344,9 @@ def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes
319
344
  sort_keys=True,
320
345
  separators=(",", ":"),
321
346
  )
322
- return f"{serialized}\n".encode()
347
+ encoded = f"{serialized}\n".encode()
348
+ if len(encoded) > JSON_OUTPUT_LIMIT_BYTES:
349
+ raise ContextLoaderError(
350
+ f"JSON output exceeded the {JSON_OUTPUT_LIMIT_BYTES} byte document limit"
351
+ )
352
+ return encoded
@@ -39,8 +39,8 @@ def _parser() -> argparse.ArgumentParser:
39
39
  choices=("markdown", "json", "json-compact"),
40
40
  default="markdown",
41
41
  help=(
42
- "output format (default: markdown); json-compact emits schema "
43
- "version 2 without duplicated source bodies"
42
+ "output format (default: markdown); json-compact omits the "
43
+ "duplicated source bodies (see README, Compact model consumption)"
44
44
  ),
45
45
  )
46
46
  return parser
@@ -26,6 +26,13 @@ DECLARED_COMMANDS_LIMIT_BYTES = 8 * 1024
26
26
  DIRECTORY_TREE_LIMIT_BYTES = 12 * 1024
27
27
  DIRECTORY_TREE_MAX_ITEMS = 300
28
28
  DIRECTORY_TREE_MAX_DEPTH = 2
29
+ NESTED_AGENTS_MAX_DEPTH = 4
30
+ NESTED_AGENTS_MAX_DIRECTORIES = 2_000
31
+ NESTED_AGENTS_MAX_FILES = 32
32
+ NESTED_AGENTS_MAX_LIST_BYTES = 4 * 1024
33
+ NESTED_AGENTS_EXCLUDED_DIRECTORIES = frozenset(
34
+ {".git", ".venv", "node_modules", "site-packages", "venv"}
35
+ )
29
36
  FILE_SCAN_LIMIT_BYTES = 16 * 1024 * 1024
30
37
  MAX_FILE_SCAN_BYTES = FILE_SCAN_LIMIT_BYTES
31
38
  TRUNCATION_MARKER = "… truncated by context-loader …"
@@ -190,6 +197,15 @@ class DirectoryTree:
190
197
  truncated: bool = False
191
198
 
192
199
 
200
+ @dataclass(frozen=True, slots=True)
201
+ class NestedContextPresence:
202
+ """Existence-only index of nested AGENTS.md files; their contents are never read."""
203
+
204
+ files: tuple[str, ...]
205
+ list_truncated: bool = False
206
+ scan_truncated: bool = False
207
+
208
+
193
209
  @dataclass(frozen=True, slots=True)
194
210
  class ProjectContext:
195
211
  instructions: CollectedFile
@@ -197,6 +213,7 @@ class ProjectContext:
197
213
  entry_files: tuple[CollectedFile, ...]
198
214
  commands: tuple[DeclaredCommand, ...]
199
215
  directory_tree: DirectoryTree
216
+ nested_context: NestedContextPresence
200
217
 
201
218
 
202
219
  @dataclass(frozen=True, slots=True)
@@ -975,6 +992,117 @@ def _collect_directory_tree(root: Path) -> DirectoryTree:
975
992
  return DirectoryTree(tuple(collected), truncated)
976
993
 
977
994
 
995
+ def display_text(value: str) -> str:
996
+ r"""Render one filesystem or Git string as printable, encodable ASCII-safe text.
997
+
998
+ Backticks, lone surrogates produced by ``surrogateescape`` decoding, and every
999
+ other non-printable codepoint become ``\xNN``/``\uNNNN``/``\UNNNNNNNN`` escapes so
1000
+ the character can cross the JSON and Markdown serialization boundaries intact.
1001
+ """
1002
+ rendered: list[str] = []
1003
+ for character in value:
1004
+ codepoint = ord(character)
1005
+ if character == "`":
1006
+ rendered.append(r"\x60")
1007
+ elif 0xDC80 <= codepoint <= 0xDCFF:
1008
+ rendered.append(f"\\x{codepoint - 0xDC00:02x}")
1009
+ elif character.isprintable():
1010
+ rendered.append(character)
1011
+ elif codepoint <= 0xFF:
1012
+ rendered.append(f"\\x{codepoint:02x}")
1013
+ elif codepoint <= 0xFFFF:
1014
+ rendered.append(f"\\u{codepoint:04x}")
1015
+ else:
1016
+ rendered.append(f"\\U{codepoint:08x}")
1017
+ return "".join(rendered)
1018
+
1019
+
1020
+ def serialized_display_length(value: str) -> int:
1021
+ """Return the UTF-8 byte size of ``value`` once escaped and JSON-serialized.
1022
+
1023
+ This is the exact bytes one ``nested_context`` entry costs, quotes included, so the
1024
+ report budget is metered on emitted output rather than on raw path bytes.
1025
+ """
1026
+ return len(json.dumps(display_text(value), ensure_ascii=False).encode("utf-8"))
1027
+
1028
+
1029
+ def collect_nested_agents_presence(repository: Path) -> NestedContextPresence:
1030
+ """List nested AGENTS.md paths under a bounded, contents-blind directory scan.
1031
+
1032
+ The scan reads directory entries only: it never opens a candidate file, never
1033
+ traverses a symlink, and reports truncation honestly so presence claims stay
1034
+ auditable. Any non-directory entry named AGENTS.md is listed, including
1035
+ symlinked ones, because existence comes from enumeration alone; entry types
1036
+ are never resolved beyond directory-vs-not, and contents remain the caller's
1037
+ responsibility to read.
1038
+ """
1039
+ files: list[str] = []
1040
+ reported_bytes = 0
1041
+ state = {"list_truncated": False, "scan_truncated": False, "directories": 0}
1042
+
1043
+ def walk(directory_descriptor: int, prefix: str, depth: int) -> None:
1044
+ nonlocal reported_bytes
1045
+ if depth > NESTED_AGENTS_MAX_DEPTH or state["directories"] >= NESTED_AGENTS_MAX_DIRECTORIES:
1046
+ state["scan_truncated"] = True
1047
+ return
1048
+ state["directories"] += 1
1049
+ try:
1050
+ with os.scandir(directory_descriptor) as iterator:
1051
+ entries = sorted(iterator, key=lambda entry: entry.name)
1052
+ except OSError:
1053
+ state["scan_truncated"] = True
1054
+ return
1055
+ subdirectories: list[str] = []
1056
+ for entry in entries:
1057
+ try:
1058
+ is_directory = entry.is_dir(follow_symlinks=False)
1059
+ except OSError:
1060
+ state["scan_truncated"] = True
1061
+ continue
1062
+ path = f"{prefix}/{entry.name}" if prefix else entry.name
1063
+ if is_directory:
1064
+ if entry.name not in NESTED_AGENTS_EXCLUDED_DIRECTORIES:
1065
+ subdirectories.append(path)
1066
+ continue
1067
+ if entry.name != "AGENTS.md" or not prefix:
1068
+ continue
1069
+ if len(files) >= NESTED_AGENTS_MAX_FILES:
1070
+ state["list_truncated"] = True
1071
+ continue
1072
+ # Charge the array's two bracket bytes to the first entry, so the budget
1073
+ # covers everything this list makes the document emit.
1074
+ name_bytes = serialized_display_length(path) + (1 if files else 2)
1075
+ if reported_bytes + name_bytes > NESTED_AGENTS_MAX_LIST_BYTES:
1076
+ state["list_truncated"] = True
1077
+ continue
1078
+ reported_bytes += name_bytes
1079
+ files.append(path)
1080
+ for path in subdirectories:
1081
+ flags = os.O_RDONLY | os.O_CLOEXEC | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
1082
+ try:
1083
+ child_descriptor = os.open(
1084
+ path.rsplit("/", 1)[-1], flags, dir_fd=directory_descriptor
1085
+ )
1086
+ except OSError:
1087
+ state["scan_truncated"] = True
1088
+ continue
1089
+ try:
1090
+ walk(child_descriptor, path, depth + 1)
1091
+ finally:
1092
+ os.close(child_descriptor)
1093
+
1094
+ flags = os.O_RDONLY | os.O_CLOEXEC | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
1095
+ try:
1096
+ root_descriptor = os.open(repository, flags)
1097
+ except OSError:
1098
+ return NestedContextPresence((), False, True)
1099
+ try:
1100
+ walk(root_descriptor, "", 0)
1101
+ finally:
1102
+ os.close(root_descriptor)
1103
+ return NestedContextPresence(tuple(files), state["list_truncated"], state["scan_truncated"])
1104
+
1105
+
978
1106
  def collect_project_context(
979
1107
  repository: Path,
980
1108
  *,
@@ -1001,4 +1129,5 @@ def collect_project_context(
1001
1129
  entry_files=entry_files,
1002
1130
  commands=_collect_commands(entry_files),
1003
1131
  directory_tree=_collect_directory_tree(repository),
1132
+ nested_context=collect_nested_agents_presence(repository),
1004
1133
  )
@@ -19,6 +19,7 @@ from .collect import (
19
19
  DirectoryTree,
20
20
  ProjectContext,
21
21
  TreeEntry,
22
+ display_text,
22
23
  render_agents_selection_audit,
23
24
  )
24
25
  from .git import RecentCommit, RepositoryState
@@ -39,30 +40,11 @@ class MarkdownRender:
39
40
  commands_truncated: bool
40
41
 
41
42
 
42
- def _display(value: str) -> str:
43
- rendered: list[str] = []
44
- for character in value:
45
- codepoint = ord(character)
46
- if character == "`":
47
- rendered.append(r"\x60")
48
- elif 0xDC80 <= codepoint <= 0xDCFF:
49
- rendered.append(f"\\x{codepoint - 0xDC00:02x}")
50
- elif character.isprintable():
51
- rendered.append(character)
52
- elif codepoint <= 0xFF:
53
- rendered.append(f"\\x{codepoint:02x}")
54
- elif codepoint <= 0xFFFF:
55
- rendered.append(f"\\u{codepoint:04x}")
56
- else:
57
- rendered.append(f"\\U{codepoint:08x}")
58
- return "".join(rendered)
59
-
60
-
61
43
  def _display_limited(value: str, limit: int) -> str | None:
62
44
  rendered: list[str] = []
63
45
  used = 0
64
46
  for character in value:
65
- piece = _display(character)
47
+ piece = display_text(character)
66
48
  size = len(piece.encode())
67
49
  if used + size > limit:
68
50
  return None
@@ -121,7 +103,7 @@ def _change_lines(state: RepositoryState) -> tuple[list[str], bool]:
121
103
  for change in state.changes:
122
104
  if len(lines) == MAX_CHANGE_ENTRIES:
123
105
  return lines, True
124
- line = f"- `{change.status} {_display(change.path)}`"
106
+ line = f"- `{change.status} {display_text(change.path)}`"
125
107
  line_bytes = len(f"{line}\n".encode())
126
108
  if used_bytes + line_bytes > MAX_CHANGE_MARKDOWN_BYTES:
127
109
  return lines, True
@@ -134,10 +116,10 @@ def _render_git_state(state: RepositoryState) -> str:
134
116
  lines = [
135
117
  "## Git State",
136
118
  "",
137
- f"- Branch: `{_display(state.branch)}`",
138
- f"- HEAD: `{_display(state.head)}`",
139
- f"- Upstream: `{_display(state.upstream)}`",
140
- f"- Ahead / behind: `{_display(state.ahead_behind)}`",
119
+ f"- Branch: `{display_text(state.branch)}`",
120
+ f"- HEAD: `{display_text(state.head)}`",
121
+ f"- Upstream: `{display_text(state.upstream)}`",
122
+ f"- Ahead / behind: `{display_text(state.ahead_behind)}`",
141
123
  f"- Worktree: `{state.worktree}`",
142
124
  "",
143
125
  "### Working Tree Changes",
@@ -170,10 +152,10 @@ def _command_line(command: DeclaredCommand) -> str:
170
152
  if command.parse_error:
171
153
  return f"- `{command.source}`: unable to parse command declarations."
172
154
  assert command.invocation is not None
173
- invocation = _display(command.invocation)
155
+ invocation = display_text(command.invocation)
174
156
  if command.target is None:
175
157
  return f"- `{invocation}`"
176
- return f"- `{invocation}` → `{_display(command.target)}`"
158
+ return f"- `{invocation}` → `{display_text(command.target)}`"
177
159
 
178
160
 
179
161
  def _bounded_lines(
@@ -262,7 +244,7 @@ def _render_recent_commits(commits: tuple[RecentCommit, ...]) -> str | None:
262
244
 
263
245
 
264
246
  def _tree_line(entry: TreeEntry) -> str:
265
- path = _display(entry.path)
247
+ path = display_text(entry.path)
266
248
  if entry.kind == "directory":
267
249
  return f"{path}/"
268
250
  if entry.kind == "symlink":
@@ -295,7 +277,7 @@ def render_markdown_with_details(state: RepositoryState, project: ProjectContext
295
277
  "# Project Context",
296
278
  "",
297
279
  f"- Schema: `{SCHEMA}`",
298
- f"- Repository: `{_display(os.fspath(state.repository))}`",
280
+ f"- Repository: `{display_text(os.fspath(state.repository))}`",
299
281
  )
300
282
  )
301
283
  commands_section, commands_truncated = _render_commands(project.commands)
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "context-loader"
7
- version = "1.2.0"
7
+ version = "1.3.1"
8
8
  description = "Render deterministic local Git context for any executor"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.13"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "context-loader"
7
- version = "1.2.0"
7
+ version = "1.3.1"
8
8
  description = "Render deterministic local Git context for any executor"
9
9
  license = { file = "LICENSE" }
10
10
  readme = "README.md"
@@ -46,5 +46,5 @@
46
46
  ],
47
47
  "schema_version": 1,
48
48
  "tool_name": "context-loader",
49
- "tool_version": "1.2.0"
49
+ "tool_version": "1.3.1"
50
50
  }
File without changes