Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Default canonical labels: `needs-triage`, `needs-info`, `ready-for-agent`, `read

Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.

### Writing style

Standards for help text, the README, and SKILL.md; inline code in help prose is always backticked (test-enforced). See `docs/agents/writing-style.md`.

## Development

Setup, from-source install, the design-doc map, and the release process live in `CONTRIBUTING.md`. The README is user-facing only.
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ no Python required.
```

- `docs/telemetry.md`, `docs/user-agent.md` — wire-visible contracts.
- `docs/agents/writing-style.md` — standards for help text, the README,
and SKILL.md. Backticked inline code in help prose is enforced by
`tests/test_help_style.py`; the hosted docs generate their CLI
reference from `help --json`, so these strings are customer-facing.

## Releasing

Expand Down
40 changes: 40 additions & 0 deletions docs/agents/writing-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Writing style

Standards for every string a user or an agent reads: help text and
docstrings (which become `ade help` and the hosted CLI reference), the
README, and SKILL.md.

## Prose standards

- Second person ("you"), active voice, direct language.
- No promotional language. Describe what a thing does.
- No idioms. Plain wording wins.
- Cite what a reader outside this repo can open: public docs, `ade help`
topics, and shipped flags.
- Leave out issue numbers (enforced), internal URLs, and anything that
resolves only inside LandingAI.
- Test every code example before it lands. Fence README and SKILL.md
code blocks with a language tag.
- Error and status messages state what happened and what to do next.

## Inline code is always backticked (enforced)

Wrap flags (`--api-key`), commands (`ade update`), paths
(`~/.ade/credentials.json`), environment variables (`ADE_ENV`), and
literal values (`priority`) in backticks wherever they appear in prose.

Why: the hosted docs generate the CLI reference from `help --json` and
render a bare `--flag` as an em dash plus the flag name; agents need
unambiguous literal boundaries; and terminal output prints backticks
literally, which is already this CLI's voice.

`tests/test_help_style.py` enforces the flag rule across the help
surface's prose fields. Pre-formatted blocks are exempt: topic bodies
and usage lines are terminal layouts and copy-pasteable commands, so
leave them bare.

## Relationship to the docs repo

The docs repo (landing-ai/docs) has stricter rules for hosted pages.
This file governs text that lives in this repo; the docs repo's
generator is the compatibility layer between the two.
86 changes: 43 additions & 43 deletions docs/reference/help.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/ade_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ def login(
environment: str | None = typer.Option(None, "--env", help=_ENV_HELP),
as_json: bool = JSON_FLAG,
) -> None:
"""Ensure the target environment is logged in; --api-key
"""Ensure the target environment is logged in; `--api-key`
authenticates with a key directly ('-' prompts with hidden input).
Targets --env, else $ADE_ENV, else production — nothing is stored
Targets `--env`, else $ADE_ENV, else production — nothing is stored
about the choice."""
home = ade_home()
resolved = resolve_target(home, environment, as_json=as_json)
Expand Down Expand Up @@ -870,7 +870,7 @@ def logout(
),
as_json: bool = JSON_FLAG,
) -> None:
"""Log out of one environment (the resolved target by default); --all
"""Log out of one environment (the resolved target by default); `--all`
clears every environment. Idempotent; OAuth refresh tokens are revoked
best-effort first."""
home = ade_home()
Expand Down
4 changes: 2 additions & 2 deletions src/ade_cli/crop.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ def crop(
None, "--download/--no-download",
help="URL-parsed items: fetch the document from its recorded URL "
"into the job item and crop from that copy — the parse itself "
"never gives the CLI the bytes (#169). This happens "
"never gives the CLI the bytes. This happens "
"automatically when no copy is attached yet (a notice and "
"progress line land on stderr); --no-download skips the fetch, "
"progress line land on stderr); `--no-download` skips the fetch, "
"and the crop then fails honestly (a crop has no empty-imagery "
"fallback).",
),
Expand Down
2 changes: 1 addition & 1 deletion src/ade_cli/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def extract(
help=f"Environment to run against: {', '.join(ENVIRONMENTS)} "
"(default: $ADE_ENV, then production). The JOB_ITEM_ID form inherits the "
"parse item's environment instead — its server-side parse job only "
"exists there — and a conflicting --env is refused.",
"exists there — and a conflicting `--env` is refused.",
),
wait: float = typer.Option(600.0, "--wait", help="Poll budget in seconds."),
force: bool = typer.Option(
Expand Down
4 changes: 2 additions & 2 deletions src/ade_cli/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def find(
None,
metavar="[JOB_ITEM_ID] [QUERY]",
help="Job item id (or unambiguous prefix) to search, then an "
"optional case-insensitive substring QUERY. With --job, the one "
"optional case-insensitive substring QUERY. With `--job`, the one "
"allowed positional is the QUERY.",
),
job_tokens: list[str] = typer.Option(
Expand Down Expand Up @@ -61,7 +61,7 @@ def find(
as_json: bool = JSON_FLAG,
id_only: bool = ID_ONLY_FLAG,
) -> None:
"""Search parsed elements locally: `find JOB_ITEM_ID [QUERY]`, or --job
"""Search parsed elements locally: `find JOB_ITEM_ID [QUERY]`, or `--job`
(repeatable) for several items; no query lists every element.

Ids discovered here are what `view --element-id` deep-links and
Expand Down
48 changes: 24 additions & 24 deletions src/ade_cli/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,11 @@
("stored", "false when the result could not be persisted"),
("store_dir", "absolute path of the job item folder"),
("artifacts", "artifact filenames written there"),
("markdown", "the parse markdown — only with --include markdown"),
("elements", "the flat projection — only with --include elements"),
("kept_copy", "with --keep-copy: whether the URL document's "
("markdown", "the parse markdown — only with `--include markdown`"),
("elements", "the flat projection — only with `--include elements`"),
("kept_copy", "with `--keep-copy`: whether the URL document's "
"copy was stored in the job item"),
("keep_copy_error", "with --keep-copy: why the copy could not "
("keep_copy_error", "with `--keep-copy`: why the copy could not "
"be stored (the parse itself still succeeded)"),
],
},
Expand Down Expand Up @@ -240,7 +240,7 @@
("text", "the element's markdown slice"),
],
"note": "One record per match, in document order; [] when nothing "
"matched. --id-only prints the element ids alone.",
"matched. `--id-only` prints the element ids alone.",
},
"crop": {
"shape": "object",
Expand All @@ -255,28 +255,28 @@
"document into the parse item before cropping (absent when "
"nothing needed fetching)"),
],
"note": "One shape whatever matched: a single --element-id is "
"count 1 with one crops[] record; a filter (--type/--page/--all) "
"note": "One shape whatever matched: a single `--element-id` is "
"count 1 with one crops[] record; a filter (`--type`/`--page`/`--all`) "
"matching nothing is count 0 with crops [].",
},
"view": {
"shape": "object",
"keys": [
("status", "'viewed' ('cropped' with --crop, 'synced' with --sync-viewers)"),
("status", "'viewed' ('cropped' with `--crop`, 'synced' with `--sync-viewers`)"),
("job_item_id", "the item rendered"),
("kind", "parse | extract"),
("path", "the self-contained view.html (or the PNG, with --crop)"),
("path", "the self-contained view.html (or the PNG, with `--crop`)"),
("built", "true when this run rebuilt the artifact"),
("pages_embedded", "pages inlined; the rest load from sidecars"),
("note", "why the render weakened, when it did (else null)"),
("downloaded", "URL items: true when this run fetched the "
"document into the job item (automatic on first view). false "
"when the automatic fetch failed (see download_error) or "
"explicit --download found the copy already attached; absent "
"explicit `--download` found the copy already attached; absent "
"when nothing needed fetching"),
("download_error", "why the automatic fetch failed, when it "
"did (the viewer still builds, previews empty; else absent)"),
("deep_link", "view.html#element=... when --element-id was given"),
("deep_link", "view.html#element=... when `--element-id` was given"),
("history_items", "items in the rebuilt sidebar read model"),
("sidebar_sync", "true when sibling viewers build in the background"),
],
Expand All @@ -293,14 +293,14 @@
("parse", "extract items: the referenced parse — {job_item_id, "
"run_id (the parse generation extracted against), missing}"),
("stale", "extract items: true when the referenced parse was "
"--force re-run after this extraction"),
"`--force` re-run after this extraction"),
("created_at / completed_at", "epoch seconds (null when unknown)"),
],
"note": "Ordered newest submission first (timestamp-less items "
"last), matching the viewer sidebar; --asc restores oldest-first. "
"Capped at the newest 100 items by default — --limit N adjusts, "
"--all lifts the cap, and a capped run says so up front (the "
"first line of the listing; on stderr for --json). The --json "
"last), matching the viewer sidebar; `--asc` restores oldest-first. "
"Capped at the newest 100 items by default — `--limit` N adjusts, "
"`--all` lifts the cap, and a capped run says so up front (the "
"first line of the listing; on stderr for `--json`). The `--json` "
"array follows the same order and cap.",
},
"history clear": {
Expand Down Expand Up @@ -376,7 +376,7 @@
("cleared", "false when there was nothing stored to clear"),
("revoked", "refresh tokens revoked best-effort"),
("scope", "environment | all"),
("environment", "the environment cleared (null with --all)"),
("environment", "the environment cleared (null with `--all`)"),
],
},
"version": {
Expand Down Expand Up @@ -455,32 +455,32 @@
CONVENTIONS = [
(
"--json",
"Every command supports --json: one stable JSON object/array on "
"Every command supports `--json`: one stable JSON object/array on "
"stdout (errors and pending payloads follow the same rule). Agents "
"should always pass it. Each command's published shape is its "
"'result' block below — the full result is always on stdout, never "
"only in a file.",
),
(
"--id-only",
"parse, extract, and find also take --id-only: just the id(s), one "
"per line, for piping (JOB=$(ade parse -d f.pdf --id-only)). Errors "
"parse, extract, and find also take `--id-only`: just the id(s), one "
"per line, for piping (`JOB=$(ade parse -d f.pdf --id-only)`). Errors "
"and hints go to stderr so a captured id is never a sentence.",
),
(
"job item ids",
"Store commands take a job item id or an unambiguous prefix. "
"Discover ids with `history list`; ambiguous or unknown ids error "
"with candidates listed. Distinct from the server-side run id: "
"--json payloads report that as run_id, and on-disk records spell "
"`--json` payloads report that as run_id, and on-disk records spell "
"the same value job_id (the wire's name) — neither is ever a "
"job item id.",
),
(
"guarantees",
"parse and extract ensure a run exists rather than fire a request: "
"an already-done run is served from disk free with an explicit "
"notice (--force consents to a re-bill); a pending run is resumed, "
"notice (`--force` consents to a re-bill); a pending run is resumed, "
"never resubmitted; Ctrl-C stops the waiting, not the work.",
),
(
Expand Down Expand Up @@ -516,7 +516,7 @@
"path": " meta.json",
"what": "commit record: kind, source, identity, params, state, "
"timestamps, artifact index. Its job_id field is the server-side "
"run id (= run_id in --json payloads), never the job item id",
"run id (= run_id in `--json` payloads), never the job item id",
},
{
"path": " job.json",
Expand Down Expand Up @@ -757,7 +757,7 @@ def help_command(
"""Print the whole-surface command reference in one call: every
command and flag, the output convention, each verb's result shape,
exit states, and the store layout. The agent bootstrap — run this
(with --json) before anything else. `help TOPIC` prints one
(with `--json`) before anything else. `help TOPIC` prints one
conceptual page instead (workflow, output, credentials, errors)."""
root = ctx.find_root().command
assert isinstance(root, TyperGroup)
Expand Down
4 changes: 2 additions & 2 deletions src/ade_cli/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"--limit",
min=1,
help=f"Keep only the newest N items (default {DEFAULT_LIST_LIMIT}), "
"whatever the order; --all lifts the cap.",
"whatever the order; `--all` lifts the cap.",
)
LIST_ALL_FLAG = typer.Option(
False, "--all", help="List every stored job item — no limit."
Expand Down Expand Up @@ -140,7 +140,7 @@ def list_items(
as_json: bool = JSON_FLAG,
) -> None:
"""List stored job items: id, kind, state, env, params, source —
the newest 100 submissions first (--limit/--all adjust, --asc for
the newest 100 submissions first (`--limit`/`--all` adjust, `--asc` for
oldest first). Extract items referencing a parse item indent
beneath it. Bare `ade history` defaults to this command."""
jobs = store.JobStore(ade_home())
Expand Down
4 changes: 2 additions & 2 deletions src/ade_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ def _force_utf8_stdio() -> None:
app.command(
"login",
help="Alias of `ade auth login`: ensure the target environment is "
"logged in; --api-key authenticates with a key directly ('-' prompts "
"logged in; `--api-key` authenticates with a key directly ('-' prompts "
"with hidden input).",
)(login)
app.command(
"logout",
help="Alias of `ade auth logout`: log out of one environment (the "
"resolved target by default); --all clears every environment.",
"resolved target by default); `--all` clears every environment.",
)(logout)
app.command()(parse)
app.command()(extract)
Expand Down
4 changes: 2 additions & 2 deletions src/ade_cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
False,
"--id-only",
help="Print only the id(s) this run produced, one per line — the "
"piping mode (JOB=$(ade parse -d f.pdf --id-only)). Takes precedence "
"over --json; errors and hints go to stderr.",
"piping mode (JOB=$(ade parse -d f.pdf `--id-only`)). Takes precedence "
"over `--json`; errors and hints go to stderr.",
)

# Machine-readable exit states, shared by every command: pending is a normal
Expand Down
8 changes: 4 additions & 4 deletions src/ade_cli/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ def parse(
ctx: typer.Context,
document: Path | None = typer.Option(
None, "-d", "--document", exists=True, dir_okay=False, readable=True,
help="Local document file to parse; pass exactly one of -d/--document or --document-url.",
help="Local document file to parse; pass exactly one of -d/`--document` or `--document-url`.",
),
document_url: str | None = typer.Option(
None, "--document-url",
help="Document URL the server fetches (identity is the URL x params; "
"a re-run dedups even if the remote content changed — --force refreshes).",
"a re-run dedups even if the remote content changed — `--force` refreshes).",
),
model: str = typer.Option(
DEFAULT_MODEL, "--model", help="Parse model registry version."
Expand Down Expand Up @@ -138,7 +138,7 @@ def parse(
"password: always rejected (422) — decrypt PDFs before upload. "
"Example: '{\"inline_markdown\": true, \"blocks\": {\"table\": "
"{\"format\": \"markdown\"}}}'. "
"Merges with --pages; giving pages in both is an error.",
"Merges with `--pages`; giving pages in both is an error.",
),
environment: str | None = typer.Option(
None, "--env",
Expand All @@ -154,7 +154,7 @@ def parse(
),
keep_copy: bool = typer.Option(
False, "--keep-copy",
help="--document-url only: also download the document into the "
help="`--document-url` only: also download the document into the "
"job item (plain HTTP, no API credits) so page previews and "
"crops render locally — fetched now, while the URL (often "
"pre-signed) still works. Without it, the first `view`/`crop` "
Expand Down
16 changes: 8 additions & 8 deletions src/ade_cli/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,18 +970,18 @@ def view(
crop: bool = typer.Option(
False,
"--crop",
help="With --element-id: render that element's PNG crop instead of "
help="With `--element-id`: render that element's PNG crop instead of "
"the HTML artifact.",
),
open_browser: bool | None = typer.Option(
None,
"--open/--no-open",
help="Open the result in the browser. Default: open when stdout is "
"a terminal; --json runs and piped output never auto-open.",
"a terminal; `--json` runs and piped output never auto-open.",
),
dpi: int | None = typer.Option(
None, "--dpi", min=1,
help=f"Page render dpi (default {DEFAULT_DPI}); with --crop, the crop "
help=f"Page render dpi (default {DEFAULT_DPI}); with `--crop`, the crop "
f"dpi (default {DEFAULT_CROP_DPI}).",
),
pages: str | None = typer.Option(
Expand All @@ -991,10 +991,10 @@ def view(
None, "--download/--no-download",
help="URL-parsed items: fetch the document from its recorded URL "
"into the job item and render page previews from that copy — "
"the parse itself never gives the CLI the bytes (#169). This "
"the parse itself never gives the CLI the bytes. This "
"happens automatically when no copy is attached yet (a notice "
"and progress line land on stderr); --no-download skips the "
"fetch and previews stay empty. Explicit --download makes a "
"and progress line land on stderr); `--no-download` skips the "
"fetch and previews stay empty. Explicit `--download` makes a "
"failed fetch an error instead of a warning. Plain HTTP, no API "
"credits; the copy is unverified against the parsed run. Also "
"works on an extract item id (fetches into its referenced parse "
Expand All @@ -1009,11 +1009,11 @@ def view(
help="Open via a local server (http://127.0.0.1) instead of file:// "
"— browser zoom then covers every viewer natively. Starts the "
"server if needed; reuses a running one. It retires itself after "
"30 idle minutes, or immediately with --stop-server.",
"30 idle minutes, or immediately with `--stop-server`.",
),
stop_server: bool = typer.Option(
False, "--stop-server",
help="Stop the local viewer server (started by --serve) and exit.",
help="Stop the local viewer server (started by `--serve`) and exit.",
),
sync_viewers: bool = typer.Option(
False, "--sync-viewers", hidden=True,
Expand Down
Loading
Loading