agent-first-data 0.18.0__tar.gz → 0.19.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.
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/PKG-INFO +1 -1
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/__init__.py +2 -0
- agent_first_data-0.19.0/agent_first_data/assets/skills/agent-first-data/SKILL.md +280 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/assets/skills/agent-first-data/references/rules.md +16 -12
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/cli.py +168 -3
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/format.py +120 -28
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data.egg-info/PKG-INFO +1 -1
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/pyproject.toml +1 -1
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/tests/test_cli.py +205 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/tests/test_format.py +35 -0
- agent_first_data-0.19.0/tests/test_no_stderr_policy.py +48 -0
- agent_first_data-0.18.0/agent_first_data/assets/skills/agent-first-data/SKILL.md +0 -179
- agent_first_data-0.18.0/tests/test_no_stderr_policy.py +0 -25
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/README.md +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/assets/protocol-v1.schema.json +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/assets/registry.json +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/assets/skills/agent-first-data/references/protocol-v1.schema.json +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/assets/skills/agent-first-data/references/registry.json +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/skill.py +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data/stream_redirect.py +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/setup.cfg +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/tests/test_skill.py +0 -0
- {agent_first_data-0.18.0 → agent_first_data-0.19.0}/tests/test_stream_redirect.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
|
|
@@ -34,6 +34,7 @@ from agent_first_data.format import (
|
|
|
34
34
|
|
|
35
35
|
from agent_first_data.cli import (
|
|
36
36
|
OutputFormat,
|
|
37
|
+
OutputTo,
|
|
37
38
|
LogFilters,
|
|
38
39
|
CliEmitter,
|
|
39
40
|
cli_parse_output,
|
|
@@ -76,6 +77,7 @@ __all__ = [
|
|
|
76
77
|
"is_valid_rfc3339_date",
|
|
77
78
|
"is_valid_rfc3339_time",
|
|
78
79
|
"OutputFormat",
|
|
80
|
+
"OutputTo",
|
|
79
81
|
"LogFilters",
|
|
80
82
|
"CliEmitter",
|
|
81
83
|
"cli_parse_output",
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-first-data
|
|
3
|
+
description: Apply and review the formal Agent-First Data specification using the Rust/Go/Python/TypeScript libraries or afdata CLI for naming, redaction, formatting, protocol envelopes, logging, linting, validation, and reading or safely editing structured JSON/TOML/YAML/dotenv/INI documents (and the +++/--- frontmatter block of a Markdown page) by dot-path. Use proactively when writing or reviewing structured data, configs, logs, transport payloads, database/wire fields, CLI output, compatibility-sensitive public and persistent field names, or when reading or editing a config/document/Markdown-frontmatter file from the shell.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Canonical source: skills/agent-first-data/SKILL.md.
|
|
7
|
+
Mirrored byte-for-byte into go/python/typescript assets/ by scripts/sync_offline_assets.py.
|
|
8
|
+
Edit only here; run that script (checked by scripts/test.sh) to propagate. -->
|
|
9
|
+
|
|
10
|
+
# Agent-First Data
|
|
11
|
+
|
|
12
|
+
Use AFDATA when naming fields or reviewing structured output. The contract is naming-based: suffixes communicate units, formatting, and redaction semantics without relying on free-text inference.
|
|
13
|
+
|
|
14
|
+
## Specification workflow
|
|
15
|
+
|
|
16
|
+
Treat the formal specification as normative, not the skill summary or an
|
|
17
|
+
example payload. In a repository checkout, read these spore-root-relative
|
|
18
|
+
sources as needed:
|
|
19
|
+
|
|
20
|
+
- `spec/agent-first-data.md` — complete cross-language naming, formatting,
|
|
21
|
+
redaction, protocol, logging, and CLI contract.
|
|
22
|
+
- `spec/registry.json` — exact suffix metadata.
|
|
23
|
+
- `spec/protocol-v1.schema.json` — protocol event schema.
|
|
24
|
+
- `spec/fixtures/` — cross-language conformance cases.
|
|
25
|
+
|
|
26
|
+
When only the installed skill is available, use the bundled equivalents:
|
|
27
|
+
|
|
28
|
+
- `references/rules.md` — detailed suffix rules, output formatting, protocol templates, logging, CLI flags, and review checklist.
|
|
29
|
+
- `references/registry.json` — machine-readable suffix registry; use this as the exact source for suffix metadata.
|
|
30
|
+
- `references/protocol-v1.schema.json` — protocol event schema for `kind:"result"`, `kind:"error"`, `kind:"progress"`, and `kind:"log"` envelopes.
|
|
31
|
+
|
|
32
|
+
If an example or README conflicts with the formal spec, follow the spec and
|
|
33
|
+
report the discrepancy. Do not invent suffix meanings or reduce the protocol
|
|
34
|
+
schema.
|
|
35
|
+
|
|
36
|
+
## Library workflow
|
|
37
|
+
|
|
38
|
+
Use the library at serialization boundaries so redaction and formatting are
|
|
39
|
+
not reimplemented ad hoc. Rust applications can build a typed protocol event
|
|
40
|
+
and render it in the required output format:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cargo add agent-first-data --no-default-features
|
|
44
|
+
pip install agent-first-data
|
|
45
|
+
npm install agent-first-data
|
|
46
|
+
go get github.com/agentfirstkit/agent-first-data/go
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```rust
|
|
50
|
+
use agent_first_data::{json_result, render, OutputFormat, OutputOptions};
|
|
51
|
+
use serde_json::json;
|
|
52
|
+
|
|
53
|
+
let event = json_result(json!({
|
|
54
|
+
"latency_ms": 1280,
|
|
55
|
+
"api_key_secret": "sk-live-example",
|
|
56
|
+
})).build();
|
|
57
|
+
let rendered = render(event.as_value(), OutputFormat::Json, &OutputOptions::default());
|
|
58
|
+
assert!(rendered.contains("\"api_key_secret\":\"***\""));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For a finite CLI execution implemented in Rust, create one `CliEmitter`, select
|
|
62
|
+
`OutputFormat` through `cli_parse_output`, enable strict protocol, and emit one
|
|
63
|
+
terminal result or error. Handle version before clap with
|
|
64
|
+
`cli_handle_version_or_continue(raw_args, name, version)`.
|
|
65
|
+
For non-CLI HTTP/MCP/SSE serialization, call `redacted_value()` or
|
|
66
|
+
`redact_url_secrets()` explicitly at the boundary.
|
|
67
|
+
|
|
68
|
+
The same library contract is available for Go, Python, and TypeScript. Use the
|
|
69
|
+
runtime's native names while preserving the same builders, output formats,
|
|
70
|
+
redaction behavior, registry, schema, and conformance fixtures; consult its
|
|
71
|
+
root-level `go/README.md`, `python/README.md`, or `typescript/README.md` when
|
|
72
|
+
working in that runtime.
|
|
73
|
+
|
|
74
|
+
## Naming and redaction decisions
|
|
75
|
+
|
|
76
|
+
- Add unit suffixes to ambiguous numeric fields: `_ms`, `_s`, `_bytes`, `_percent`, `_sats`, `_msats`, currency suffixes, and timestamp suffixes.
|
|
77
|
+
- Use `_secret` for values/subtrees that must redact to `***` in every output format.
|
|
78
|
+
- Use `_url` only for whole URL values whose userinfo password or suffix-named secret query params should be scrubbed.
|
|
79
|
+
- Do not scan arbitrary free text for secrets. Rename fields or configure explicit secret names at the serialization boundary.
|
|
80
|
+
- Before changing public API, wire, database, or persistent field names, report the compatibility impact and get approval.
|
|
81
|
+
|
|
82
|
+
## Evaluation rules
|
|
83
|
+
|
|
84
|
+
- Trigger this skill for field naming, structured output, configs, logs, CLI output, protocol events, MCP/HTTP/SSE payloads, database columns, wire fields, or persisted JSON. Do not wait for the user to say "AFDATA" when the work changes those surfaces.
|
|
85
|
+
- In a repository checkout, treat `spec/agent-first-data.md`,
|
|
86
|
+
`spec/registry.json`, and `spec/protocol-v1.schema.json` as authoritative; in
|
|
87
|
+
an installed skill, use their bundled `references/` equivalents. Do not
|
|
88
|
+
replace them with invented suffix meanings, a reduced schema, or free-text
|
|
89
|
+
interpretation.
|
|
90
|
+
- For secrets, fix the field name or serializer configuration. Use `_secret` or configured exact secret names; do not rely on scanning arbitrary strings or object debug output.
|
|
91
|
+
- Run `afdata lint` on JSON/JSONL examples, JSON Schema, MCP schemas, or serialized samples when the CLI is available. Run `afdata validate` on finite protocol events/streams. If a check cannot run, report that explicitly.
|
|
92
|
+
- Before modifying an external project's public API, wire format, database schema, or persisted field names, stop and report the compatibility impact; request approval before applying the change.
|
|
93
|
+
|
|
94
|
+
## Redaction behavior contract
|
|
95
|
+
|
|
96
|
+
Fields named `_secret` or `_url` and values passing through AFDATA output rendering (`render`, in any format) are automatically redacted: secrets become `***`, URL userinfo passwords and suffix-named query parameters are scrubbed. When serializing outside output formatters (HTTP, MCP, SSE), call `redacted_value()` or `redact_url_secrets()` at the serialization boundary. PII redaction, domain-specific privacy policies (header allowlists, API scope sensitivity), and per-field secret naming are each spore's responsibility; the library provides field-name-based mechanics only.
|
|
97
|
+
|
|
98
|
+
## Logging behavior contract
|
|
99
|
+
|
|
100
|
+
One-time CLI output uses `json_log()` or `CliEmitter` for a single event; serialization via `render()` applies output formatting. Construct the emitter in finite mode for a one-shot command (`result`→stdout, `error`/`progress`/`log`→stderr) or stream mode for an interleaved event stream (all events on one destination) — see the Output stream contract. Long-running services or processes depending on structured logging (tonic, sqlx, hyper, etc. via tracing) initialize with `afdata_tracing::try_init()` (Rust only) to wire process-wide logging; other languages emit log events explicitly via builders or integrate their own structured logging.
|
|
101
|
+
|
|
102
|
+
## CLI workflow
|
|
103
|
+
|
|
104
|
+
Install `afdata` when a shell workflow needs validation, formatting, or skill
|
|
105
|
+
administration without embedding a library:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cargo install agent-first-data
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Use it for deterministic checks:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
afdata lint payload.json
|
|
115
|
+
afdata validate events.jsonl
|
|
116
|
+
afdata render payload.json --output yaml
|
|
117
|
+
afdata skill validate skills/example-skill
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- Every command's first positional is its input; `-` reads stdin explicitly.
|
|
121
|
+
There is no implicit stdin fallback, and omitting the input is always a
|
|
122
|
+
usage error that names the fix — never a hang.
|
|
123
|
+
- Run `afdata lint` for ordinary JSON/JSONL, JSON Schema, MCP input/output
|
|
124
|
+
schemas, or a structured document (`--input-format`/extension detection
|
|
125
|
+
picks TOML/YAML/dotenv/INI; JSON/JSONL stays the default dual-mode input).
|
|
126
|
+
- Run `afdata validate` for AFDATA protocol events or finite event streams.
|
|
127
|
+
- Run `afdata render` to apply AFDATA redaction/formatting to arbitrary JSON
|
|
128
|
+
or JSONL. `--output` accepts `json`, `yaml`, or `plain`; JSON and YAML keep
|
|
129
|
+
original keys/values (structure-preserving), Plain is the lossy human
|
|
130
|
+
renderer.
|
|
131
|
+
- Treat findings as contract issues unless the owning tool intentionally
|
|
132
|
+
documents a non-AFDATA field.
|
|
133
|
+
|
|
134
|
+
## Output stream contract
|
|
135
|
+
|
|
136
|
+
AFDATA CLIs route protocol events by consumption mode, not by event shape.
|
|
137
|
+
|
|
138
|
+
- A **finite one-shot command** (the default) puts `kind:"result"` on `stdout`
|
|
139
|
+
and `kind:"error"`/`progress`/`log` on `stderr`, so `x=$(tool …)` never
|
|
140
|
+
captures a failure as data, `tool … >/dev/null` never swallows a diagnostic,
|
|
141
|
+
and `tool … | next` never pipes an error envelope in as input. The exit
|
|
142
|
+
code — not the stream — is set by the terminal kind; routing follows `kind`,
|
|
143
|
+
not the exit code.
|
|
144
|
+
- An **event stream** (interleaved events consumed in order) keeps every event,
|
|
145
|
+
including `error`, on one stream so ordering survives.
|
|
146
|
+
- `afdata` and any CLI built on the library expose `--output-to
|
|
147
|
+
<split|stdout|stderr>` (default `split`). `stdout`/`stderr` collapse the whole
|
|
148
|
+
stream — errors included — onto one destination, for a consumer that would
|
|
149
|
+
rather read one stream and branch on `kind`. `--output` picks the format,
|
|
150
|
+
`--output-to` picks the destination. Send an event stream to a file by
|
|
151
|
+
collapsing it (`--output-to stdout`) and redirecting (`--stdout-file PATH`).
|
|
152
|
+
- The raw-scalar readers (`value`/`paths`/`keys`) are intrinsically split (raw
|
|
153
|
+
data on `stdout`, error envelope on `stderr`) and reject a non-default
|
|
154
|
+
`--output-to`.
|
|
155
|
+
- When building a CLI on the library, choose the mode by how the output is
|
|
156
|
+
consumed — do not reflexively pick finite. A plain one-shot shell command uses
|
|
157
|
+
**finite** mode (errors → stderr, so shell capture and pipelines stay safe). A
|
|
158
|
+
tool whose output is itself an event stream (interleaved `progress`/`log` then
|
|
159
|
+
a terminal `result`/`error` — e.g. streaming query rows), or that is
|
|
160
|
+
agent-facing and wants the caller to read a single structured channel and
|
|
161
|
+
branch on `kind`, uses **stream/unified** mode (every event, errors included,
|
|
162
|
+
on `stdout`). A stream's terminal error MUST stay on the stream, so such a tool
|
|
163
|
+
keeps even its usage errors on `stdout` for one consistent channel. Never write
|
|
164
|
+
to `stderr` ad hoc — route every diagnostic through the emitter so redaction
|
|
165
|
+
and framing still apply.
|
|
166
|
+
- Emit a terminal event with the emitter's `finish` / `finish_result` helpers,
|
|
167
|
+
which map the write outcome to a broken-pipe-safe process exit code (success →
|
|
168
|
+
the given code, a hung-up reader → 0, any other write failure → a nonzero
|
|
169
|
+
output-failure code) instead of hand-rolling that dance — they work in either
|
|
170
|
+
mode (the emitter's mode decides the stream; `finish` only maps the outcome).
|
|
171
|
+
Construct an error with the error builder (`json_error(code, message)` with
|
|
172
|
+
`hint`/`retryable`/extra fields as needed) — that builder *is* the error type —
|
|
173
|
+
and pass the built event to `finish`; there is no separate error-emitting
|
|
174
|
+
convenience.
|
|
175
|
+
|
|
176
|
+
## Document workflow
|
|
177
|
+
|
|
178
|
+
Use `get`/`value`/`paths`/`keys` to read a JSON, TOML, YAML, dotenv, or INI
|
|
179
|
+
document and `set`/`add`/`remove`/`unset` to edit one in place, instead of
|
|
180
|
+
`sed`, regular expressions, or a generic reserializer — those can reorder
|
|
181
|
+
keys, drop comments, or change quoting/anchors in the rest of the file. Every
|
|
182
|
+
document command's first positional is `FILE` (or `-` for stdin on a read
|
|
183
|
+
command); mutation commands never read stdin and reject `-` as a usage error.
|
|
184
|
+
To edit only the metadata block of a Markdown page, pass `--input-format
|
|
185
|
+
toml-frontmatter` (a `+++` block) or `yaml-frontmatter` (a `---` block): the
|
|
186
|
+
same read/mutate commands then address dot-paths inside the frontmatter and
|
|
187
|
+
leave the Markdown body byte-for-byte untouched. Frontmatter is never
|
|
188
|
+
auto-detected — the format must be named explicitly, since extension detection
|
|
189
|
+
only ever resolves to a whole-file format — so reach for it deliberately
|
|
190
|
+
instead of `sed`-ing a page's `+++`/`---` header.
|
|
191
|
+
|
|
192
|
+
- `get FILE` returns the whole document as one AFDATA record
|
|
193
|
+
(`code:"document"`); `get FILE KEY` narrows it to one dot-path value in the
|
|
194
|
+
same shape. Pass every known sensitive legacy field name with
|
|
195
|
+
`--secret-name FIELD` (an exact field name, not a dot-path). A secret-named
|
|
196
|
+
leaf (`_secret`/`_SECRET`, or an exact `--secret-name` match) is still
|
|
197
|
+
redacted to `"***"` even when directly targeted — use `value
|
|
198
|
+
--reveal-secret` when the real value is genuinely needed.
|
|
199
|
+
- `value FILE KEY` writes only the raw scalar to stdout, with no AFDATA
|
|
200
|
+
envelope and no forced trailing newline, for direct use in shell
|
|
201
|
+
substitution: `value=$(afdata value config.toml server.host)`. Only scalars
|
|
202
|
+
are supported; a container errors (`document_not_scalar`) — use `get` for a
|
|
203
|
+
subtree. A secret-named leaf errors (`document_secret_redacted`) unless
|
|
204
|
+
`--reveal-secret` is passed. On any failure `value`'s stdout is always
|
|
205
|
+
empty — the error envelope goes to stderr instead — so
|
|
206
|
+
`port=$(afdata value config.toml server.port)` never captures a JSON error
|
|
207
|
+
as data. `--default VAL` prints `VAL` instead of erroring when the path is
|
|
208
|
+
absent or the value is `null` (an empty string is a real value and does not
|
|
209
|
+
trigger it), collapsing the common "read or fall back" shell idiom into one
|
|
210
|
+
call with no `2>/dev/null` guard needed.
|
|
211
|
+
- `paths FILE [KEY]` and `keys FILE [KEY]` enumerate a container's immediate
|
|
212
|
+
children, one per line, with the same empty-stdout-on-failure contract as
|
|
213
|
+
`value`. **`paths` output feeds back into afdata** (full grammar-escaped
|
|
214
|
+
dot-paths from the root — pipe straight into `value`/`get`/`unset` or
|
|
215
|
+
extend with `"$p.field"`); **`keys` output goes to external tools** (raw,
|
|
216
|
+
unescaped key names/array indices — what a package manager or another CLI
|
|
217
|
+
expects). Never use `paths`'s output as a literal name, or `keys`'s output
|
|
218
|
+
as a dot-path — they only diverge on a key containing a dot or space, so
|
|
219
|
+
the wrong one works until it silently doesn't. A scalar target errors
|
|
220
|
+
(`document_not_container`); `--missing-ok` turns a missing KEY specifically
|
|
221
|
+
into empty output + exit 0 (other errors still fail). Path grammar does not
|
|
222
|
+
escape spaces or other IFS characters, so the only correct consumption
|
|
223
|
+
shape is a `while IFS= read -r` loop, never `for x in $(...)`:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
while IFS= read -r p; do
|
|
227
|
+
afdata value config.toml "$p.slug"
|
|
228
|
+
done < <(afdata paths config.toml extra.tools)
|
|
229
|
+
```
|
|
230
|
+
- Escape a literal dot as `\.` and a literal backslash as `\\` in a dot-path;
|
|
231
|
+
every command shares this grammar (`paths`'s escaped output re-parses
|
|
232
|
+
through the exact same rules it was emitted from), so an unrecognized
|
|
233
|
+
escape or empty segment is always an error, never a guess.
|
|
234
|
+
- `add`/`remove` operate on a keyed list — an array of objects addressed by a
|
|
235
|
+
slug — and always require an explicit `--slug-field FIELD`; the CLI never
|
|
236
|
+
infers an identity field. Keyed editing is implemented for JSON and YAML
|
|
237
|
+
only; TOML, dotenv, and INI return a structured "not implemented" error, so
|
|
238
|
+
edit their list entries by writing the whole value another way. Adding a
|
|
239
|
+
slug that already exists, or removing/unsetting one that does not, is
|
|
240
|
+
always an error (`document_slug_exists`/`document_slug_not_found`/
|
|
241
|
+
`document_path_not_found`) — never a silent no-op; wrap the call yourself
|
|
242
|
+
when idempotence is wanted.
|
|
243
|
+
- `set`'s bare VALUE (and `add`'s `FIELD=VALUE`) is always a plain string —
|
|
244
|
+
no shape-guessing, so a leading-zero ID or SHA never gets reinterpreted as
|
|
245
|
+
a number. Writing an exact type goes through `--value-type
|
|
246
|
+
string|number|bool|null|json` instead; `json` is the only entry point for
|
|
247
|
+
an array or object. Overwriting an *existing* scalar of a different kind
|
|
248
|
+
with a bare VALUE is a usage error that names both escape hatches (keep the
|
|
249
|
+
type with `--value-type <kind>`, or convert explicitly with `--value-type
|
|
250
|
+
string`) — a brand-new key never needs `--value-type`.
|
|
251
|
+
- For `set`, choose `--secret-from stdin|prompt|fd:<N>|env:<VAR>` instead of a
|
|
252
|
+
positional value when the value must not land in shell history or `ps`:
|
|
253
|
+
`stdin` (pipe), `prompt` (human, terminal echo off), `fd:N` (Unix
|
|
254
|
+
automation that needs stdin for something else), or `env:VAR` (already in
|
|
255
|
+
the process environment). There is no inline `--secret-from=VALUE`-style
|
|
256
|
+
argv source — that would defeat the point.
|
|
257
|
+
- Mutations preserve the rest of the source document — comments, key order,
|
|
258
|
+
unrelated formatting — and refuse to write through a symlink or (on Unix) a
|
|
259
|
+
multi-link hardlink target. Treat a failed mutation as leaving the original
|
|
260
|
+
file untouched rather than assuming a partial write, and reread before
|
|
261
|
+
retrying. A format backend that isn't compiled in, or input outside the
|
|
262
|
+
documented dotenv/INI dialect, is a structured error to report, not a cue to
|
|
263
|
+
hand-edit the file. A successful mutation's result carries the `path`
|
|
264
|
+
actually written, for confirming which file changed when scripting several.
|
|
265
|
+
- Branch on `error.code`, not the message text: document errors use stable
|
|
266
|
+
codes (`document_path_not_found`, `document_type_mismatch`,
|
|
267
|
+
`document_slug_not_found`, `document_slug_exists`, `document_parse_failed`,
|
|
268
|
+
`document_io_failed`, and a few narrower ones), while a malformed
|
|
269
|
+
invocation (`--input-format`, `--value-type`, `FIELD=VALUE`, a mutation
|
|
270
|
+
given `-`, …) is always `document_usage_error` at exit 2 — distinct from
|
|
271
|
+
every runtime document error, which exits 1.
|
|
272
|
+
|
|
273
|
+
## Review checklist
|
|
274
|
+
|
|
275
|
+
1. Numeric fields with units carry the right suffix and safe integer/string representation.
|
|
276
|
+
2. Timestamps use `_epoch_s`, `_epoch_ms`, decimal-string `_epoch_ns`, or `_rfc3339`.
|
|
277
|
+
3. Secrets use `_secret` or configured exact secret names; no fallback path leaks them.
|
|
278
|
+
4. CLI output uses AFDATA helpers for JSON/YAML/plain formatting and structured errors, and routes by consumption mode (finite: `result`→stdout, `error`→stderr; stream: one destination) rather than writing to stderr ad hoc.
|
|
279
|
+
5. Logs use AFDATA structured logging or equivalent field-name redaction.
|
|
280
|
+
6. Transport payloads preserve the protocol envelope when AFDATA compliance is claimed.
|
|
@@ -41,6 +41,8 @@ Integer vs string is not uniform across siblings: `_epoch_s` and `_epoch_ms` are
|
|
|
41
41
|
|
|
42
42
|
`_rfc3339` needs a **mandatory** offset: `YYYY-MM-DDThh:mm:ss[.fff]` followed by `Z` or `±HH:MM`. A bare `2026-02-14T10:30:00` (no offset), a space instead of `T`, or a trailing IANA name (`...Asia/Shanghai`) is rejected by `afdata lint`. `afdata lint` also type-checks the numeric suffixes (durations `_s`/`_ms`/…, currency `_cents`/`_micro`/`_jpy`) and rejects a `_url` with internal whitespace or bare `user:pass@host` credentials.
|
|
43
43
|
|
|
44
|
+
A `null` value is exempt from every suffix type constraint above: `null` means the field is absent/unset, not present-with-the-wrong-type, so `law_repeal_at_epoch_s: null` is not a `suffix_type_mismatch`. Absence may be written as an omitted key or as an explicit `null` — both are valid. The constraint applies only once a value is present and non-null.
|
|
45
|
+
|
|
44
46
|
### Strict string formats
|
|
45
47
|
|
|
46
48
|
| Suffix | Format | Example |
|
|
@@ -225,22 +227,24 @@ Every protocol event uses `kind` plus one same-named payload:
|
|
|
225
227
|
| `"result"` | Terminal success |
|
|
226
228
|
| `"error"` | Terminal error with non-empty `error.code` and `error.message` |
|
|
227
229
|
|
|
228
|
-
Channel policy:
|
|
229
|
-
- `stdout`
|
|
230
|
-
-
|
|
231
|
-
- `stderr`
|
|
230
|
+
Channel policy — the stream follows the consumption mode, not the event shape:
|
|
231
|
+
- **Finite one-shot command (default):** `result` → `stdout`; `error`/`progress`/`log` → `stderr`. Routing follows `kind`, not the exit code, so `stdout` carries only successful payloads (`x=$(tool …)`, `tool | next`, and `tool >/dev/null` all stay safe).
|
|
232
|
+
- **Event stream (interleaved events consumed in order):** every event, including `error`, stays on one stream so ordering survives; do not split it across `stdout`/`stderr`.
|
|
233
|
+
- `--output-to <split|stdout|stderr>` (default `split`) selects the mode: `stdout`/`stderr` collapse the whole stream onto one destination. `--output` selects format, `--output-to` selects destination.
|
|
234
|
+
- Raw-scalar readers (`value`/`paths`/`keys`) are intrinsically split and reject a non-default `--output-to`.
|
|
235
|
+
- Choose the mode by how the output is consumed, not reflexively: a plain one-shot shell command uses **finite**; a tool whose output is itself an event stream (streaming rows, interleaved progress/log) or that is agent-facing and wants one structured channel uses **stream/unified** (all on `stdout`, errors included — a stream's terminal error must stay on the stream). When building a CLI on the library, emit terminal events with the emitter's `finish`/`finish_result` helpers (success → the given code, broken pipe → 0, other write failure → a nonzero output-failure code); build errors with the `json_error` builder (the error *type*, carrying hint/retryable/fields) and pass the event to `finish` — the helpers work in either mode.
|
|
232
236
|
|
|
233
237
|
Optional stream redirection:
|
|
234
|
-
- use `--stdout-file <PATH>` to redirect
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
- do not convert stderr diagnostics to JSON; Rust panics, Python tracebacks, and runtime errors stay native stderr bytes
|
|
238
|
+
- use `--stdout-file <PATH>` / `--stderr-file <PATH>` to redirect a stream's bytes to a file (process-level, beneath the channel routing above)
|
|
239
|
+
- send an event stream to a file by collapsing it (`--output-to stdout`) and redirecting (`--stdout-file <PATH>`); there is no separate events-file flag
|
|
240
|
+
- native panics/tracebacks stay raw `stderr` bytes; in finite mode `stderr` also carries the formatted AFDATA `error`/`progress`/`log` envelopes, so a reader parses JSON lines and tolerates interleaved native bytes
|
|
238
241
|
- treat this as stream destination control, not a second protocol stream and not stream copying
|
|
239
242
|
- do not implement application-level rotation
|
|
240
243
|
|
|
241
244
|
Recommended enforcement:
|
|
242
|
-
-
|
|
243
|
-
-
|
|
245
|
+
- route every `stderr` write through the emitter's diagnostic/error path; no ad-hoc stderr that bypasses the formatter in runtime code
|
|
246
|
+
- Rust: keep native panic output as the only unstructured `stderr` (no stray `eprintln!` / `std::io::stderr`)
|
|
247
|
+
- Go/Python/TypeScript: source-policy tests that fail on ad-hoc stderr APIs in runtime code, with the emitter's own sink as the sanctioned exception
|
|
244
248
|
|
|
245
249
|
### Templates
|
|
246
250
|
|
|
@@ -263,7 +267,7 @@ Startup payload fields are tool-defined; `config` is recommended, while `version
|
|
|
263
267
|
| MCP tool | JSON |
|
|
264
268
|
| SSE stream | JSONL |
|
|
265
269
|
|
|
266
|
-
All use `kind` plus the same-named payload and optional `trace`.
|
|
270
|
+
All use `kind` plus the same-named payload and optional `trace`. An event stream keeps its events on one stream (never split); a finite one-shot command splits by `kind` (`result` → stdout, `error` → stderr) — see Channel policy.
|
|
267
271
|
|
|
268
272
|
Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event and `retryable` on every error. Log and progress payloads remain tool-defined in both profiles. Use strict validation before claiming compliance with all recommendations.
|
|
269
273
|
|
|
@@ -351,4 +355,4 @@ Use this checklist when the skill is evaluating a change, not just documenting c
|
|
|
351
355
|
7. Environment variables follow `UPPER_SNAKE_CASE` with the same suffixes
|
|
352
356
|
8. One-time CLI output uses `json_log()`/`CliEmitter` + output helpers; long-running services use `afdata_tracing::try_init()` (Rust) to capture via tracing; other languages emit log events explicitly via builders
|
|
353
357
|
9. Database columns use AFDATA suffixes on generic types (`duration_ms INTEGER`, not `duration INTEGER`); native types like `TIMESTAMPTZ` don't need suffixes
|
|
354
|
-
10. CLI flag parsing uses `cli_parse_output()`/`cli_parse_log_filters()`/`build_cli_error()`/version helpers — not custom reimplementations; uses `try_parse()` not `parse()` in Rust so clap
|
|
358
|
+
10. CLI flag parsing uses `cli_parse_output()`/`cli_parse_log_filters()`/`build_cli_error()`/version helpers — not custom reimplementations; uses `try_parse()` not `parse()` in Rust so a clap/usage error becomes a structured `kind:"error"` envelope (routed to stderr under the default split, exit 2), never raw clap text
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import enum
|
|
6
|
+
import sys
|
|
6
7
|
from typing import Any, Callable, Mapping, Iterator
|
|
7
8
|
|
|
8
9
|
from agent_first_data.format import (
|
|
@@ -28,6 +29,46 @@ class OutputFormat(enum.Enum):
|
|
|
28
29
|
PLAIN = "plain"
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
class OutputTo(enum.Enum):
|
|
33
|
+
"""Where a CliEmitter sends its events, selected by ``--output-to``.
|
|
34
|
+
|
|
35
|
+
The stream an event lands on follows the program's *consumption mode*, not
|
|
36
|
+
the event's shape (see the spec's CLI Event Framing):
|
|
37
|
+
|
|
38
|
+
- ``OutputTo.SPLIT`` (the default) is finite one-shot mode: ``result`` goes
|
|
39
|
+
to stdout, while ``error``/``progress``/``log`` go to stderr. stdout
|
|
40
|
+
therefore carries only successful payloads, so a shell capture or pipe
|
|
41
|
+
never mistakes a failure for data.
|
|
42
|
+
- ``OutputTo.STDOUT`` / ``OutputTo.STDERR`` are event-stream mode: every
|
|
43
|
+
event, including ``error``, is collapsed onto that one stream so a consumer
|
|
44
|
+
reading it in order (branching on ``kind``) sees preserved ordering.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
SPLIT = "split"
|
|
48
|
+
STDOUT = "stdout"
|
|
49
|
+
STDERR = "stderr"
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def parse(cls, value: str) -> OutputTo:
|
|
53
|
+
"""Parse an ``--output-to`` value: ``split`` (default), ``stdout``, or ``stderr``.
|
|
54
|
+
|
|
55
|
+
Raises ValueError with a message suitable for build_cli_error on unknown values.
|
|
56
|
+
|
|
57
|
+
>>> OutputTo.parse("split")
|
|
58
|
+
<OutputTo.SPLIT: 'split'>
|
|
59
|
+
>>> OutputTo.parse("xml")
|
|
60
|
+
Traceback (most recent call last):
|
|
61
|
+
...
|
|
62
|
+
ValueError: unsupported --output-to 'xml'; expected split, stdout, or stderr
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
return cls(value)
|
|
66
|
+
except ValueError:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"unsupported --output-to {value!r}; expected split, stdout, or stderr"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
31
72
|
class LogFilters:
|
|
32
73
|
"""Log event filter matcher."""
|
|
33
74
|
|
|
@@ -125,7 +166,24 @@ def render(value: Any, format: OutputFormat, *, options: OutputOptions | None =
|
|
|
125
166
|
|
|
126
167
|
|
|
127
168
|
class CliEmitter:
|
|
128
|
-
"""Stateful emitter for
|
|
169
|
+
"""Stateful emitter for structured CLI executions.
|
|
170
|
+
|
|
171
|
+
The output format, redaction policy, and stream routing are fixed when the
|
|
172
|
+
emitter is created. Emitting after a terminal event, emitting a repeated
|
|
173
|
+
terminal event, and writer failures all raise.
|
|
174
|
+
|
|
175
|
+
Routing follows the consumption mode (:class:`OutputTo`):
|
|
176
|
+
|
|
177
|
+
- :meth:`finite` / :meth:`finite_with` — finite one-shot: ``result`` → the
|
|
178
|
+
primary writer (stdout), ``error``/``progress``/``log`` → the diagnostic
|
|
179
|
+
writer (stderr). The recommended default for a one-shot CLI, so shell
|
|
180
|
+
capture and pipelines never treat a failure as data.
|
|
181
|
+
- :meth:`stream` (and the plain ``CliEmitter(writer, ...)`` constructor) —
|
|
182
|
+
event stream: every event, including ``error``, goes to the single writer,
|
|
183
|
+
preserving interleaved ordering.
|
|
184
|
+
- :meth:`from_output_to` builds either shape from a parsed :class:`OutputTo`
|
|
185
|
+
selector.
|
|
186
|
+
"""
|
|
129
187
|
|
|
130
188
|
def __init__(
|
|
131
189
|
self,
|
|
@@ -133,13 +191,86 @@ class CliEmitter:
|
|
|
133
191
|
format: OutputFormat,
|
|
134
192
|
output_options: OutputOptions | None = None,
|
|
135
193
|
log_fields: Callable[[], Mapping[str, Any]] | None = None,
|
|
194
|
+
*,
|
|
195
|
+
diagnostic: Any | None = None,
|
|
136
196
|
) -> None:
|
|
197
|
+
"""Create an event-stream emitter: every event goes to ``writer``.
|
|
198
|
+
|
|
199
|
+
This is the unified/stream form (alias :meth:`stream`). Pass
|
|
200
|
+
``diagnostic`` — or use :meth:`finite`/:meth:`finite_with` — for finite
|
|
201
|
+
one-shot mode, which routes ``result`` to ``writer`` and every diagnostic
|
|
202
|
+
(``error``/``progress``/``log``) to ``diagnostic``.
|
|
203
|
+
"""
|
|
137
204
|
self._writer = writer
|
|
205
|
+
self._diagnostic = diagnostic
|
|
138
206
|
self._format = format
|
|
139
207
|
self._output_options = output_options or OutputOptions()
|
|
140
208
|
self._terminal_emitted = False
|
|
141
209
|
self._log_fields_provider = log_fields
|
|
142
210
|
|
|
211
|
+
@classmethod
|
|
212
|
+
def stream(
|
|
213
|
+
cls,
|
|
214
|
+
writer: Any,
|
|
215
|
+
format: OutputFormat,
|
|
216
|
+
output_options: OutputOptions | None = None,
|
|
217
|
+
log_fields: Callable[[], Mapping[str, Any]] | None = None,
|
|
218
|
+
) -> CliEmitter:
|
|
219
|
+
"""Create an event-stream emitter: every event, including ``error``, goes
|
|
220
|
+
to the single ``writer``, preserving interleaved ordering. Pick this when
|
|
221
|
+
the consumer reads one ordered stream and branches on ``kind``.
|
|
222
|
+
"""
|
|
223
|
+
return cls(writer, format, output_options, log_fields)
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def finite_with(
|
|
227
|
+
cls,
|
|
228
|
+
result_writer: Any,
|
|
229
|
+
diagnostic: Any,
|
|
230
|
+
format: OutputFormat,
|
|
231
|
+
output_options: OutputOptions | None = None,
|
|
232
|
+
log_fields: Callable[[], Mapping[str, Any]] | None = None,
|
|
233
|
+
) -> CliEmitter:
|
|
234
|
+
"""Create a finite one-shot emitter with explicit sinks: ``result`` goes
|
|
235
|
+
to ``result_writer``, while ``error``/``progress``/``log`` go to
|
|
236
|
+
``diagnostic``.
|
|
237
|
+
"""
|
|
238
|
+
return cls(result_writer, format, output_options, log_fields, diagnostic=diagnostic)
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def finite(
|
|
242
|
+
cls,
|
|
243
|
+
format: OutputFormat,
|
|
244
|
+
output_options: OutputOptions | None = None,
|
|
245
|
+
log_fields: Callable[[], Mapping[str, Any]] | None = None,
|
|
246
|
+
) -> CliEmitter:
|
|
247
|
+
"""Create a finite one-shot emitter wired to the process streams:
|
|
248
|
+
``result`` → ``sys.stdout``, ``error``/``progress``/``log`` →
|
|
249
|
+
``sys.stderr``. The recommended default for a one-shot CLI.
|
|
250
|
+
"""
|
|
251
|
+
return cls.finite_with(sys.stdout, sys.stderr, format, output_options, log_fields)
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def from_output_to(
|
|
255
|
+
cls,
|
|
256
|
+
selector: OutputTo,
|
|
257
|
+
format: OutputFormat,
|
|
258
|
+
output_options: OutputOptions | None = None,
|
|
259
|
+
log_fields: Callable[[], Mapping[str, Any]] | None = None,
|
|
260
|
+
) -> CliEmitter:
|
|
261
|
+
"""Build an emitter from a parsed :class:`OutputTo` selector, wired to the
|
|
262
|
+
process streams: ``SPLIT`` is finite mode (``result`` → stdout,
|
|
263
|
+
everything else → stderr); ``STDOUT``/``STDERR`` are event-stream mode
|
|
264
|
+
onto that one stream.
|
|
265
|
+
"""
|
|
266
|
+
if selector is OutputTo.SPLIT:
|
|
267
|
+
return cls.finite_with(sys.stdout, sys.stderr, format, output_options, log_fields)
|
|
268
|
+
if selector is OutputTo.STDOUT:
|
|
269
|
+
return cls.stream(sys.stdout, format, output_options, log_fields)
|
|
270
|
+
if selector is OutputTo.STDERR:
|
|
271
|
+
return cls.stream(sys.stderr, format, output_options, log_fields)
|
|
272
|
+
raise ValueError(f"unsupported OutputTo selector: {selector!r}")
|
|
273
|
+
|
|
143
274
|
def with_log_fields(self, provider: Callable[[], Mapping[str, Any]]) -> CliEmitter:
|
|
144
275
|
"""Set a provider callable for default log event fields.
|
|
145
276
|
|
|
@@ -177,8 +308,14 @@ class CliEmitter:
|
|
|
177
308
|
merged_log.update(log_payload)
|
|
178
309
|
envelope["log"] = merged_log
|
|
179
310
|
|
|
180
|
-
|
|
181
|
-
|
|
311
|
+
# Finite mode (a diagnostic sink is present) splits by kind: `result`
|
|
312
|
+
# stays on the primary writer (stdout), while `error`/`progress`/`log`
|
|
313
|
+
# are diagnostics routed to the diagnostic writer (stderr). Event-stream
|
|
314
|
+
# mode (no diagnostic sink) keeps every event on the single writer.
|
|
315
|
+
use_diagnostic = kind != "result" and self._diagnostic is not None
|
|
316
|
+
sink = self._diagnostic if use_diagnostic else self._writer
|
|
317
|
+
sink.write(render(envelope, self._format, options=self._output_options) + "\n")
|
|
318
|
+
flush = getattr(sink, "flush", None)
|
|
182
319
|
if flush is not None:
|
|
183
320
|
flush()
|
|
184
321
|
if kind in ("result", "error"):
|
|
@@ -219,6 +356,34 @@ class CliEmitter:
|
|
|
219
356
|
event = json_log({"level": level.value, "message": message}).build()
|
|
220
357
|
self.emit(event)
|
|
221
358
|
|
|
359
|
+
def finish(self, event: Event | dict, success_code: int) -> int:
|
|
360
|
+
"""Emit ``event`` as the terminal event and resolve the outcome to a
|
|
361
|
+
process exit code, so a one-shot CLI need not hand-roll the
|
|
362
|
+
emit-then-exit dance.
|
|
363
|
+
|
|
364
|
+
Returns ``success_code`` on a successful write; ``0`` if the write failed
|
|
365
|
+
because the reader hung up (broken pipe); ``4`` on any other emit,
|
|
366
|
+
validation, or write failure. A library never calls :func:`sys.exit`
|
|
367
|
+
itself — return this code from the program's entry point.
|
|
368
|
+
"""
|
|
369
|
+
try:
|
|
370
|
+
self.emit(event)
|
|
371
|
+
except BrokenPipeError:
|
|
372
|
+
return 0
|
|
373
|
+
except Exception:
|
|
374
|
+
return 4
|
|
375
|
+
return success_code
|
|
376
|
+
|
|
377
|
+
def finish_result(self, payload: Any) -> int:
|
|
378
|
+
"""Convenience over :meth:`finish`: emit a ``result`` payload and return
|
|
379
|
+
``0`` on success.
|
|
380
|
+
|
|
381
|
+
Errors have no matching convenience: build the event through the error
|
|
382
|
+
builder — ``json_error(code, message).hint_if_some(hint)...build()``, which
|
|
383
|
+
also carries ``retryable`` and extra fields — and pass it to
|
|
384
|
+
:meth:`finish` with the desired exit code."""
|
|
385
|
+
return self.finish(json_result(payload).build(), 0)
|
|
386
|
+
|
|
222
387
|
|
|
223
388
|
def build_cli_version(version: str) -> dict:
|
|
224
389
|
"""Build a standard CLI version result event.
|