Files
amethyst/.claude/skills/amy-expert/references/output-conventions.md
T
Claude 6ef0c372b1 docs(cli): make USAGE.md the README; move contract material to DEVELOPMENT
USAGE.md was the better README — entry-point users want examples and
quick start, not the public-API contract. Flip them and refresh the
amy-expert skill so it matches the post-refactor reality.

cli/README.md (was USAGE.md):
- Install, quick start, seven worked examples, full command reference,
  output modes, multi-account workflows, agent recipes, troubleshooting.
- Cross-refs point at DEVELOPMENT.md for the contract / architecture
  and ROADMAP.md for what's coming.

cli/DEVELOPMENT.md absorbs the old README's architecture sections:
- New "Public contract" section at the top — the stable promises
  (text-default + --json contract, stderr for humans, exit codes,
  ~/.amy/ as the world).
- "Local event store" deep-dive with the cache-helper API.
- "Relay routing" rules table.
- "Full on-disk layout" tree with annotations.

cli/ROADMAP.md, cli/USAGE.md:
- ROADMAP cross-refs collapsed (no more USAGE.md row).
- USAGE.md deleted — content lives in README now.

.claude/skills/amy-expert refreshed end-to-end:
- SKILL.md description + Rules 2 and 4 rewritten for the dual-output
  contract (text default, --json opt-in) and the ~/.amy/ layout.
- "Where things live" listing matches the current source tree
  (Output.kt, Aliases.kt, UseCommand.kt, secrets/, all the new
  command files).
- "Common mistakes" lists the new traps: don't read user.home
  directly, don't add a global flag that collides with subcommand
  --name, don't use Json.writeLine (it's gone).
- references/command-template.md uses Output.emit / Output.error
  (Json.writeLine / Json.error helpers no longer exist).
- references/output-conventions.md rewritten around the dual-mode
  contract — same JSON shape rules, but framed as "this is what
  --json emits" rather than "this is stdout."
2026-04-25 16:35:10 +00:00

143 lines
4.7 KiB
Markdown

# Output conventions
amy ships a dual-output contract. Default stdout is human-readable
text (a YAML-ish render of the underlying result map); `--json` flips
stdout to a single JSON object per success. The text shape can drift;
the `--json` shape is the public API.
Commands always emit via `Output.emit(mapOf(...))`. The map IS the
JSON shape — the renderer in `Output.kt` derives the text from the
same map. Don't write two render paths; write one map and let
`Output` pick.
## Channels
| Stream | Default mode | `--json` mode |
|---|---|---|
| **stdout** | YAML-ish text from `Output.emit(...)` | Exactly one JSON object per successful invocation |
| **stderr** | Human progress logs, warnings, per-relay ACK traces, stack traces, `printUsage()` output, errors as `error: <code>: <detail>` | Same logs, plus errors as `{"error":...,"detail":...}` |
If a command needs to emit structured data for machines, it goes on
stdout and is automatically JSON under `--json`. If it needs to
explain what it's doing to a human watching, stderr.
## Exit codes
| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Runtime error. |
| `2` | Bad arguments. |
| `124` | `await` timed out. |
Throw the right exception type in commands:
- `IllegalArgumentException` → exit 2 automatically.
- `AwaitTimeout` → exit 124 automatically.
- Anything else → exit 1.
The top-level `main()` in `Main.kt` handles the translation. Don't
try-catch at the command level unless you're converting a third-party
exception into one of the above.
## `--json` object shape
### Top-level
Always an object. Never an array, never a primitive, never a
newline-delimited stream.
```json
{ "event_id": "...", "kind": 1, "published_to": [...] }
```
### Keys
- Stable snake_case.
- Additive evolution is safe; renaming or removing a key is a
breaking change.
- Don't nest unnecessarily. `{"data":{...}}` is noise.
### Identifiers
| Thing | Form |
|---|---|
| Event ID | 64-char lowercase hex string. Key name: `event_id`. |
| Pubkey (primary subject) | hex **and** bech32. Keys: `pubkey` + `npub`. |
| Pubkey (secondary reference) | hex only. Key: `pubkey`. |
| Relay URL | Normalized string (`wss://…`). Never an object. |
| Timestamps | Unix seconds, integer. Key names end in `_at`. The text renderer auto-formats these as `2026-04-25 13:42:11Z (8m ago)`. |
| Group ID (Marmot) | Hex string. Key: `group_id`. |
| Byte counts | Integer. Key names end in `_bytes`. The text renderer auto-formats these as `8.7 KiB`. |
### Collections
- Pluralise: `messages`, `members`, `admins`, `events`.
- Always an array (possibly empty), never `null`.
- Order: oldest-first unless there's a good reason otherwise — state
it in the key name (`messages_newest_first`) if you flip it.
### Booleans
- Use `true`/`false` in the result map. The text renderer prints them
as `yes`/`no` (green/red); `--json` keeps the literal booleans.
- Name keys so `true` is the expected/successful state:
`is_member`, `published`, `accepted`.
### Publish results
When a command publishes an event, the canonical output shape is:
```json
{
"event_id": "<hex>",
"kind": 1,
"published_to": ["wss://relay.a/", "wss://relay.b/"],
"rejected_by": ["wss://relay.c/"]
}
```
`published_to` is relays that ACK'd `true`. `rejected_by` is relays
that ACK'd `false`. Relays that didn't answer before the timeout
appear in neither — add `timed_out_on` if you need to surface them.
### Error shape
Default mode (text):
```text
error: not_member: <gid>
```
Under `--json`:
```json
{ "error": "not_member", "detail": "<gid>" }
```
- `error` is a short, stable, lower_snake code. Agents can branch on
it.
- `detail` is free text — OK to change between versions.
- Common codes today: `bad_args`, `no_identity`, `no_account`,
`exists`, `bad_key`, `not_member`, `no_dm_relays`, `timeout`,
`runtime`. Reuse before inventing.
Use `Output.error("code", "detail")` from commands; it picks the
right channel and format based on the active mode.
## Never
- `println(...)` of anything except `Output.emit(...)`.
- `Json.writeLine` / `Json.error` — that helper is gone; use the
`Output` object instead.
- Multi-line JSON (pretty-printed) under `--json`. One line, always.
- Mixing stdout lines — one command invocation emits one stdout line
in `--json` mode. If you need progress updates, they go on stderr.
- Machine output to stderr. The whole point is clean separation.
- Silent fallbacks — if a relay rejects your publish, say so in the
result map.
- Building text rendering by hand. Trust the `Output.kt` renderer:
it handles alignment, colour, byte/timestamp formatting, nested
maps and lists. If you need a bespoke render for one command,
pass a custom render lambda — don't go around `Output`.