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
266 changes: 238 additions & 28 deletions format-stream.jq
Original file line number Diff line number Diff line change
@@ -1,37 +1,247 @@
# Formats claude -p --output-format stream-json events into clean,
# human-readable lines for a live terminal. Fed raw NDJSON on stdin.
#
# Stateful: walks the stream via foreach so tool_result lines can be traced
# back to the tool_use that produced them (matched by tool_use_id), which is
# what lets Read/Glob/Grep/Bash results be recognized and condensed below.

def truncate(n):
if (. | length) > n then .[0:n] + "..." else . end;

def tool_summary:
if .name == "Bash" then (.input.command // "" | tostring | truncate(150))
else (.input | tostring | truncate(150))
def strip_ansi:
gsub("\\[[0-9;]*[a-zA-Z]"; "");
Comment on lines +11 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sample=$'\033[?25lhidden\033[0m'

printf '%s' "$sample" |
  jq -Rr 'gsub("\u001b\\[[0-9;]*[a-zA-Z]"; "")' |
  od -An -tx1

printf '%s' "$sample" |
  jq -Rr 'gsub("\u001b\\[[0-?]*[ -/]*[`@-`~]"; "")' |
  od -An -tx1

Repository: c0d3ster/nightlight

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '(^|/)(format-stream\.jq|overnight\.sh)$' || true

printf '\nRelevant snippets:\n'
if [ -f format-stream.jq ]; then
  nl -ba format-stream.jq | sed -n '1,25p'
fi
if [ -f overnight.sh ]; then
  nl -ba overnight.sh | sed -n '120,140p'
fi

printf '\nPattern occurrences:\n'
rg -n 'gsub\("\u001b|\[0-9;\]\*|\[0-?]\*\|private|CSI|strip_ansi' format-stream.jq overnight.sh || true

printf '\nBehavioral probe with Python (no repo code/exec):\n'
python3 - <<'PY'
import re
samples = [
    "\033[?25lhidden\033[0m",
    "\033[?1049h\033[2J\033[H",
    "\033[38;2;255;128;0mcolored",
]
patterns = {
    "current": re.compile(r"\x1b\[[0-9;]*[a-zA-Z]"),
    "complete": re.compile(r"\x1b\[[0-?]*[ -/]*[`@-`~]"),
}
for sample in samples:
    print(f"INPUT={sample!r}")
    for name, re_pat in patterns.items():
        print(f"  {name}={re_pat.sub('', sample)!r}")
PY

Repository: c0d3ster/nightlight

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Relevant snippets:\n'
if [ -f format-stream.jq ]; then
  line=1
  while IFS= read -r content || [ -n "$content" ]; do
    printf '%d:%s\n' "$line" "$content"
    [ "$line" -ge 25 ] && break
    line=$((line + 1))
  done < format-stream.jq
fi
if [ -f overnight.sh ]; then
  line=1
  while IFS= read -r content || [ -n "$content" ]; do
    printf '%d:%s\n' "$line" "$content"
    [ "$line" -ge 140 ] && break
    line=$((line + 1))
  done < overnight.sh
fi

printf '\nPattern occurrences:\n'
rg -n 'gsub\("\u001b|\[0-9;\]\*|\[0-?]\*\|private|CSI|strip_ansi' format-stream.jq overnight.sh nightly.sh .github 2>/dev/null || true

printf '\nBehavioral probe with Python:\n'
python3 - <<'PY'
import re

samples = [
    "\033[?25lhidden\033[0m",
    "\033[?1049h\033[2J\033[H",
    "\033[38;2;255;128;0mcolored",
]

patterns = {
    "current": re.compile(r"\x1b\[[0-9;]*[a-zA-Z]"),
    "complete": re.compile(r"\x1b\[[0-?]*[ -/]*[`@-`~]"),
}

for sample in samples:
    print(f"INPUT={sample!r}")
    for name, regex in patterns.items():
        print(f"  {name}={regex.sub('', sample)!r}")
PY

Repository: c0d3ster/nightlight

Length of output: 8535


Remove private CSI escape sequences in both log paths.

Use the complete CSI regex in format-stream.jq and the matching error handler in overnight.sh so private sequences such as ESC[?25l do not remain in the logs.

📍 Affects 2 files
  • format-stream.jq#L11-L12 (this comment)
  • overnight.sh#L130-L130
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@format-stream.jq` around lines 11 - 12, Update strip_ansi in format-stream.jq
(format-stream.jq lines 11-12) and the matching error handler in overnight.sh
(overnight.sh line 130) to use the complete CSI escape-sequence pattern,
including optional private-mode markers such as ?. Ensure both log paths remove
sequences like ESC[?25l.


def strip_cr:
gsub("\r"; "");

# git's own core.autocrlf notice - fires on add/commit/checkout for any file
# with mixed line endings, never actionable, pure noise.
def strip_git_crlf_warnings:
split("\n")
| map(select(test("^warning: in the working copy of .*LF will be replaced by CRLF") | not))
| join("\n");

# drops everything from the first "diff --git" marker onward - the actual
# diff is redundant with what the PR itself will show, and a unified diff
# in the middle of a status/commit summary is pure noise here.
def strip_git_diff_body:
(split("\n")) as $lines |
(reduce $lines[] as $line
({out: [], in_diff: false};
if ($line | test("^diff --git ")) then {out: .out, in_diff: true}
elif .in_diff then .
else {out: (.out + [$line]), in_diff: false}
end)
).out | join("\n");

def non_empty_lines:
split("\n") | map(select(length > 0));

# aligns continuation lines under the " < " prefix (6 chars) so multi-line
# results read as one indented block instead of falling back to column 0.
def indent_continuations:
gsub("\n"; "\n ");

def tool_summary($name; $input):
if $name == "Bash" then
($input.command // "" | tostring | strip_ansi) as $cmd |
if ($cmd | test("git\\s+commit\\b.*-m\\s+\"")) then
"git commit: " + ($cmd | capture("-m\\s+\"(?<msg>[^\\n\"]*)").msg | truncate(150))
else
($cmd | truncate(150))
end
else ($input | tostring | strip_ansi | truncate(150))
end;

if .type == "system" and .subtype == "init" then
"=== session started (model: \(.model)) ==="
elif .type == "assistant" then
(.message.content[]? |
if .type == "text" then
.text
elif .type == "tool_use" then
" > " + .name + "(" + tool_summary + ")"
else empty
end)
elif .type == "user" then
(.message.content[]? |
if .type == "tool_result" then
(if (.content | type) == "array" then
(.content | map(.text? // "") | join(" "))
# vitest: pull the "Test Files"/"Tests" summary lines plus each FAIL entry,
# regardless of where they land in the (already tail-truncated) output.
def summarize_vitest:
(split("\n")) as $lines |
($lines | map(select(test("^\\s*(Test Files|Tests)\\s")))) as $summary_lines |
($lines
| map(select(test("^\\s*FAIL\\s+\\S+\\s+")))
| map(capture("^\\s*FAIL\\s+\\S+\\s+(?<rest>.+)$").rest)
) as $failed |
($summary_lines | join("\n")) as $summary |
if ($failed | length) > 0 then
$summary + "\n" + ($failed | map(" FAILED " + .) | join("\n"))
else
$summary
end;

# tsc: each "<file>:<line>:<col> - error TSxxxx: ..." line plus the closing
# "Found N errors in M files." line.
def summarize_tsc:
(split("\n")) as $lines |
($lines | map(select(test("^\\S+\\.tsx?:[0-9]+:[0-9]+ - error TS")))) as $errors |
($lines | map(select(test("^Found [0-9]+ errors? in"))) | first) as $found_line |
($errors | map(" " + .) | join("\n")) as $body |
if $found_line then $found_line + "\n" + $body else $body end;

# eslint (stylish formatter): each file header groups its own indented
# "<line>:<col> error|warning message rule" lines; track the current file
# as we walk so each problem line can be reattached to it.
def summarize_eslint:
(split("\n")) as $lines |
(reduce $lines[] as $line
({file: null, out: []};
if ($line | test("^\\S.*\\.(ts|tsx|js|jsx|mjs|cjs)$")) then
{file: $line, out: .out}
elif ($line | test("^\\s+[0-9]+:[0-9]+\\s+(error|warning)\\s")) then
.out += [(.file // "?") + " " + ($line | ltrimstr(" "))]
else
(.content | tostring)
end) as $text |
" < " + ($text | truncate(200))
else empty
end)
elif .type == "result" then
"=== session done: \(.subtype) | $\((.total_cost_usd * 10000 | round) / 10000) | \((.duration_ms / 1000) | floor)s | \(.num_turns) turns ==="
else
empty
end
.
end)
) as $acc |
($lines | map(select(test("^✖ [0-9]+ problems? \\("))) | first) as $summary_line |
($acc.out | map(" " + .) | join("\n")) as $body |
if $summary_line then $summary_line + "\n" + $body else $body end;

# git status --short: condense the "XY <path>" lines into a count per status
# code (modified/added/deleted/renamed/untracked); any non-status lines in
# the same output (e.g. a chained "git log --oneline") pass through as-is.
def summarize_git_status:
(non_empty_lines) as $lines |
($lines | map(select(test("^[ MADRCU?]{2}\\s\\S")))) as $status_lines |
($lines | map(select(test("^[ MADRCU?]{2}\\s\\S") | not))) as $other_lines |
(if ($status_lines | length) == 0 then null
else
($status_lines | map(.[0:2])) as $codes |
([
{k: "modified", n: ($codes | map(select(contains("M"))) | length)},
{k: "added", n: ($codes | map(select(contains("A"))) | length)},
{k: "deleted", n: ($codes | map(select(contains("D"))) | length)},
{k: "renamed", n: ($codes | map(select(contains("R"))) | length)},
{k: "untracked", n: ($codes | map(select(. == "??")) | length)}
] | map(select(.n > 0)) | map("\(.n) \(.k)") | join(", "))
end) as $status_summary |
([$status_summary] + $other_lines | map(select(. != null)) | join("\n"));

# git commit: keep just the "[branch hash] subject" confirmation line and the
# diffstat summary, dropping the lefthook/commitlint box-drawing noise and
# per-file "create mode"/"delete mode" listing in between.
def summarize_git_commit:
(split("\n")) as $lines |
($lines | map(select(test("^\\[\\S+ [0-9a-f]{6,}\\] "))) | first) as $commit_line |
($lines | map(select(test("[0-9]+ files? changed"))) | first) as $diffstat_line |
([$commit_line, $diffstat_line] | map(select(. != null)) | join("\n"));

# true for a Bash command that's purely dumping/listing file contents -
# chains of cat/ls/echo/head/tail joined by "&&"/";"/"||" (optionally behind
# "cd ... &&", "2>/dev/null" fallbacks, and "| head -N"/"| tail -N" trims).
# Same noise-vs-signal tradeoff as the Read tool suppression above: the
# request line already shows what was inspected, so the dump adds nothing.
def is_pure_inspect($cmd):
($cmd | sub("^cd\\s+\"[^\"]*\"\\s*&&\\s*"; "")) as $rest |
([$rest | splits("\\s*(&&|\\|\\||;)\\s*")]) as $parts |
($parts | length) > 0 and
($parts | all(
test("^(cat|ls|echo|head|tail|find)\\b[^|<>]*(\\s+2>\\s*(/dev/null|&1))?(\\s*\\|\\s*(head|tail)\\b[^|<>]*)?$")
or
test("^(tasklist|ps)\\b[^|<>]*(\\s+2>\\s*(/dev/null|&1))?(\\s*\\|\\s*grep\\b[^|<>]*)?$")
));

def summarize_bash:
. as $text |
if ($text | test(" Test Files ")) then summarize_vitest
elif ($text | test("error TS[0-9]+:")) then summarize_tsc
elif ($text | test("✖ [0-9]+ problems? \\(")) then summarize_eslint
elif ($text | test("Ready in [0-9.]+\\s*m?s")) then
(split("\n") | map(select(test("Ready in [0-9.]+\\s*m?s"))) | first)
else
(non_empty_lines) as $lines |
($lines | map(select(test("^\\[[A-Z]+\\]"))) | length) as $tagged |
if $tagged >= 2 and
($lines[-1] | test("success|complete|done|failed|error"; "i") or test("[✅❌]")) then
$lines[-1]
else
($lines | if length > 5 then .[-5:] else . end | join("\n") | truncate(300))
end
end;

# recognizes a Bash-invoked "grep <pattern> ..." (optionally behind "cd ...
# &&" and a trailing "| head/tail -N"); returns the pattern so it gets the
# same one-line match-count summary as the dedicated Grep tool, instead of
# dumping every matched line.
def bash_grep_pattern($cmd):
($cmd | sub("^cd\\s+\"[^\"]*\"\\s*&&\\s*"; "")) as $rest |
($rest | sub("\\s*\\|\\s*(head|tail)\\b.*$"; "")) as $core |
if ($core | test("^grep\\s")) then
($core | capture("^grep\\s+(-[a-zA-Z]+\\s+)*\"(?<pat>[^\"]*)\"").pat // null)
else
null
end;

def tool_result_text($content):
if ($content | type) == "array" then
($content | map(.text? // "") | join(" "))
else
($content | tostring)
end;

def format_event($event; $tools):
if $event.type == "system" and $event.subtype == "init" then
"=== session started (model: \($event.model)) ==="
elif $event.type == "assistant" then
($event.message.content[]? |
if .type == "text" then
.text
elif .type == "tool_use" then
" > " + .name + "(" + tool_summary(.name; .input) + ")"
else empty
end)
elif $event.type == "user" then
($event.message.content[]? |
if .type == "tool_result" then
($tools[.tool_use_id] // {}) as $tool |
($tool.name // "") as $tool_name |
(tool_result_text(.content) | strip_ansi | strip_cr | strip_git_crlf_warnings | strip_git_diff_body) as $text |
if $tool_name == "Read" then
empty
elif $tool_name == "Glob" then
($text | non_empty_lines | length) as $n |
" < found " + ($n | tostring) +
(if $n == 1 then " file matching \"" else " files matching \"" end) +
($tool.input.pattern // "?") + "\""
elif $tool_name == "Grep" then
($text | non_empty_lines | length) as $n |
($tool.input.output_mode == "files_with_matches") as $is_files |
" < " + ($n | tostring) +
(if $is_files then (if $n == 1 then " file" else " files" end)
else (if $n == 1 then " match" else " matches" end) end) +
" for \"" + ($tool.input.pattern // "?") + "\""
elif $tool_name == "Bash" and is_pure_inspect($tool.input.command // "") then
empty
Comment on lines +199 to +214

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'is_error|tool_name == "Read"|is_pure_inspect' format-stream.jq overnight.sh

Repository: c0d3ster/nightlight

Length of output: 2951


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- format-stream.jq relevant sections ---'
sed -n '1,80p;110,145p;175,235p' format-stream.jq

echo
echo '--- overnight.sh relevant sections ---'
sed -n '100,135p' overnight.sh

Repository: c0d3ster/nightlight

Length of output: 9127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"1","is_error":true,"content":"Read failed: missing file at /tmp/missing.txt"}]}}
' | jq -r -f format-stream.jq

printf '%s\n' '{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"2","is_error":true,"content":"bash -i exited 1"}], "previous_event_id":"old"}}, {
"type":"assistant","message":{"content":[{"type":"tool_use","id":"1","name":"Read","input":{"file":"/tmp/missing.txt"}}]}
' | jq -n  --slurp -r -f format-stream.jq

Repository: c0d3ster/nightlight

Length of output: 283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '{
"type":"assistant","message":{"content":[{"type":"tool_use","id":"1","name":"Read","input":{"file":"/tmp/missing.txt"}}]}
},{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"1","is_error":true,"content":"Read failed: missing file at /tmp/missing.txt"}]}}
' | jq -r -f format-stream.jq

printf '%s\n' '{
"type":"assistant","message":{"content":[{"type":"tool_use","id":"2","name":"Bash","input":{"command":"cat /tmp/missing.txt"}}]}
},{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"2","is_error":true,"content":"cat: /tmp/missing.txt: No such file or directory"}]}}
' | jq -r -f format-stream.jq

printf '%s\n' '{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"3","is_error":true,"content":"permission denied"}]}}
' | jq -r -f format-stream.jq

Repository: c0d3ster/nightlight

Length of output: 400


Expose failed inspection tool errors before suppression.

When a correlated tool_result has .is_error == true, the Read and pure-inspection Bash branches currently emit no live output while overnight.sh only appends the error after stream processing finishes. Let failed inspection results write through, e.g. in a shared first check, so the live readable log does not hide actionable tool failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@format-stream.jq` around lines 199 - 214, Update the tool-result branching in
format-stream.jq to handle .is_error == true before the Read and pure-inspection
Bash suppression branches. Write failed correlated inspection results through to
the live readable output, while preserving suppression for successful Read and
pure-inspection Bash results and existing formatting for Glob and Grep.

elif $tool_name == "Bash" then
($tool.input.command // "") as $cmd |
(bash_grep_pattern($cmd)) as $grep_pat |
if $grep_pat then
($text | non_empty_lines | length) as $n |
" < " + ($n | tostring) + (if $n == 1 then " match" else " matches" end) +
" for \"" + $grep_pat + "\""
elif ($cmd | test("git\\s+commit\\b")) then
" < " + ($text | summarize_git_commit | indent_continuations)
elif ($cmd | test("git\\s+status\\b.*(--short|-s)\\b")) then
" < " + ($text | summarize_git_status | indent_continuations)
else
" < " + ($text | summarize_bash | indent_continuations)
end
else
" < " + ($text | truncate(200) | indent_continuations)
end
else empty
end)
elif $event.type == "result" then
"=== session done: \($event.subtype) | $\(($event.total_cost_usd * 10000 | round) / 10000) | \(($event.duration_ms / 1000) | floor)s | \($event.num_turns) turns ==="
else
empty
end;

foreach (., inputs) as $event
( {};
if $event.type == "assistant" then
reduce ($event.message.content[]? | select(.type == "tool_use")) as $tu
(.; .[$tu.id] = {name: $tu.name, input: $tu.input})
else . end;
format_event($event; .)
)
15 changes: 15 additions & 0 deletions overnight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ run_repo() {
| jq -r -f format-stream.jq \
| tee "$readable_log"

# Append genuine tool/harness errors (is_error results - permission denials,
# bad exit codes, missing files) to the same errlog used for the claude
# process's own stderr, then drop the file entirely if nothing landed in it.
jq -r '
select(.type == "user") | .message.content[]? |
select(.type == "tool_result" and .is_error == true) |
(if (.content | type) == "array" then
(.content | map(.text? // "") | join(" "))
else
(.content | tostring)
end) |
gsub("\\[[0-9;]*[a-zA-Z]"; "")
' "$raw_log" >> "$errlog"
[[ -s "$errlog" ]] || rm -f "$errlog"

update_stats "$name" "$raw_log"
}

Expand Down