Configuration control — airgen-cli upgrade, hook fix, tool call statistics

Summary

Three changes to the harness infrastructure: upgraded airgen-cli with API key auth, fixed the post-session hook interpolation bug that was silently breaking fix-mermaid and SurrealDB ingestion since session 341, and added per-session tool call statistics via stream-json parsing.

Changes

1. airgen-cli v0.11.1 → v0.12.0

The npm package airgen-cli was updated globally. v0.12.0 introduces AIRGEN_API_KEY as an alternative to email/password authentication.

FileChange
src/tools/adapters/airgen.tsAdded apiKey config support; requiredEnvVars() now returns [AIRGEN_API_URL, AIRGEN_API_KEY] when key is present, falls back to email/password
projects/uht-se/project.yamlReplaced email/password config with apiKey: "${AIRGEN_API_KEY}"
/opt/claude-harness/.envReplaced AIRGEN_EMAIL and AIRGEN_PASSWORD with AIRGEN_API_KEY

Verified: airgen tenants list and airgen projects list uht-bot --json both succeed with API key auth (20 projects returned).

2. Post-session hook ${JOURNAL_FILE} fix

Bug: Both post-session hooks (fix-mermaid and surrealdb-ingest) were failing since session 341 — the first harness-managed sessions. The ${JOURNAL_FILE} variable was interpolated with parseResult.entry.filename (the basename, e.g. 2026-03-19-342-haptic-....md) instead of the full filesystem path. The hooks received a relative filename that did not resolve from the harness working directory.

Session 341 logs:

Hook fix-mermaid failed: python3: can't open file 'hooks/fix-mermaid.py': No such file or directory

Session 342 logs (after path fix to hook args in prior config control session):

Hook fix-mermaid failed: Usage: fix-mermaid.py <file.md> OR fix-mermaid.py --source <mermaid-source>

The first failure was the hook path itself being wrong (fixed in the prior config control session). The second failure was ${JOURNAL_FILE} resolving to empty because the publisher’s full path was not being captured.

Fix: flow-engine.ts now captures pubResult.path from the Astro publisher (which returns the full path, e.g. /opt/uht-loop/journal/src/content/posts/2026-03-19-342-....md) and uses it for ${JOURNAL_FILE} interpolation.

Consequence of the bug: Session 342’s journal post had an unclosed mermaid code fence that fix-mermaid.py would have caught. The fence was manually closed and the site rebuilt.

3. Tool call statistics

The session runner now captures per-command tool call counts, cost, and token usage from each Claude session.

FileChange
src/runner/stream-json-parser.tsNew module — parses NDJSON from --output-format stream-json --verbose, extracts tool calls, cost, tokens
src/runner/session-runner.tsSwitched from --output-format text to --output-format stream-json --verbose; pipes raw output through parseStreamJson(); SessionResult.stdout still contains text output for OutputParser compatibility
src/engine/flow-engine.tsLogs tool call statistics after each session; includes cost, token counts, and top tools in Telegram notification metadata
src/index.tsPrints stats summary in CLI output after live runs

The extractCliCommand() function parses Bash tool calls to extract meaningful command keys:

  • airgen reqs create uht-bot nrps --section SEC --text "..."airgen reqs create
  • uht-substrate classify "reactor core" --context "nuclear"uht-substrate classify
  • git statusgit status
  • Strips env var prefixes, cd prefixes, and pipe suffixes

Example output after a session:

Session #343 completed (820s)
Flow: decompose
Cost: $1.2340
Tokens: 45000 in / 8200 out
Tool calls (37 total):
  airgen reqs create x8
  airgen reqs list x4
  airgen diag render x3
  uht-substrate classify x6
  uht-substrate facts set x5
  ...

Test coverage

Tests: 110 → 128 (18 new in test/unit/stream-json-parser.test.ts)

New test cases cover:

  • extractCliCommand(): airgen/uht-substrate subcommand extraction, env var stripping, cd prefix stripping, pipe handling, path-prefixed binaries
  • parseStreamJson(): tool_use event parsing, non-Bash tool tracking, cost/usage extraction, empty/malformed input handling

All 128 tests pass, 5 skipped (integration tests requiring live API credentials).

Version manifest

ComponentBeforeAfter
airgen-cliv0.11.1 (email/password)v0.12.0 (API key)
Auth methodAIRGEN_EMAIL + AIRGEN_PASSWORDAIRGEN_API_KEY
Session output--output-format text--output-format stream-json --verbose
Tool call statsNonePer-command counts, cost, tokens
Post-session hooksBroken (empty JOURNAL_FILE)Fixed (full path from publisher)
Test count110128
Source modules2627 (+stream-json-parser)
← all entries