I usually ask the agent to store updates to the code in a dedicated format in daily MD files.
After a while, however, the agent with Deepseek V4 Flash 0731 forgot the instructions and started storing the trace logs in random MD files, sometimes in existing files and sometimes in new files stored in /tmp.
I fixed this by forcing the agent to create a CI script that required a commentary for the log. Then the CI script stored the log in the expected format and file.
I share my fix with you, but I'm not sure how you want the harness to manage this with the agent.
# BASH :: ci.sh
#!/usr/bin/env sh
# project local CI — shared by pre-commit/pre-push hooks and humans.
#
# Usage: scripts/ci.sh {lint|types|frontend|test|e2e|e2e-fast|spec-inventory|docs-links|logbook|all}
# lint ruff check + format check + docs-links + spec-inventory + logbook
# (fast; used by pre-commit)
# types pyright type checking
# frontend frontend typecheck + lint + tests + build
# test pytest unit/contract tests
# e2e Playwright browser tests (boots an isolated backend; needs Redis)
# e2e-fast quick E2E iteration: failure stack + ONE healthy spec
# (default: navigation; pass a spec name as second arg, e.g.
# scripts/ci.sh e2e-fast scenes-board)
# spec-inventory every e2e *.spec.ts must have a row in the docs inventory
# docs-links every relative .md link in docs/ must resolve to an existing file
# logbook every .agent/LOGBOOKS/*.md entry header must be
# '[UTC timestamp] [agent] [task] [event type]' (LOGBOOK.md)
# all lint + types + frontend + test (used by pre-push)
#
# LOG COMMENTARY (mandatory for lint and all):
# The lint (pre-commit) and all (pre-push) targets REQUIRE a short free-form
# commentary describing what was done/updated since the last CI execution.
# Pass it via --log "<text>" or the CI_LOG_COMMENT env var (the pre-commit/
# pre-push hooks inherit it from `git commit` / `git push`). Without it the
# target FAILS with:
# "ci: FAIL - a log commentary about what has been updated is mandatory and
# expected to validate the CI"
# On success ci.sh APPENDS a properly formatted entry (header '[UTC timestamp]
# [agent] [task] [event type]' + the commentary body) to today's day file
# .agent/LOGBOOKS/<YYYY-MM-DD>.md (rule: .agent/LOGBOOK.md), and inside the
# pre-commit hook stages that day file so the entry rides along with the
# commit. Optional tag overrides: --agent NAME (default: buffy), --task TEXT
# (default: current commit subject), --event TYPE (default mapped from the
# task prefix, e.g. feat->implementation, fix->bugfix, docs->documentation,
# test->testing, refactor->refactoring, chore->maintenance, perf->performance,
# else implementation). Env equivalents: CI_LOG_AGENT, CI_LOG_TASK,
# CI_LOG_EVENT.
#
# NOTE: `all` deliberately excludes `e2e`: it boots servers and needs Redis, so
# it stays an explicit, opt-in target rather than part of the fast pre-push
# loop. Run `scripts/ci.sh e2e` to include the browser suite.
#
# Every step degrades gracefully while there is no source/tests/deps yet, so
# the hooks never block an empty repository bootstrap.
set -u
# --- Self-snapshot: run from a stable private copy ---------------------------
# dash reads script files incrementally while executing. If this file is
# rewritten IN PLACE by a parallel process during a long run (e.g. an editor
# or a concurrent agent touching scripts/ci.sh while `all` runs its several-
# minute suites), dash's tail reads see mixed old/new content and die with a
# spurious 'Syntax error: ";;" unexpected' at EOF even though every check
# passed. Re-exec from a snapshot so the running instance always parses the
# exact content it started with, whatever happens to the working-tree file.
if [ "${NE_CI_SNAPSHOT:-}" != "1" ]; then
NE_CI_SNAPSHOT=1
NE_CI_SELF="$0"
SNAPSHOT="$(mktemp "${TMPDIR:-/tmp}/ci.XXXXXX.sh")" || exit 1
NE_CI_SNAPSHOT_PATH="$SNAPSHOT"
NE_CI_ROOT="$(cd "$(dirname "$0")/.." && pwd 2>/dev/null || pwd)"
export NE_CI_SNAPSHOT NE_CI_SELF NE_CI_SNAPSHOT_PATH NE_CI_ROOT
if ! cat "$0" > "$SNAPSHOT" 2>/dev/null; then
rm -f "$SNAPSHOT"
exit 1
fi
chmod +x "$SNAPSHOT" 2>/dev/null || true
exec "$SNAPSHOT" "$@"
fi
trap 'rm -f "${NE_CI_SNAPSHOT_PATH:-}"' EXIT
ROOT="${NE_CI_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
VENV="$ROOT/.venv/bin"
cd "$ROOT" || exit 1
# --- CLI arg parsing (target + optional --log/--agent/--task/--event) -------
target="${1:-all}"
shift 2>/dev/null || true
LOG_COMMENT=""
LOG_AGENT=""
LOG_TASK=""
LOG_EVENT=""
E2E_SPEC=""
while [ "$#" -gt 0 ]; do
case "$1" in
--log) LOG_COMMENT="${2:-}"; shift 2 2>/dev/null || break ;;
--agent) LOG_AGENT="${2:-}"; shift 2 2>/dev/null || break ;;
--task) LOG_TASK="${2:-}"; shift 2 2>/dev/null || break ;;
--event) LOG_EVENT="${2:-}"; shift 2 2>/dev/null || break ;;
*)
if [ -z "$E2E_SPEC" ]; then
E2E_SPEC="$1"
else
echo "ci: unexpected argument: $1" >&2
exit 2
fi
shift
;;
esac
done
# Env-var fallbacks (the pre-commit/pre-push hooks inherit these from
# `git commit` / `git push`): --log beats CI_LOG_COMMENT, etc.
LOG_COMMENT="${LOG_COMMENT:-${CI_LOG_COMMENT:-}}"
LOG_AGENT="${LOG_AGENT:-${CI_LOG_AGENT:-buffy}}"
LOG_TASK="${LOG_TASK:-${CI_LOG_TASK:-}}"
LOG_EVENT="${LOG_EVENT:-${CI_LOG_EVENT:-}}"
count_py() {
find src tests scripts -name '*.py' -type f 2>/dev/null | wc -l
}
count_tests() {
find tests \( -name 'test_*.py' -o -name '*_test.py' \) -type f 2>/dev/null | wc -l
}
logbook_reminder() {
# Unmissable nudge for AI dev agents: every pre-commit/pre-push run prints
# the logbook rule, so a session cannot forget to record its trace log in
# today's day file (rule: .agent/LOGBOOK.md). Printed unconditionally.
echo ""
echo "/!\ IMPORTANT RULE: save the trace log in today's logbook file as defined in .agent/LOGBOOK.md"
echo " day file: .agent/LOGBOOKS/<YYYY-MM-DD>.md (UTC day)"
echo " entry format: [UTC timestamp] [agent] [task] [event type] + what happened / why / next action"
echo " lint/all now REQUIRE a commentary: pass --log \"<text>\" or CI_LOG_COMMENT='<text>'"
echo " (ci.sh appends it formatted to today's day file and stages it in pre-commit)"
echo ""
}
# --- Mandatory log commentary (lint = pre-commit, all = pre-push) -----------
# The gate targets refuse to run without a commentary describing what was done
# since the last CI execution, so an AI agent cannot commit/push without
# logging (rule: .agent/LOGBOOK.md). On success the entry is appended to the
# day file in the exact tagged format the cmd_logbook guard validates.
require_ci_log() {
# Fail hard when a gate target runs without a (non-whitespace) commentary.
if [ -n "$LOG_COMMENT" ] && \
[ -n "$(printf '%s' "$LOG_COMMENT" | tr -d '[:space:]' 2>/dev/null)" ]; then
return 0
fi
echo ""
echo "ci: FAIL - a log commentary about what has been updated is mandatory and expected to validate the CI" >&2
echo " describe what was done/updated since the last CI execution and re-run with the commentary, e.g.:" >&2
echo " CI_LOG_COMMENT='implemented X, fixed Y' git commit -m '...' (hooks inherit this env var)" >&2
echo " CI_LOG_COMMENT='implemented X, fixed Y' git push" >&2
echo " scripts/ci.sh $target --log \"implemented X, fixed Y\" (direct runs)" >&2
echo " ci.sh will store it formatted in .agent/LOGBOOKS/<YYYY-MM-DD>.md (see .agent/LOGBOOK.md)" >&2
echo "" >&2
if [ -n "$LOG_TASK" ] || [ -n "$LOG_EVENT" ]; then
echo "ci: note: --task/--event were given but --log was not; the commentary text is the required part." >&2
fi
return 1
}
log_event_from_task() {
# Map a task/commit-subject prefix to a logbook event type.
case "$1" in
feat*|add*) echo "implementation" ;;
fix*) echo "bugfix" ;;
docs*) echo "documentation" ;;
test*) echo "testing" ;;
refactor*) echo "refactoring" ;;
chore*) echo "maintenance" ;;
perf*) echo "performance" ;;
*) echo "implementation" ;;
esac
}
current_commit_subject() {
# Best available task label, in order of preference:
# 1. an explicit --task/CI_LOG_TASK
# 2. the first line of the commentary (self-describing; also the only
# reliable source inside the pre-commit hook — git does NOT rewrite
# .git/COMMIT_EDITMSG with the new message before pre-commit runs, so
# it would yield the PREVIOUS commit's subject)
# 3. the latest commit subject (pre-push, where the commit already exists)
# 4. a generic label
if [ -n "$LOG_TASK" ]; then
echo "$LOG_TASK"
return 0
fi
if [ -n "$LOG_COMMENT" ]; then
subject="$(printf '%s' "$LOG_COMMENT" | head -n1 | cut -c1-120)"
[ -n "$subject" ] && { echo "$subject"; return 0; }
fi
subject="$(git log -1 --format=%s 2>/dev/null)"
if [ -n "$subject" ]; then
echo "$subject"
return 0
fi
echo "ci run"
}
append_logbook_entry() {
# Persist the mandatory commentary as a properly formatted entry in today's
# day file. Called ONLY after the gate target fully succeeded. Inside the
# pre-commit hook the day file is staged so the entry rides the commit.
[ -n "$LOG_COMMENT" ] || return 0
[ -d ".agent/LOGBOOKS" ] || return 0
day="$(date -u +%Y-%m-%d)"
logfile=".agent/LOGBOOKS/$day.md"
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
agent="${LOG_AGENT:-buffy}"
task="$(current_commit_subject)"
# Tags must be single-line and free of ']' to satisfy the logbook guard.
agent="$(printf '%s' "$agent" | tr -d '\r\n]' | cut -c1-64)"
task="$(printf '%s' "$task" | tr -d '\r\n]' | cut -c1-120)"
event="${LOG_EVENT:-$(log_event_from_task "$task")}"
event="$(printf '%s' "$event" | tr -d '\r\n]' | cut -c1-32)"
header="[$ts] [$agent] [$task] [$event]"
# Avoid duplicate entries when a re-run carries the same commentary: use the
# first line of the commentary as a stable signature (multi-line-safe).
sig="$(printf '%s' "$LOG_COMMENT" | head -n1 | cut -c1-120)"
if [ -f "$logfile" ] && [ -n "$sig" ] && grep -Fq -- "$sig" "$logfile"; then
echo "ci: logbook entry already present (identical commentary) - not appending again"
else
# Create the day file with its standard header on first entry of the day.
if [ ! -f "$logfile" ]; then
printf '%s\n' "<!-- ./.agent/LOGBOOKS/$day.md -->" > "$logfile"
printf '%s\n' "# Agent Log - $day" >> "$logfile"
printf '%s\n' "" >> "$logfile"
printf '%s\n' "Per-day agent log file. Rule and entry format: see \`../LOGBOOK.md\`." >> "$logfile"
printf '%s\n' "" >> "$logfile"
fi
{
printf '%s\n\n' "---"
printf '%s\n' "$header"
# Body lines starting with '[' must be indented: the logbook guard treats
# any line starting with '[' as a tagged entry header, so a raw '[x]' line
# in the commentary would fail the next cmd_logbook run.
printf '%s\n' "$LOG_COMMENT" | tr -d '\r' | while IFS= read -r bline; do
case "$bline" in
\[*) printf ' %s\n' "$bline" ;;
*) printf '%s\n' "$bline" ;;
esac
done
printf '%s\n' ""
} >> "$logfile"
echo "ci: logbook entry appended to $logfile ($header)"
fi
# Stage inside the pre-commit hook so the entry travels with the commit (also
# on the dedupe path: a manual run may have already appended the entry).
if [ "${NE_CI_IS_HOOK:-}" = "pre-commit" ]; then
git add "$logfile" 2>/dev/null || true
fi
return 0
}
cmd_lint() {
logbook_reminder
if [ "$(count_py)" -eq 0 ]; then
echo "ci: no Python source yet, skipping lint"
return 0
fi
"$VENV/ruff" check . || return 1
"$VENV/ruff" format --check . || return 1
cmd_docs_links || return 1
cmd_spec_inventory || return 1
cmd_logbook || return 1
}
cmd_types() {
if [ "$(count_py)" -eq 0 ]; then
echo "ci: no Python source yet, skipping type check"
return 0
fi
"$VENV/pyright" . || return 1
}
cmd_frontend() {
if [ ! -f "src/frontend/package.json" ]; then
echo "ci: no frontend yet, skipping frontend checks"
return 0
fi
if [ ! -d "src/frontend/node_modules" ]; then
echo "ci: frontend dependencies missing — run 'cd src/frontend && npm install' or 'scripts/setup.sh'" >&2
return 1
fi
cmd_spec_inventory || return 1
(cd src/frontend && npm run typecheck && npm run lint && npm run test && npm run build) || return 1
}
cmd_test() {
if [ "$(count_tests)" -eq 0 ]; then
echo "ci: no tests yet, skipping test run"
return 0
fi
"$VENV/pytest" || return 1
}
cmd_spec_inventory() {
# Every Playwright spec under src/frontend/e2e must have a row in the spec
# inventory table of docs/04_OPERATIONS/12_Testing_Quality_Gates.md (§4.4) and
# that row's 'Covers (user-visible acceptance, GATE 6)' cell must be non-empty,
# so a new *.spec.ts cannot land without its docs row + acceptance note. Runs
# on `frontend`, `e2e`, `e2e-fast`, `all` and standalone as `spec-inventory`.
e2e_dir="src/frontend/e2e"
inventory_doc="docs/04_OPERATIONS/12_Testing_Quality_Gates.md"
if [ ! -d "$e2e_dir" ] || [ ! -f "$inventory_doc" ]; then
echo "ci: spec-inventory skipped (no e2e specs or inventory doc yet)"
return 0
fi
missing=""
empty_covers=""
for spec in "$e2e_dir"/*.spec.ts; do
[ -f "$spec" ] || continue
name="$(basename "$spec")"
# Inventory rows use a backticked cell: | `queues-cpu-busy.spec.ts` |
if ! grep -q "| \`$name\` |" "$inventory_doc"; then
missing="$missing $name"
continue
fi
# Covers is the 6th table column (awk field 7: rows lead with '|').
covers="$(grep -F "| \`$name\` |" "$inventory_doc" | \
awk -F'|' '{ gsub(/^[ \t]+|[ \t]+$/, "", $7); print $7 }' | head -1)"
if [ -z "$covers" ]; then
empty_covers="$empty_covers $name"
fi
done
if [ -n "$missing" ] || [ -n "$empty_covers" ]; then
[ -n "$missing" ] && \
echo "ci: FAIL — E2E spec(s) missing from the inventory table (§4.4 of $inventory_doc):$missing" >&2
[ -n "$empty_covers" ] && \
echo "ci: FAIL — E2E spec(s) with an EMPTY 'Covers (user-visible acceptance, GATE 6)' cell:$empty_covers" >&2
echo "ci: add/complete the row per spec (name → page → stack → fixture → acceptance note) before pushing." >&2
return 1
fi
echo "ci: spec-inventory OK ($(ls "$e2e_dir"/*.spec.ts 2>/dev/null | wc -l) specs documented, covers present)"
return 0
}
cmd_logbook() {
# Enforce .agent/LOGBOOK.md: every entry must start with the tagged header
# line '[UTC timestamp] [agent] [task] [event type]'. Two rules:
# 1. Any line starting with '[' must be a full 4-tag header with a UTC ISO
# timestamp (Z or ±00:00, optional fractional seconds) — a future
# session cannot drop the timestamp or tags.
# 2. Bullets ('- '/'* ') and '##'-level headings are NOT the entry format;
# they are flagged while they appear BEFORE the first tagged entry
# (a day file written entirely in bullet/heading form, e.g. without any
# timestamp at all, fails). Inside the body of a tagged entry (after the
# first header) free-form prose is allowed, and thematic breaks
# (---/***/___) are fine anywhere, so legacy day files whose tails were
# bulk-written before the format was enforced keep passing.
# Runs standalone as `logbook` and inside `lint` (pre-commit).
if [ ! -d ".agent/LOGBOOKS" ]; then
echo "ci: logbook skipped (no .agent/LOGBOOKS directory yet)"
return 0
fi
header_re='^\[[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-]00:00)\] \[[^]]+\] \[[^]]+\] \[[^]]+\][[:space:]]*$'
bad=""
files=0
entries=0
for log in .agent/LOGBOOKS/*.md; do
[ -f "$log" ] || continue
files=$((files + 1))
seen_header=0
lineno=0
while IFS= read -r line; do
lineno=$((lineno + 1))
case "$line" in
\[*)
if printf '%s\n' "$line" | grep -Eq "$header_re"; then
seen_header=1
entries=$((entries + 1))
else
bad="$bad\n $log:$lineno -> $line (does not match the 4-tag UTC header format)"
fi
;;
\-\-\-* | \*\*\** | ___*)
# Thematic break (---, ***, ___): fine anywhere.
;;
*)
if [ "$seen_header" -eq 0 ]; then
case "$line" in
-*) bad="$bad\n $log:$lineno -> $line (bullet before any tagged entry)" ;;
\**) bad="$bad\n $log:$lineno -> $line (bullet before any tagged entry)" ;;
+*) bad="$bad\n $log:$lineno -> $line (bullet before any tagged entry)" ;;
\#\#*) bad="$bad\n $log:$lineno -> $line (heading before any tagged entry)" ;;
esac
fi
;;
esac
done < "$log"
done
if [ -n "$bad" ]; then
echo "ci: FAIL — logbook entries must start with '[UTC timestamp] [agent] [task] [event type]' (see .agent/LOGBOOK.md):" >&2
printf '%b\n' "$bad" >&2
echo "ci: fix the offending line(s) above before pushing." >&2
return 1
fi
echo "ci: logbook OK ($files day file(s), $entries entry header(s) conform)"
return 0
}
cmd_docs_links() {
# Every relative link target in docs/*.md must resolve to an existing file.
# Anchors (#...), http(s), mailto, scheme and protocol-relative targets are
# skipped. Runs standalone as `docs-links` and inside `lint` (pre-commit).
if [ ! -d "docs" ]; then
echo "ci: docs-links skipped (no docs directory yet)"
return 0
fi
broken=""
checked=0
for doc in $(find docs -name '*.md' -type f); do
dir="$(dirname "$doc")"
for target in $(grep -oE '\]\([^)]*\)' "$doc" 2>/dev/null | sed -n 's/^\](//; s/)$//p'); do
case "$target" in
\#* | http://* | https://* | mailto:* | //* | /*) continue ;;
*://*) continue ;;
esac
clean="${target%%#*}" # drop an optional #anchor fragment
checked=$((checked + 1))
if [ -n "$clean" ] && [ ! -e "$dir/$clean" ]; then
broken="$broken\n $doc -> $target"
fi
done
done
if [ -n "$broken" ]; then
echo "ci: FAIL — broken relative links in docs/ ($checked targets checked):" >&2
printf '%b\n' "$broken" >&2
echo "ci: fix or re-target the links above before pushing." >&2
return 1
fi
echo "ci: docs-links OK ($checked relative link targets checked)"
return 0
}
cmd_e2e() {
if [ ! -f "src/frontend/playwright.config.ts" ]; then
echo "ci: no playwright config, skipping e2e"
return 0
fi
if [ ! -d "src/frontend/node_modules" ]; then
echo "ci: frontend dependencies missing — run 'cd src/frontend && npm install' or 'scripts/setup.sh'" >&2
return 1
fi
# The E2E suite boots an isolated backend that needs a local Redis.
if ! redis-cli ping >/dev/null 2>&1; then
echo "ci: e2e skipped (Redis is not running)"
return 0
fi
cmd_spec_inventory || return 1
(cd src/frontend && npx playwright test) || return 1
# The analysis-failure spec runs against a dedicated failing-mock backend.
(cd src/frontend && npx playwright test --config=playwright.failure.config.ts) || return 1
}
cmd_e2e_fast() {
# Quick local iteration: one healthy spec (default: navigation) plus the
# failure stack. Each boot still takes a few seconds, but only one healthy
# spec runs instead of the whole suite. Optional second arg overrides the
# spec (e.g. scripts/ci.sh e2e-fast volumes-compile).
if [ ! -f "src/frontend/playwright.config.ts" ]; then
echo "ci: no playwright config, skipping e2e-fast"
return 0
fi
if [ ! -d "src/frontend/node_modules" ]; then
echo "ci: frontend dependencies missing — run 'cd src/frontend && npm install' or 'scripts/setup.sh'" >&2
return 1
fi
if ! redis-cli ping >/dev/null 2>&1; then
echo "ci: e2e-fast skipped (Redis is not running)"
return 0
fi
cmd_spec_inventory || return 1
spec="${1:-navigation}" # plain variable: POSIX sh has no `local`
echo "ci: e2e-fast — healthy spec '$spec' + failure config"
(cd src/frontend && npx playwright test "$spec") || return 1
(cd src/frontend && npx playwright test --config=playwright.failure.config.ts) || return 1
}
case "$target" in
lint)
require_ci_log || exit 1
if cmd_lint; then
append_logbook_entry
else
exit 1
fi
;;
types) cmd_types ;;
frontend) cmd_frontend ;;
test) cmd_test ;;
e2e) cmd_e2e ;;
e2e-fast) cmd_e2e_fast "${E2E_SPEC:-navigation}" ;;
spec-inventory) cmd_spec_inventory ;;
docs-links) cmd_docs_links ;;
logbook) cmd_logbook ;;
all)
require_ci_log || exit 1
if cmd_lint && cmd_types && cmd_frontend && cmd_test; then
append_logbook_entry
else
exit 1
fi
;;
*)
echo "usage: ${NE_CI_SELF:-$0} {lint|types|frontend|test|e2e|e2e-fast|spec-inventory|docs-links|logbook|all} [--log TEXT] [--agent NAME] [--task TEXT] [--event TYPE]" >&2
echo " scripts/ci.sh e2e-fast [spec] — one healthy spec + failure stack" >&2
echo " lint/all require --log TEXT or CI_LOG_COMMENT (logbook commentary, see .agent/LOGBOOK.md)" >&2
exit 2
;;
esac
I usually ask the agent to store updates to the code in a dedicated format in daily MD files.
After a while, however, the agent with Deepseek V4 Flash 0731 forgot the instructions and started storing the trace logs in random MD files, sometimes in existing files and sometimes in new files stored in /tmp.
I fixed this by forcing the agent to create a CI script that required a commentary for the log. Then the CI script stored the log in the expected format and file.
I share my fix with you, but I'm not sure how you want the harness to manage this with the agent.