8gent Code
Architecture

Goal Loop

The goal-loop is the runtime behind /goal. It lives in packages/goal/ as pure logic, then gets consumed by the daemon's RPC layer and surfaced by the TUI's LiveFocalStripWithGoal overlay and the Electron app's GoCommandBar.

Pure logic. Mock executor + mock judge in tests. No I/O leaks. The daemon owns persistence, ledger, and event fanout.

Components

packages/goal/
  types.ts              Budget, Counters, Receipt, GoalEvent, JudgeVerdict
  goal-loop.ts          The orchestrator. Turn loop, budget enforcement,
                        judge dispatch, sub-goal injection, abort handling.
  state-machine.ts      pending -> running -> judging -> completed|stopped|failed
  budget.ts             Per-axis counters; resolveBudget, shouldStop
  judge.ts              Verdict validation, anti-collusion gate,
                        confidence floor enforcement
  receipt.ts            Final-result assembly: verdict + cost + evidence
  executor-eight.ts     EightExecutor - wraps packages/eight/agent.ts
  judge-failover.ts     FailoverJudge - local-first chain via
                        packages/providers/failover.ts
  persistence.ts        GoalPersistence - SQLite goal_runs + goal_events
  ledger.ts             Append-only HMAC-signed hash-chain JSONL

State machine

pending -> running -> judging -> (running | completed | stopped | failed)
                                          ^
                                          +-- terminal states

Transitions are explicit. IllegalTransitionError thrown on any non-allowed step. The same machine is used in unit tests against mocks and in production against real models.

Judge contract

Every turn the executor produces a turn summary. The judge receives the goal text, the turn summary, and a falsifiable success criterion (extracted at goal-start by a lightweight rewrite of the goal). The judge returns:

type JudgeVerdict = {
  decision: "satisfied" | "continue" | "failed";
  confidence: number;   // 0..1
  reason: string;       // one short sentence, no AI-speak
};

Non-negotiable judge rules

  1. Anti-collusion: the judge constructor rejects if judge model id equals executor model id. Self-grading defeats the loop.
  2. Fail open: any judge failure (timeout, malformed JSON, network error, unreachable provider) returns {decision: "continue", confidence: 0, reason: "judge unavailable, deferring to budget"}. Never wedge.
  3. Structured output only: free-text verdicts are parse-rejected.
  4. Verifies by execution, not self-report: the prompt instructs the judge to look at evidence, not to trust the agent's claim that it succeeded.

Budget enforcement

Five axes, all enforced before the judge is even called:

maxTurns          per-run turn ceiling
maxTokens         in + out across all turns
maxWallclockMs    real time from goal.started to terminal state
maxFilesChanged   files touched via Write/Edit/Delete tools
maxEgressBytes    HTTP body bytes shipped by the agent
maxDissentStreak  consecutive "continue" verdicts before forced stop

Defaults are local-first: 12 turns, 100k tokens, 10 min, 50 files, 25MB egress, 8 dissent streak. Per-run overrides via daemon RPC.

Persistence

packages/db/migrations.ts v2 adds:

CREATE TABLE goal_runs (
  id              TEXT PRIMARY KEY,
  session_id      TEXT NOT NULL,
  goal_text       TEXT NOT NULL,
  status          TEXT NOT NULL,
  budget_turns    INTEGER,
  budget_tokens   INTEGER,
  budget_wallclock_ms INTEGER,
  judge_verdict   TEXT,
  started_at      INTEGER,
  ended_at        INTEGER,
  created_at      INTEGER NOT NULL
);

CREATE TABLE goal_events (
  run_id  TEXT NOT NULL,
  seq     INTEGER NOT NULL,
  kind    TEXT NOT NULL,
  payload TEXT NOT NULL,
  ts      INTEGER NOT NULL,
  PRIMARY KEY (run_id, seq)
);

/goal resume hydrates a run from these tables after a daemon restart.

Append-only ledger

Every goal event ALSO writes to ~/.8gent/runs/{runId}/ledger.jsonl:

{"seq":1,"prev_hash":"00..00","hash":"<sha256>","ts":1700000000,"kind":"run.started","payload":{...},"sig":"<hmac>"}
{"seq":2,"prev_hash":"<prev>","hash":"<sha256>","ts":1700000001,"kind":"turn.completed","payload":{...},"sig":"<hmac>"}
  • hash = sha256(prev_hash || canonical(payload))
  • sig = hmac-sha256(canonical(payload), daemon_key)
  • Daemon key at ~/.8gent/keys/state-hmac.key, mode 0600, auto-generated on first use
  • Ledger.verify() walks the chain end-to-end, returns the first failure with {atSeq, reason}

A tampered payload, a broken chain link, or a forged signature all surface. The ledger is the audit trail for autonomous runs.

Daemon RPC

Five inbound methods + three event types:

goal.start    { sessionId, goal, budget?, judgeModel? } -> { runId }
goal.status   { runId } -> { snapshot }
goal.subgoal  { runId, text } -> { accepted }
goal.abort    { runId } -> { accepted }
goal.resume   { runId } -> { known }

(events streamed)
goal.turn     per turn
goal.judge    per verdict
goal.done     terminal state

Both the TUI's GoalClient and the Electron app's goal-client.ts speak this protocol. In-process transport (no daemon required) and WebSocket transport (daemon over localhost:18789) both implement the same GoalTransport interface.

Sub-agents

When a goal needs cross-functional work, the loop can spawn sub-agents via packages/eight/harness/spawn.ts. Each sub-agent runs in an isolated git worktree with capability narrowing, persona validation, and a structured result that flows back to the parent. The agent pool caps concurrency at 10.