context-loader 1.2.0__tar.gz → 1.3.0__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.0
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.0**. 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.0'
226
226
  ```
227
227
 
228
228
  ```bash
229
- pip install 'context-loader==1.2.0'
229
+ pip install 'context-loader==1.3.0'
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.0`. 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.0'
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.0'
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.0`):
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
 
@@ -344,10 +344,10 @@ consumption”. The declared contract for `--format json` is:
344
344
 
345
345
  ```json
346
346
  {
347
- "schema_version": 1,
347
+ "schema_version": 3,
348
348
  "tool": {
349
349
  "name": "context-loader",
350
- "version": "1.2.0"
350
+ "version": "1.3.0"
351
351
  },
352
352
  "repository": {
353
353
  "requested_path": "/canonical/requested/path",
@@ -377,6 +377,11 @@ consumption”. The declared contract for `--format json` is:
377
377
  "context": "the same assembled Markdown context",
378
378
  "context_sha256": "sha256-hex",
379
379
  "statuses": [],
380
+ "nested_context": {
381
+ "files": ["docs/AGENTS.md"],
382
+ "list_truncated": false,
383
+ "scan_truncated": false
384
+ },
380
385
  "warnings": []
381
386
  }
382
387
  ```
@@ -389,31 +394,60 @@ context, in assembly order, after the existing newline normalization and truncat
389
394
  distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
390
395
  only `repository` sources and does not add any global-file discovery.
391
396
 
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.
397
+ `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
398
 
394
399
  The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
395
400
  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.
401
+ target path. Existing per-source fields remain unchanged; the document's exact key
402
+ set (now including `nested_context`) is named by its `schema_version`, described below.
403
+
404
+ `nested_context` is always present and existence-only: `files` lists repository-relative paths of
405
+ non-directory `AGENTS.md` entries under subdirectories, found by a bounded scan that enumerates
406
+ directory entries, never opens or reads a candidate file and never traverses a symlink; it carries
407
+ no sizes, no mtimes and no contents. A symlinked (even dangling) `AGENTS.md` is listed, because its
408
+ existence comes from the directory entry itself; the scan never resolves what it points at. Any
409
+ `.git`, `.venv`, `venv`, `node_modules` or `site-packages` directory is skipped at every depth:
410
+ package-manager and interpreter-managed trees are not authored repository instructions.
411
+ `list_truncated` means more matching entries exist beyond the report caps (32 paths and 4 KiB of
412
+ path bytes); `scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or
413
+ a directory or entry could not be read, so absence of a path is not proof of absence of the file.
414
+ Paths are sanitized with the same escaping the Markdown uses for repository-derived text, so the
415
+ document is always valid UTF-8. The corresponding
416
+ `nested_agents_list_truncated` and `nested_agents_scan_truncated` status entries mirror both flags.
417
+ Presence is not instruction: whether a nested file applies, and its text, remain the caller's
418
+ judgment; Markdown output is unchanged.
397
419
 
398
420
  ### Compact model consumption (`--format json-compact`)
399
421
 
400
- `--format json-compact` emits a `schema_version` 2 document: exactly the version-1 document with
422
+ `--format json-compact` emits a `schema_version` 4 document: exactly the version-3 document with
401
423
  `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.
424
+ `nested_context`, `warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`,
425
+ `path`, `content_sha256` and optional `selection` — is identical to `--format json` for the same
426
+ arguments.
427
+ `sources` is provenance and index metadata, not the place a body must be fetched from: every omitted
428
+ body is already inside `context` verbatim, so consumers should read `context` for rule text and
429
+ reopen a source file only when the rendered body was truncated or selected away — never to recover a
430
+ body that the document already carried.
404
431
  The projection exists because the selected source bodies already occur verbatim inside `context`,
405
432
  so carrying them again in `sources` duplicates a large share of the document bytes (measured up to
406
433
  ~44% on real worktrees, scaling with how much of `context` those bodies occupy) for a model
407
434
  consumer without adding information. Nothing is lost: each omitted body remains inside `context`,
408
435
  `content_sha256` still fingerprints that rendered body, and `path` plus `canonical_root` locate the
409
436
  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`.
437
+ defined above). `schema_version` is an exact document selector, not an upgrade marker: version 4 is
438
+ a field-set projection of version 3, so consumers must branch on the value and must not apply
439
+ `>=`-superset reasoning. Introducing this compact format does
440
+ not otherwise change default Markdown bytes, exit codes or determinism.
441
+
442
+ One historical exception is acknowledged: releases 1.0.0 and 1.1.0 both emitted `schema_version` 1
443
+ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward each emitted number
444
+ names exactly one key set, and the `nested_context` renumber below enforces that rule going forward.
445
+
446
+ The JSON schema version and package version are independent: this build emits `schema_version` `3`
447
+ for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.0`. Version 1
448
+ and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
449
+ `nested_context` changes the default document's key set, version 3 names the current full-document
450
+ key set and version 4 names the compact projection.
417
451
  Callers must depend only on fields declared above.
418
452
  The document contains no generated time or random identifier, so unchanged input produces identical
419
453
  JSON bytes. On failure, stdout remains empty and stderr contains only a short diagnostic.
@@ -428,7 +462,9 @@ Resolving an instruction hierarchy stays with the calling agent harness. That in
428
462
  shared or user-level instruction file outside the repository, nested or scoped `AGENTS.md` files
429
463
  under subdirectories, and any include or import directive written inside an instruction file:
430
464
  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.
465
+ of the repository-root `AGENTS.md` only; it never changes which files are read. The JSON-only
466
+ `nested_context` field reports the existence of nested `AGENTS.md` paths without reading them;
467
+ deciding whether any of them applies stays with the calling agent harness exactly as before.
432
468
 
433
469
  A successful run therefore proves that the bounded root context was collected and rendered. It
434
470
  does not prove that every instruction applicable to a task has been loaded.
@@ -469,6 +505,8 @@ scan that ends before EOF still reports the characters it could not select.
469
505
  - All entry-file bodies: 24 KiB
470
506
  - Declared commands: 8 KiB
471
507
  - Directory tree: 12 KiB, 300 entries, and depth 2
508
+ - Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, and at most 32 reported paths;
509
+ file contents are never read
472
510
  - Working-tree changes: 100 paths and 4 KiB
473
511
  - Recent commits: 8
474
512
  - Git subprocess output: 16 MiB (bounded while reading)
@@ -507,7 +545,8 @@ or candidate-file content.
507
545
 
508
546
  ## Not Included
509
547
 
510
- Version 1.0.0 does not provide AI summaries, project-type detection, nested `AGENTS.md` handling,
548
+ Version 1.3.0 does not provide AI summaries, project-type detection, loading or transport of
549
+ nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
511
550
  Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
512
551
  network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
513
552
 
@@ -521,8 +560,9 @@ just check
521
560
 
522
561
  ## Version maintenance
523
562
 
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`
563
+ The versions in `pyproject.toml` and `context_loader/__init__.py`, the `tool_version` in
564
+ `tool_cli_contract.json`, the matching `CHANGELOG.md` section, required tests, and the README
565
+ current-stable declaration plus `context-loader==X.Y.Z`
526
566
  install pins must change in the same release-preparation batch. `CHANGELOG.md` is the
527
567
  authoritative version-change record. `scripts/release.py` rejects README stable/install
528
568
  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.0**. 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.0'
15
15
  ```
16
16
 
17
17
  ```bash
18
- pip install 'context-loader==1.2.0'
18
+ pip install 'context-loader==1.3.0'
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.0`. 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.0'
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.0'
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.0`):
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
 
@@ -133,10 +133,10 @@ consumption”. The declared contract for `--format json` is:
133
133
 
134
134
  ```json
135
135
  {
136
- "schema_version": 1,
136
+ "schema_version": 3,
137
137
  "tool": {
138
138
  "name": "context-loader",
139
- "version": "1.2.0"
139
+ "version": "1.3.0"
140
140
  },
141
141
  "repository": {
142
142
  "requested_path": "/canonical/requested/path",
@@ -166,6 +166,11 @@ consumption”. The declared contract for `--format json` is:
166
166
  "context": "the same assembled Markdown context",
167
167
  "context_sha256": "sha256-hex",
168
168
  "statuses": [],
169
+ "nested_context": {
170
+ "files": ["docs/AGENTS.md"],
171
+ "list_truncated": false,
172
+ "scan_truncated": false
173
+ },
169
174
  "warnings": []
170
175
  }
171
176
  ```
@@ -178,31 +183,60 @@ context, in assembly order, after the existing newline normalization and truncat
178
183
  distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
179
184
  only `repository` sources and does not add any global-file discovery.
180
185
 
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.
186
+ `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
187
 
183
188
  The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
184
189
  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.
190
+ target path. Existing per-source fields remain unchanged; the document's exact key
191
+ set (now including `nested_context`) is named by its `schema_version`, described below.
192
+
193
+ `nested_context` is always present and existence-only: `files` lists repository-relative paths of
194
+ non-directory `AGENTS.md` entries under subdirectories, found by a bounded scan that enumerates
195
+ directory entries, never opens or reads a candidate file and never traverses a symlink; it carries
196
+ no sizes, no mtimes and no contents. A symlinked (even dangling) `AGENTS.md` is listed, because its
197
+ existence comes from the directory entry itself; the scan never resolves what it points at. Any
198
+ `.git`, `.venv`, `venv`, `node_modules` or `site-packages` directory is skipped at every depth:
199
+ package-manager and interpreter-managed trees are not authored repository instructions.
200
+ `list_truncated` means more matching entries exist beyond the report caps (32 paths and 4 KiB of
201
+ path bytes); `scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or
202
+ a directory or entry could not be read, so absence of a path is not proof of absence of the file.
203
+ Paths are sanitized with the same escaping the Markdown uses for repository-derived text, so the
204
+ document is always valid UTF-8. The corresponding
205
+ `nested_agents_list_truncated` and `nested_agents_scan_truncated` status entries mirror both flags.
206
+ Presence is not instruction: whether a nested file applies, and its text, remain the caller's
207
+ judgment; Markdown output is unchanged.
186
208
 
187
209
  ### Compact model consumption (`--format json-compact`)
188
210
 
189
- `--format json-compact` emits a `schema_version` 2 document: exactly the version-1 document with
211
+ `--format json-compact` emits a `schema_version` 4 document: exactly the version-3 document with
190
212
  `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.
213
+ `nested_context`, `warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`,
214
+ `path`, `content_sha256` and optional `selection` — is identical to `--format json` for the same
215
+ arguments.
216
+ `sources` is provenance and index metadata, not the place a body must be fetched from: every omitted
217
+ body is already inside `context` verbatim, so consumers should read `context` for rule text and
218
+ reopen a source file only when the rendered body was truncated or selected away — never to recover a
219
+ body that the document already carried.
193
220
  The projection exists because the selected source bodies already occur verbatim inside `context`,
194
221
  so carrying them again in `sources` duplicates a large share of the document bytes (measured up to
195
222
  ~44% on real worktrees, scaling with how much of `context` those bodies occupy) for a model
196
223
  consumer without adding information. Nothing is lost: each omitted body remains inside `context`,
197
224
  `content_sha256` still fingerprints that rendered body, and `path` plus `canonical_root` locate the
198
225
  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`.
226
+ defined above). `schema_version` is an exact document selector, not an upgrade marker: version 4 is
227
+ a field-set projection of version 3, so consumers must branch on the value and must not apply
228
+ `>=`-superset reasoning. Introducing this compact format does
229
+ not otherwise change default Markdown bytes, exit codes or determinism.
230
+
231
+ One historical exception is acknowledged: releases 1.0.0 and 1.1.0 both emitted `schema_version` 1
232
+ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward each emitted number
233
+ names exactly one key set, and the `nested_context` renumber below enforces that rule going forward.
234
+
235
+ The JSON schema version and package version are independent: this build emits `schema_version` `3`
236
+ for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.0`. Version 1
237
+ and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
238
+ `nested_context` changes the default document's key set, version 3 names the current full-document
239
+ key set and version 4 names the compact projection.
206
240
  Callers must depend only on fields declared above.
207
241
  The document contains no generated time or random identifier, so unchanged input produces identical
208
242
  JSON bytes. On failure, stdout remains empty and stderr contains only a short diagnostic.
@@ -217,7 +251,9 @@ Resolving an instruction hierarchy stays with the calling agent harness. That in
217
251
  shared or user-level instruction file outside the repository, nested or scoped `AGENTS.md` files
218
252
  under subdirectories, and any include or import directive written inside an instruction file:
219
253
  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.
254
+ of the repository-root `AGENTS.md` only; it never changes which files are read. The JSON-only
255
+ `nested_context` field reports the existence of nested `AGENTS.md` paths without reading them;
256
+ deciding whether any of them applies stays with the calling agent harness exactly as before.
221
257
 
222
258
  A successful run therefore proves that the bounded root context was collected and rendered. It
223
259
  does not prove that every instruction applicable to a task has been loaded.
@@ -258,6 +294,8 @@ scan that ends before EOF still reports the characters it could not select.
258
294
  - All entry-file bodies: 24 KiB
259
295
  - Declared commands: 8 KiB
260
296
  - Directory tree: 12 KiB, 300 entries, and depth 2
297
+ - Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, and at most 32 reported paths;
298
+ file contents are never read
261
299
  - Working-tree changes: 100 paths and 4 KiB
262
300
  - Recent commits: 8
263
301
  - Git subprocess output: 16 MiB (bounded while reading)
@@ -296,7 +334,8 @@ or candidate-file content.
296
334
 
297
335
  ## Not Included
298
336
 
299
- Version 1.0.0 does not provide AI summaries, project-type detection, nested `AGENTS.md` handling,
337
+ Version 1.3.0 does not provide AI summaries, project-type detection, loading or transport of
338
+ nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
300
339
  Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
301
340
  network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
302
341
 
@@ -310,8 +349,9 @@ just check
310
349
 
311
350
  ## Version maintenance
312
351
 
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`
352
+ The versions in `pyproject.toml` and `context_loader/__init__.py`, the `tool_version` in
353
+ `tool_cli_contract.json`, the matching `CHANGELOG.md` section, required tests, and the README
354
+ current-stable declaration plus `context-loader==X.Y.Z`
315
355
  install pins must change in the same release-preparation batch. `CHANGELOG.md` is the
316
356
  authoritative version-change record. `scripts/release.py` rejects README stable/install
317
357
  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.0"
@@ -20,14 +20,18 @@ from .collect import (
20
20
  AgentsSelectionAudit,
21
21
  AgentsSelectionInputError,
22
22
  CollectedFile,
23
+ NestedContextPresence,
23
24
  ProjectContext,
24
25
  collect_project_context,
25
26
  )
26
27
  from .git import ContextLoaderError, collect_repository
27
- from .render import render_markdown_with_details, rendered_source_contents
28
+ from .render import _display, render_markdown_with_details, rendered_source_contents
28
29
 
29
- JSON_SCHEMA_VERSION = 1
30
- COMPACT_JSON_SCHEMA_VERSION = 2
30
+ # Each value names one exact document shape and is never reused: 1 and 2 are the
31
+ # shapes published in release 1.2.0, so adding the nested_context object to both
32
+ # formats moved the default document to 3 and the compact projection to 4.
33
+ JSON_SCHEMA_VERSION = 3
34
+ COMPACT_JSON_SCHEMA_VERSION = 4
31
35
  TOOL_NAME = "context-loader"
32
36
 
33
37
  _STATUS_CODE_BY_MESSAGE = {
@@ -81,6 +85,7 @@ class ProjectContextResult:
81
85
  context_sha256: str
82
86
  warnings: tuple[str, ...]
83
87
  statuses: tuple[ContextStatus, ...]
88
+ nested_context: NestedContextPresence
84
89
 
85
90
 
86
91
  def _text_sha256(content: str) -> str:
@@ -185,11 +190,16 @@ def _build_statuses(
185
190
  for entry in project.directory_tree.entries:
186
191
  if entry.kind == "unreadable_directory":
187
192
  subject = entry.path if entry.path else "."
188
- statuses.append(_status("unreadable", "tree_entry", subject))
193
+ statuses.append(_status("unreadable", "tree_entry", _display(subject)))
189
194
 
190
195
  for title in omitted_sections:
191
196
  statuses.append(_status("section_omitted", "section", title))
192
197
 
198
+ if project.nested_context.list_truncated:
199
+ statuses.append(_status("nested_agents_list_truncated", "nested_context", "AGENTS.md"))
200
+ if project.nested_context.scan_truncated:
201
+ statuses.append(_status("nested_agents_scan_truncated", "nested_context", "AGENTS.md"))
202
+
193
203
  deduped: list[ContextStatus] = []
194
204
  seen: set[tuple[str, str, str]] = set()
195
205
  for status in statuses:
@@ -239,6 +249,7 @@ def load_project_context(
239
249
  context_sha256=hashlib.sha256(rendered.output).hexdigest(),
240
250
  warnings=(),
241
251
  statuses=statuses,
252
+ nested_context=project.nested_context,
242
253
  )
243
254
 
244
255
 
@@ -309,6 +320,13 @@ def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes
309
320
  _source_document(source, include_content=not compact) for source in result.sources
310
321
  ],
311
322
  "statuses": [_status_document(status) for status in result.statuses],
323
+ "nested_context": {
324
+ # Filesystem names arrive undecoded (surrogate-escaped); sanitize exactly like
325
+ # tree/change paths so no raw byte can reach the UTF-8 document.
326
+ "files": [_display(path) for path in result.nested_context.files],
327
+ "list_truncated": result.nested_context.list_truncated,
328
+ "scan_truncated": result.nested_context.scan_truncated,
329
+ },
312
330
  "context": result.context,
313
331
  "context_sha256": result.context_sha256,
314
332
  "warnings": list(result.warnings),
@@ -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,82 @@ def _collect_directory_tree(root: Path) -> DirectoryTree:
975
992
  return DirectoryTree(tuple(collected), truncated)
976
993
 
977
994
 
995
+ def collect_nested_agents_presence(repository: Path) -> NestedContextPresence:
996
+ """List nested AGENTS.md paths under a bounded, contents-blind directory scan.
997
+
998
+ The scan reads directory entries only: it never opens a candidate file, never
999
+ traverses a symlink, and reports truncation honestly so presence claims stay
1000
+ auditable. Any non-directory entry named AGENTS.md is listed, including
1001
+ symlinked ones, because existence comes from enumeration alone; entry types
1002
+ are never resolved beyond directory-vs-not, and contents remain the caller's
1003
+ responsibility to read.
1004
+ """
1005
+ files: list[str] = []
1006
+ reported_bytes = 0
1007
+ state = {"list_truncated": False, "scan_truncated": False, "directories": 0}
1008
+
1009
+ def walk(directory_descriptor: int, prefix: str, depth: int) -> None:
1010
+ nonlocal reported_bytes
1011
+ if depth > NESTED_AGENTS_MAX_DEPTH or state["directories"] >= NESTED_AGENTS_MAX_DIRECTORIES:
1012
+ state["scan_truncated"] = True
1013
+ return
1014
+ state["directories"] += 1
1015
+ try:
1016
+ with os.scandir(directory_descriptor) as iterator:
1017
+ entries = sorted(iterator, key=lambda entry: entry.name)
1018
+ except OSError:
1019
+ state["scan_truncated"] = True
1020
+ return
1021
+ subdirectories: list[str] = []
1022
+ for entry in entries:
1023
+ try:
1024
+ is_directory = entry.is_dir(follow_symlinks=False)
1025
+ except OSError:
1026
+ state["scan_truncated"] = True
1027
+ continue
1028
+ path = f"{prefix}/{entry.name}" if prefix else entry.name
1029
+ if is_directory:
1030
+ if entry.name not in NESTED_AGENTS_EXCLUDED_DIRECTORIES:
1031
+ subdirectories.append(path)
1032
+ continue
1033
+ if entry.name != "AGENTS.md" or not prefix:
1034
+ continue
1035
+ if len(files) >= NESTED_AGENTS_MAX_FILES:
1036
+ state["list_truncated"] = True
1037
+ continue
1038
+ name_bytes = len(path.encode("utf-8", errors="surrogateescape"))
1039
+ name_bytes += 1 if files else 0
1040
+ if reported_bytes + name_bytes > NESTED_AGENTS_MAX_LIST_BYTES:
1041
+ state["list_truncated"] = True
1042
+ continue
1043
+ reported_bytes += name_bytes
1044
+ files.append(path)
1045
+ for path in subdirectories:
1046
+ flags = os.O_RDONLY | os.O_CLOEXEC | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
1047
+ try:
1048
+ child_descriptor = os.open(
1049
+ path.rsplit("/", 1)[-1], flags, dir_fd=directory_descriptor
1050
+ )
1051
+ except OSError:
1052
+ state["scan_truncated"] = True
1053
+ continue
1054
+ try:
1055
+ walk(child_descriptor, path, depth + 1)
1056
+ finally:
1057
+ os.close(child_descriptor)
1058
+
1059
+ flags = os.O_RDONLY | os.O_CLOEXEC | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
1060
+ try:
1061
+ root_descriptor = os.open(repository, flags)
1062
+ except OSError:
1063
+ return NestedContextPresence((), False, True)
1064
+ try:
1065
+ walk(root_descriptor, "", 0)
1066
+ finally:
1067
+ os.close(root_descriptor)
1068
+ return NestedContextPresence(tuple(files), state["list_truncated"], state["scan_truncated"])
1069
+
1070
+
978
1071
  def collect_project_context(
979
1072
  repository: Path,
980
1073
  *,
@@ -1001,4 +1094,5 @@ def collect_project_context(
1001
1094
  entry_files=entry_files,
1002
1095
  commands=_collect_commands(entry_files),
1003
1096
  directory_tree=_collect_directory_tree(repository),
1097
+ nested_context=collect_nested_agents_presence(repository),
1004
1098
  )
@@ -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.0"
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.0"
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.0"
50
50
  }
File without changes