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.
- {context_loader-1.2.0 → context_loader-1.3.0}/PKG-INFO +68 -28
- {context_loader-1.2.0 → context_loader-1.3.0}/README.md +67 -27
- {context_loader-1.2.0 → context_loader-1.3.0}/context_loader/__init__.py +1 -1
- {context_loader-1.2.0 → context_loader-1.3.0}/context_loader/application.py +22 -4
- {context_loader-1.2.0 → context_loader-1.3.0}/context_loader/cli.py +2 -2
- {context_loader-1.2.0 → context_loader-1.3.0}/context_loader/collect.py +94 -0
- {context_loader-1.2.0 → context_loader-1.3.0}/pyproject.toml +1 -1
- {context_loader-1.2.0 → context_loader-1.3.0}/pyproject.toml.orig +1 -1
- {context_loader-1.2.0 → context_loader-1.3.0}/tool_cli_contract.json +1 -1
- {context_loader-1.2.0 → context_loader-1.3.0}/LICENSE +0 -0
- {context_loader-1.2.0 → context_loader-1.3.0}/context_loader/git.py +0 -0
- {context_loader-1.2.0 → context_loader-1.3.0}/context_loader/render.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: context-loader
|
|
3
|
-
Version: 1.
|
|
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.
|
|
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.
|
|
225
|
+
uv tool install 'context-loader==1.3.0'
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
```bash
|
|
229
|
-
pip install 'context-loader==1.
|
|
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.
|
|
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.
|
|
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.
|
|
253
|
+
pip install 'context-loader==1.3.0'
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
For development or source-based installs tracking current repository (`1.
|
|
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`
|
|
268
|
-
heading contract
|
|
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":
|
|
347
|
+
"schema_version": 3,
|
|
348
348
|
"tool": {
|
|
349
349
|
"name": "context-loader",
|
|
350
|
-
"version": "1.
|
|
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,
|
|
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
|
|
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`
|
|
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`,
|
|
403
|
-
`content_sha256` and optional `selection` — is identical to `--format json` for the same
|
|
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
|
|
411
|
-
a field-set projection of version
|
|
412
|
-
`>=`-superset reasoning.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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.
|
|
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
|
|
525
|
-
section, required tests, and the README
|
|
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.
|
|
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.
|
|
14
|
+
uv tool install 'context-loader==1.3.0'
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pip install 'context-loader==1.
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
+
pip install 'context-loader==1.3.0'
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
For development or source-based installs tracking current repository (`1.
|
|
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`
|
|
57
|
-
heading contract
|
|
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":
|
|
136
|
+
"schema_version": 3,
|
|
137
137
|
"tool": {
|
|
138
138
|
"name": "context-loader",
|
|
139
|
-
"version": "1.
|
|
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,
|
|
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
|
|
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`
|
|
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`,
|
|
192
|
-
`content_sha256` and optional `selection` — is identical to `--format json` for the same
|
|
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
|
|
200
|
-
a field-set projection of version
|
|
201
|
-
`>=`-superset reasoning.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
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
|
|
314
|
-
section, required tests, and the README
|
|
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
|
|
@@ -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
|
-
|
|
30
|
-
|
|
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
|
|
43
|
-
"
|
|
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
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|