8.3 KiB
goal — pi extension
Operator-set goal with a forced check<->proceed loop. /goal <text> activates a goal; the
extension keeps the agent working and reporting until it declares the goal satisfied via the
goal_report tool, then retains a recallable completed outcome and returns the session to normal operation.
Requirements record: docs/PRD.md in mosaic-brain (issue #52). Design locked with Jason
2026-08-28 (Q1-Q8).
Usage
/goal full current or completed goal, state, and counters
/goal <text> set (or replace, with a notify) the active goal
/goal --max N <text> set with a non-default run limit (default 25)
/goal stop pause the loop, goal retained
/goal resume reconcile a paused or waiting goal (kicks one check turn)
/goal clear remove the goal entirely
GOAL_MAX_CHECKS env overrides the default cap for new goals (invalid values fall back to 25).
Loop mechanics
before_agent_startappends the active goal and reporting instructions to the system prompt every turn, so the goal survives context growth and compaction.- When the agent settles with ready work and no unresolved wait, the extension injects a check
prompt (
sendUserMessage), forcing the next turn: the check<->proceed loop. Before reporting, follow the continuation loop inskills-local/ms-proactive-agent/SKILL.md. - The agent reports via
goal_report, the loop's only exit:satisfied+ evidence: completed outcome retained for recall, normal operation (evidence,reason, andin_progressstructured progress use thems-executive-updateformat —skills-local/ms-executive-update/SKILL.md— so the operator reads one shape everywhere)blocked+ reason: loop paused, reason surfaced to the operatorin_progress: evidence plus a structured progress record naming the nearest gate, owner, last live measurement, and next action
- Successful non-
goal_reporttool results create one observable work event. Measurement, action, and delegation reports require that event. Non-tool work requires a concrete artifact. Waiting requires an approved watch id or concrete next-check condition. - Empty, malformed, unsupported, or duplicate
in_progressreports do not reset the check counter. They increment a persisted no-progress counter and pause after three reports by default (GOAL_MAX_NO_PROGRESS_REPORTS). Repeated valid wait reports remain active without claiming new progress or triggering a false-positive pause. - Esc (run abort) and run errors pause the loop instead of re-injecting;
/goal resumecontinues. Operator-typed messages always flow through unchanged. - No-report cap: N consecutive check turns without an accepted report auto-pause the loop and
notify. Default 25 (
GOAL_MAX_CHECKS), per-goal--max N.
The loop needs a persistent session (TUI or RPC). In print mode (pi -p) the goal directive
and seat-durable state still apply, but check turns are not forced — print mode exits when the
prompt pipeline completes, and a forced turn there races session teardown (measured).
State
Incarnation-durable state survives /new, /resume, and reloads within one process. This
repository's native development installation stores it under .pi/state/goal/. The identity is
the process-launch incarnation, never PI_SESSION_ID. Unknown-owner legacy goal-state.json
files are quarantined rather than inherited. One active goal exists per incarnation; setting a
new goal replaces that incarnation's goal.
State stores counters, bounded routing fields, and a SHA-256 evidence fingerprint. It does not persist free-form progress evidence.
Development installation
This directory is canonical source. Do not edit .pi/extensions/; generate that native-test
installation with scripts/sync-dev-extensions.sh. The sync copies goal/ and its
mosaic-core/lib/ dependency as ordinary files, verifies content hashes, and refuses to
overwrite installation drift. It also enforces that goal/index.ts is the only extension
entrypoint in this bounded package.
Start the isolated native test with bash scripts/goal-dev.sh. It runs the sync and passes the
generated entrypoint explicitly to Pi while disabling ambient extension discovery. This does
not install or reload the live fleet extension and does not add the extension to Docker.
Directory layout: index.ts for wiring, lib/ for logic, and test/ for hermetic checks.
Pi supplies the extension API, TypeBox, and pi-ai imports at runtime.
Tests
node --test extensions/goal/test/*.test.ts
bash scripts/test-extension-package.sh
The suite includes pure state, persistence, incarnation fencing, exact report-only loop, duplicate evidence, legitimate wait, headless extension-runtime, and enforcement-neutralization red controls.
Quiet waits and optional deadlines
Every accepted wait suspends automatic checks, including untimed waits. No
cooldown or model polling is used. A next-check condition describes the dependency;
it is not executed as code, and the extension does not register a watch for it.
Existing external message/watch delivery or explicit /goal resume can start
reconciliation. Incoming messages alone do not clear the wait. The model must
verify the dependency and report substantive progress or completion. Unrelated
input preserves the wait. Explicit resume clears the old wait; another unresolved
wait report suspends checks again. Paused and blocked goals still require explicit
operator resume, not merely an incoming message.
Use /goal --wait-timeout 60 <goal text> to add one deadline wake to a wait.
The flag accepts 10–86400 seconds and combines with --max N. Without it,
no deadline timer is armed. Existing running sessions require a controlled reload
after an approved deployment; do not reload unrelated seats for a trial.
A valid goal_report with status: "in_progress", progress.kind: "wait", an
owner, and a watch id or concrete nextCheck persists the wait and yields.
A deadline is persisted only when the operator configured one.
There are no model heartbeats during that wait. Repeated wait reports preserve
its original deadline. A substantive progress report clears the wait. The
extension neither evaluates arbitrary shell conditions nor sends messages to
other agents: a relevant incoming message can start an ordinary turn, or the
one-shot deadline asks the agent to reconcile the dependency once.
There is one automatic deadline wake per goal or explicit /goal resume.
Reporting another wait after that wake pauses the goal, including when its
wording changes. This intentionally limits the initial rollout; multiple
successive timed dependencies require explicit resumption. Deadline expiry
never supplies approval or permits the agent to invent missing input.
The timer belongs to this Pi process. Reload within the same incarnation restores the original deadline; shutdown cancels timers. A new process has a new fenced goal identity and does not inherit another incarnation's goal. This patch is not a process supervisor. Print mode records a manual wait and never schedules a wake. No external systemd timer is installed.
If the runtime is busy at expiry, readiness is checked in code every second
for at most 60 seconds; this also covers manual compaction with no settle event.
If still busy, the goal pauses with a UI notice. A dispatched deadline wake
must appear in before_agent_start within 30 seconds or the goal pauses without
resending. The matching event confirms turn start, not successful task execution.
Stop, clear, replacement, satisfaction, and blocking cancel owned timers.
Late reports cannot settle a paused goal. Reports also refuse if the goal changes
while an asynchronous policy check is running. State snapshots use atomic file
replacement; failed saves stop continuation instead of claiming a saved result.
An in-memory pending-check guard prevents duplicate settle/startup events from queuing the same check before its matching start is observed. It does not cancel messages already queued by Pi, span independent extension instances, or supply a process supervisor. Wait state itself persists across same-incarnation reloads.
The tests exercise timed and untimed waits, deadline dispatch and observation, unresolved pause, compaction, reload, cancellation, print mode, malformed persisted waits, and actual filesystem write failure. This is separate from live seat acceptance.