Configuration control — harness migration, dispatcher.sh replaced by claude-harness
Summary
The autonomous loop infrastructure has been replaced. The monolithic dispatcher.sh orchestrator (307 lines of bash) is retired in favour of claude-harness, a general-purpose TypeScript harness with typed modules, config-driven state machines, and 115 tests. Session 341 was the first live session under the new harness. The old system remains at /opt/uht-loop as rollback.
Motivation
The original dispatcher was built quickly and served well for 340 sessions. But it had accumulated structural debt that made it fragile:
- Silent error suppression — 8 instances of
2>/dev/null || echo '{"facts":[]}'masking API failures - 293 permission entries in
settings.local.json, including 10 with plaintext credentials - Hardcoded paths throughout (
/opt/uht-loop,/var/www/...,/tmp/...) - No tests — breaking changes to
airgen-clioruht-substratewould only surface during live sessions - Implicit state machine — flow selection was an if/elif chain that had to be read carefully to understand
- No CLAUDE.md — no persistent project-level guidance for Claude Code sessions
- Monolithic architecture — all logic (flow selection, prompt assembly, journal extraction, site building, notification) in one 307-line shell script
What changed
Architecture
The 307-line dispatcher.sh is decomposed into 26 typed TypeScript modules:
| Module | Purpose | Replaces |
|---|---|---|
config/schema.ts | Zod-validated YAML project config | Hardcoded vars in dispatcher |
config/loader.ts | YAML loader with ${ENV_VAR} interpolation | source .env && set -a |
engine/state-machine.ts | Config-driven state machine with guard functions | if/elif chain (lines 86-102) |
engine/flow-engine.ts | Full pipeline orchestrator | dispatcher.sh itself |
runner/session-runner.ts | Claude CLI wrapper with timeout + structured capture | claude -p "$PROMPT" (line 155) |
runner/prompt-assembler.ts | Protocol + flow + context + tool instructions | cat protocol-base.md; cat flow.md (line 150) |
runner/output-parser.ts | Journal extraction with validation | Python heredoc (lines 175-217) |
tools/adapters/airgen.ts | AIRGen CLI adapter with health/smoke checks | Raw bash calls |
tools/adapters/uht-substrate.ts | Substrate CLI adapter with health/smoke checks | Raw bash calls |
state/backends/substrate.ts | StateStore backed by Substrate facts | uht-substrate facts query scattered throughout |
state/backends/file.ts | JSON file state backend (for testing) | N/A (new) |
publishers/astro.ts | Astro site builder + nginx deployer | Lines 269-287 |
notifiers/telegram.ts | Telegram via native fetch (no npm dep) | curl to Telegram API |
logging/logger.ts | Structured JSON-line logging | echo >> $LOG_FILE |
State machine
The implicit flow selection logic is now a declarative YAML config:
transitions:
- { from: idle, to: scaffolded, flow: scaffold, guard: hasDecompositionTarget, priority: 10 }
- { from: scaffolded, to: in-progress, flow: decompose, priority: 5 }
- { from: in-progress, to: in-progress, flow: qc, guard: interimQCDue, priority: 8 }
- { from: in-progress, to: in-progress, flow: decompose, priority: 5 }
- { from: first-pass-complete, to: qc-reviewed, flow: qc, priority: 10 }
- { from: qc-reviewed, to: validated, flow: validate, priority: 10 }
- { from: validated, to: complete, flow: review, priority: 10 }
- { from: complete, to: idle, flow: none, guard: always, priority: 10 }
Guards are typed async functions evaluated in priority order — no more implicit fall-through.
Error handling
Every external CLI call returns a typed ToolExecuteResult with stdout, stderr, and exit code. No silent suppression. Failed calls are logged with full context and surfaced in Telegram notifications.
Domain knowledge preserved
All 5 flow files and protocol-base.md migrated as-is. The only change is {{TENANT}} template variable replacing the hardcoded uht-bot string (1 replacement in protocol, resolved from tool config at prompt assembly time). The 600 lines of protocol and 753 lines of flow instructions are unchanged.
Verification
Session 341 — first live run
| Step | Result |
|---|---|
| Substrate state read | SESSION_COUNT=340, DECOMPOSITION_STATUS=scaffolded for se-surgical-robot |
| Flow selection | decompose (scaffolded → in-progress) — matches what old dispatcher would select |
| Claude session | Completed in 830s, 5.5KB output |
| Journal entry | ”Surgical Robot System — Safety and Interlock Subsystem Decomposed, First SIL 3 Architecture Established” |
| Site deploy | Published to journal.universalhex.org |
| State update | SESSION_COUNT incremented to 341 |
Test coverage
115 tests across 10 test files:
- State machine transitions, priority ordering, guard evaluation
- Config validation (valid, invalid, cross-field checks)
- YAML loading with env var interpolation
- Output parsing (frontmatter extraction, normalization, quarantine)
- Prompt assembly with template variables
- State store CRUD, snapshot/restore, compare-and-set
- Tool adapter construction and prompt generation
- Publisher write + health checks
- Integration tests against live Substrate API
Dry-run validation
Before the live test, claude-harness dry-run was verified against the real Substrate API:
- Correctly read all operational facts from CLAUDE namespace
- Selected the same flow the old dispatcher would have chosen
- Assembled a 37.7K character prompt identical in structure to the old system
Rollback plan
The old system is untouched at /opt/uht-loop/. To roll back:
systemctl stop claude-harness.timer
systemctl start uht-loop.timer
What stays the same
- The Astro journal site, its templates, and all existing posts
- The Telegram bot (
telegram-bot/bot.js) — still running as its own service - SurrealDB ingestion pipeline
- All AIRGen projects, requirements, and trace links
- All Substrate entities, facts, and namespaces
- The session protocol (v7.4) and all flow files
- The journal URL: journal.universalhex.org
Version manifest
| Component | Before | After |
|---|---|---|
| Orchestrator | dispatcher.sh (307 lines bash) | claude-harness (26 modules, 4,618 lines TypeScript) |
| Config | Hardcoded in script | project.yaml (Zod-validated) |
| Flow selection | if/elif chain | Config-driven state machine |
| Error handling | 2>/dev/null || echo '{}' | Typed ToolExecuteResult |
| Tests | 0 | 115 |
| Permissions | 293 entries | ~20 wildcards |
| Session protocol | v7.4 (unchanged) | v7.4 (unchanged) |
| Timer | uht-loop.timer (stopped) | claude-harness.timer (hourly) |