A CLI tool that converts structured JSONL event streams from the pi AI agent harness into human-readable log output for CI workflows.
When AI agents run in non-interactive CI sessions, they emit structured JSON events to stdout. This tool pipes those JSONL lines into readable formatted logs, showing the agent's thinking, responses, and tool calls.
go build -o forhumans-cli ./cmd/...Pipe JSONL event stream to stdin:
cat output.jsonl | ./forhumans-cliOr in CI:
./agent-harness | ./forhumans-cli--lifecycle— Show AGENT START/END and TURN START/END events (default: false)
# Show full lifecycle structure
cat output.jsonl | ./forhumans-cli --lifecycle
# Minimal output (default)
cat output.jsonl | ./forhumans-cliEach log line follows the format: HH:MM:SS [TAG] content
| Tag | Description | Example |
|---|---|---|
[SESSION] |
Session metadata | [SESSION] id=680697ea cwd=/Users/awa/Source/forhumans |
[AGENT START/END] |
Agent lifecycle | [AGENT START] |
[TURN START/END] |
Conversation turn boundaries | [TURN START] |
[USER] |
User message | [USER] run |
[THINKING] |
LLM reasoning (truncated at 200 chars) | [THINKING] User says "run". Probably want to list files... |
[RESPONSE] |
LLM response | [RESPONSE] Here's the output... |
[TOOL CALL] |
Tool invocation with arguments | [TOOL CALL] bash({"command":"ls -R ."}) |
[TOOL START] |
Tool execution beginning | [TOOL START] bash args={...} |
[TOOL END] |
Tool execution complete with result | [TOOL END] bash error=false result=output.jsonl |
[UNKNOWN] |
Unknown event type (raw JSON) | [UNKNOWN] {"type":"custom_event",...} |
The formatter skips delta events and intermediate updates to keep output clean:
*_deltaevents (thinking_delta, text_delta)*_start/*_updateintermediate events (except tool_execution_start/end)- Non-user message_start events
Unknown event types are printed as [UNKNOWN] with the raw JSON, allowing visibility into new event types without code changes.
Responses and other multi-line content are automatically indented to align continuation lines with the start of the content on the first line:
11:59:02 [RESPONSE] Created **`random_numbers.json`** with a JSON array:
```json
[42, 7, 19]
```
This improves readability when responses span multiple lines, especially in CI logs with many concurrent messages.
The project uses clean package separation:
internal/events/— Event types and JSON unmarshaling, timestamp parsing, text extractioninternal/formatter/— Formatting logic, truncation, output generationcmd/main.go— CLI entry point, JSONL reader
Run all tests:
go test -v ./...Coverage includes:
- 40 unit tests across both packages
- All event types (session, agent, turn, user, thinking, response, tool call/execution)
- Edge cases (empty content, truncation, newlines, unknown types)
- Timestamp parsing (ISO 8601 and milliseconds epoch)
- Text extraction and block filtering
$ cat output.jsonl | ./forhumans-cli
09:06:19 [SESSION] id=680697ea cwd=/Users/awa/Source/forhumans
09:06:19 [USER] list files
09:06:20 [THINKING] User wants to see files. I'll use bash ls.
09:06:20 [TOOL CALL] bash({"command":"ls -R ."})
09:06:20 [TOOL START] bash args={"command":"ls -R ."}
09:06:20 [TOOL END] bash error=false result=output.jsonl random_numbers.json
09:06:21 [RESPONSE] Here are the files in the directory...$ cat output.jsonl | ./forhumans-cli --lifecycle
09:06:19 [SESSION] id=680697ea cwd=/Users/awa/Source/forhumans
09:06:19 [AGENT START]
09:06:19 [TURN START]
09:06:19 [USER] list files
09:06:20 [THINKING] User wants to see files. I'll use bash ls.
09:06:20 [TOOL CALL] bash({"command":"ls -R ."})
09:06:20 [TOOL START] bash args={"command":"ls -R ."}
09:06:20 [TOOL END] bash error=false result=output.jsonl random_numbers.json
09:06:21 [RESPONSE] Here are the files in the directory...
09:06:21 [TURN END]
09:06:21 [AGENT END]09:06:19 [TURN START]
09:06:19 [USER] do something
09:06:19 [UNKNOWN] {"type":"custom_event","data":"..."}
09:06:20 [RESPONSE] Done
09:06:21 [TURN END]
- Add handling in
internal/formatter/formatter.go'sFormat()method - Add unit tests in
internal/formatter/formatter_test.go - Run
go test -v ./...to verify
Unknown types automatically fall through to the default case, which prints raw JSON.
None — uses only Go standard library.