shinchoku.

進捗(しんちょく) — Japanese for "progress"

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.

$ your-cli --out result.jsonl {"v":1,"event":"start","title":"import","total":120} ← a panel is born {"v":1,"event":"progress","current":3,"total":120} ← the bar moves {"v":1,"event":"log","level":"warn","msg":"row 42 skipped"} ← the log turns yellow {"v":1,"event":"artifact","path":"result.jsonl"} ← a save button appears {"v":1,"event":"done","summary":"4,520 items"} ← notify & record

Why a protocol?

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.

Pipe-native

stdout is the transport. Pipes, SSH, a WebSocket relay — anything that carries lines carries the protocol unchanged.

printf-compliant

printf '{"v":1,"event":"done"}\n' is a valid producer. The libraries are convenience, not the contract.

Tolerant by contract

Non-JSON lines are plain logs. Unknown events and fields are ignored. Legacy tools are already valid producers.

Seven events, that's all

Domain words travel as values, never as vocabulary. That's why one consumer works for every tool.

Isn't this OpenTelemetry's job?

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.

shinchokuOpenTelemetry
unit of interestone run of one processa fleet of services
progress (current/total)first-class eventno such concept
transportstdout → parent processnetwork → collector / backend
producer costprintf is enoughSDK + exporter (+ collector)
crash handlingexit code is authoritativetail 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.

Nothing here is new — on purpose

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.

From test harnesses

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.

From CI runners

Progress, artifacts and annotations as specially formatted lines a runner turns into UI. The right vocabulary — but each dialect belongs to one vendor.

From --json modes

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.

From process supervisors

The child narrates its state; the parent holds the verdict. "The exit code decides" comes straight from this tradition.

The vocabulary

eventfieldsmeaning
starttitle?, total?announces the run; should be first
progresscurrent, total?, msg?units completed so far
loglevel, msginfo / warn / error (non-fatal)
metrickey, valuenamed counter shown verbatim
artifactpath, label?an output file exists — offer it
donesummary?finished successfully; last event
failedmsgfatal failure; last event

Reserved for the future: ask, row, heartbeat. Additions never bump "v" — the consumer rules below make them compatible by construction.

One protocol, five helpers

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")

Consumer rules — tolerance is the contract

  1. A line that isn't valid JSON → show it as a plain log. Never an error.
  2. Unknown event → ignore or show as a log. Never an error.
  3. Unknown fields → ignore.
  4. stderr → plain logs.
  5. The exit code decides. Non-zero exit with no 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.