A tiny NDJSON protocol for batch CLIs to report their progress — so any observer can draw the bar, send the failure notification and keep the history, without knowing anything about the work.
Think of the tracking barcode on a parcel. Every parcel carries different contents — books, food, clothes — but one tracking screen works for all of them, because the status format is shared, not the contents. shinchoku is that barcode for batch work: crawls, exports, transcodes, backups, AI inference. The observer understands the envelope (seven event types); the contents (your domain) stay yours.
stdout is the transport. Pipes, SSH, a WebSocket relay — anything that carries lines carries the protocol unchanged.
printf '{"v":1,"event":"done"}\n' is a valid producer. The libraries are convenience, not the contract.
Non-JSON lines are plain logs. Unknown events and fields are ignored. Legacy tools are already valid producers.
Domain words travel as values, never as vocabulary. That's why one consumer works for every tool.
Different question. OpenTelemetry answers "what is happening across my services?" — traces, metrics and logs, pushed over the network to a collector for later analysis. shinchoku answers "how far along is this one process?" — narrated on stdout to whoever spawned it.
| shinchoku | OpenTelemetry | |
|---|---|---|
| unit of interest | one run of one process | a fleet of services |
| progress (current/total) | first-class event | no such concept |
| transport | stdout → parent process | network → collector / backend |
| producer cost | printf is enough | SDK + exporter (+ collector) |
| crash handling | exit code is authoritative | tail telemetry may be lost |
They compose rather than compete: a consumer may forward events into an OTel pipeline. In the parcel metaphor — shinchoku is the tracking barcode; OpenTelemetry is the logistics company's back office. See SPEC §10, Non-goals.
shinchoku combines four old, proven ideas and adds none of its own. The only contribution is the combination: the same old transport, one tolerant, domain-neutral vocabulary — so a single consumer can finally serve every tool.
A process reports by writing plain lines on stdout; the reader parses them tolerantly. Decades of proof — but there the vocabulary is bound to pass/fail.
Progress, artifacts and annotations as specially formatted lines a runner turns into UI. The right vocabulary — but each dialect belongs to one vendor.
Machine-readable events streamed one JSON line at a time. The right transport — but every tool invents a private vocabulary, so no consumer crosses tools.
The child narrates its state; the parent holds the verdict. "The exit code decides" comes straight from this tradition.
| event | fields | meaning |
|---|---|---|
| start | title?, total? | announces the run; should be first |
| progress | current, total?, msg? | units completed so far |
| log | level, msg | info / warn / error (non-fatal) |
| metric | key, value | named counter shown verbatim |
| artifact | path, label? | an output file exists — offer it |
| done | summary? | finished successfully; last event |
| failed | msg | fatal failure; last event |
Reserved for the future: ask, row,
heartbeat. Additions never bump "v" — the
consumer rules below make them compatible by construction.
Emitters for Rust, Go, Node, PHP and Python live in this repository — each one a zero-dependency*, ~100-line convenience wrapper. (*Rust uses serde.)
# Cargo.toml [dependencies] shinchoku = "0.1" use shinchoku::Event; Event::start().title("import").total(120).emit(); Event::progress(3).total(120).emit(); Event::done().summary("4,520 items").emit(); // consumers: features = ["parse"] → tolerant parse_line()
// go get github.com/uiuifree/shinchoku/go
import shinchoku "github.com/uiuifree/shinchoku/go"
shinchoku.Start("import", 120)
shinchoku.Progress(3, 120)
shinchoku.Done("4,520 items")
// npm install shinchoku (ESM)
import * as shinchoku from "shinchoku";
shinchoku.start({ title: "import", total: 120 });
shinchoku.progress(3, 120);
shinchoku.done("4,520 items");
// composer require shinchoku/shinchoku
use Shinchoku\Shinchoku;
Shinchoku::start('import', 120);
Shinchoku::progress(3, 120);
Shinchoku::done('4,520 items');
# pip install shinchoku
import shinchoku
shinchoku.start(title="import", total=120)
shinchoku.progress(3, total=120)
shinchoku.done("4,520 items")
event → ignore or show as a log. Never an error.failed seen → synthesize a failure. The protocol narrates the run; the exit code judges it.These five rules are why adding events is a compatible change, and why a consumer that only implements rule 5 is already a valid, minimal consumer. Everything else — bars, metrics, artifact buttons — is progressive enhancement.