# Lifecycle and architecture

## Components

`src/index.ts` creates one `GoalManager`, registers commands, tools, and renderers, and binds Pi lifecycle events. `GoalManager` owns durable `GoalState` plus ephemeral `RuntimeState`; `GoalScheduler` controls autonomous turns. Contract parsing, persistence, progress detection, verification, judging, prompts, and UI are isolated in their corresponding source modules.

## Event flow

| Pi event | pi-let action |
| --- | --- |
| `session_start`, `session_tree` | Read only the selected parent-linked branch, invalidate runtime callbacks, restore latest valid snapshot, and pause restored active work. |
| `before_agent_start` | For an active goal, append—not replace—the Pi system prompt with the goal protocol. |
| `agent_start` | Clear continuation latch and per-run tool, response, token, and blocker signals. |
| `tool_execution_start` | Record stable signatures for tool name and canonicalized arguments. |
| `tool_execution_end` | Keep up to 12 non-goal tool results, with text limited to 4,000 characters each. |
| `turn_end` | Store assistant response and add input, cache-read, cache-write, and output usage to the run. |
| `agent_settled` | The *only* scheduling trigger: finalize and persist the iteration, evaluate limits, then possibly queue one continuation. |
| `session_compact` | Refresh UI only; goal state is never inserted into a compaction summary. |
| `session_shutdown` | Invalidate callbacks, abort evaluation, and clear UI. |

There is intentionally no `agent_end` scheduler. Duplicate settled callbacks, retries, stale callbacks, replacement, clearing, pause, compaction activity, queued work, and shutdown cannot make duplicated autonomous turns because a runtime generation and continuation latch are checked before scheduling.

## Scheduler order

At `agent_settled`, the scheduler defers work once via its queue, confirms the same goal id/revision/generation remains active, finalizes exactly one iteration, and persists it. There is no iteration, token, or no-progress limit. It continues until the goal is completed, explicitly blocked, paused, archived, or encounters an evaluator error. Only then does it require `ctx.isIdle()` and no pending messages. Pending user messages, retry work, compaction retries, and other extensions win; their later settled event may continue the goal.

Initial scheduling follows the same idle/pending/latch checks but does not consume an iteration. Continuations are hidden custom messages sent as `followUp` with `triggerTurn: true`.

## Progress and limits

Goals run for an unbounded number of settled iterations. Each iteration adds run input/output tokens to cumulative totals for observability. Progress combines sorted tool signatures, verification outcome tuples, judge decision/gaps, and a normalized final response. Numeric values are normalized to `#`; near-identical responses use word-set Jaccard similarity of at least 0.9 when evidence is unchanged. No-progress data is retained for diagnostics but does not stop the goal.

A new material cycle clears a non-run blocker. `/goal resume` resets no-progress tracking and clears a blocker. Iteration and token usage are accounted after each settled run for observability.

## Statuses and blockers

- `active`: autonomous scheduling is permitted.
- `paused`: user pause, safe restoration, or inability to send a continuation.
- `complete`: only an approved judge decision can reach it.
- `archived`: explicitly archived by the user; cannot resume.
- `blocked`: a confirmed external blocker or a judge blocker.
- `evaluator_error`: verifier, authentication, network, cancellation, malformed judge output, or other evaluator failure; it fails closed.

`goal_blocked` fingerprints normalized reason plus required action. Repeated claims in a single worker run do not advance the count. A required user action blocks immediately; otherwise the same fingerprint must recur across three runs.

## Branch persistence

Every meaningful change appends a `pi-let.goal.state.v1` snapshot and normally a concise `pi-let.goal.transition.v1` display entry. A clear writes a snapshot whose state is `null`, preventing older branch snapshots from returning. Restoration scans backward for the latest valid snapshot. Callers must supply `getBranch()`, not all session entries, so a sibling branch cannot leak state.
