0229: Activity-fired self-update
Status: accepted (2026-11-01) · Scope: product tier (core/autoupdate, commands/autoupdate, the prompt and session-start hooks, the createHarneryProgram pre-action hook)
Context
Section titled “Context”Agents working in a shared checkout drift behind its upstream branch, and behind the tools and generated settings the project manages outside Git, unless someone remembers to refresh. A stale checkout runs old instructions, old hooks, and old commands, and nothing tells the agent so.
A scheduler is the obvious answer, but it runs when nobody is working and sleeps when they are. Each operating system needs its own setup (launchd, systemd timers, Task Scheduler), and a failed scheduled run is invisible until someone goes looking for its log.
Decision
Section titled “Decision”Activity fires the update. Every prompt, every session start, and every host
CLI command built on createHarneryProgram calls autoUpdateTick. The feature
is inert until a project sets autoUpdate.enabled: true in its own
.harnery/config.jsonc; a user-global value never turns it on.
- One-stat hot path. The tick reads the mtime of a throttle stamp under
.harnery/private/autoupdate/. When the stamp is younger thancheckMinutes(default 15), it returns. Otherwise it touches the stamp and launches a detachedautoupdate run --backgroundchild, so no hook or command waits for the work. A project without anautoUpdatesection never loads the engine. - Three levels of early exit. (1) The stamp is fresh: done. (2) In the
child,
git ls-remotereads the upstream branch’s commit and a local ancestry test checks whether HEAD already contains it. Nothing is fetched at this stage. A shallow fingerprint of the watched paths outside Git and the daily-step clock are checked at the same time. If all three are current: done. (3) Only then does the child run the host’s steps. - The host owns the steps.
stepsanddailyStepsare argv arrays run from the project root, typically the host’s own safe pull and sync command. Harnery never fetches, resets, or discards changes itself. The first failing step stops the run. - Notice protocol. A step reports to agents by printing a line that starts
with
harnery-notice:. Exit code 75 means the step did what it could but a person must act (sign in again, resolve a conflict); that outcome is reported and not retried as a failure. Notices are queued in the auto-update state and delivered through the prompt and session-start hooks, once per agent session, as lines that start withAuto-update:. - Automatic notices. When HEAD moves during a run, the engine compares the old and new commits. A change to agent instruction files or to hook settings produces a notice telling agents to reread them, because a running session keeps whatever it loaded at start.
- Failure backoff and a lock. A failed run is retried no sooner than an hour later. A lock file keeps two children from running at once; a tick that finds a live run returns without launching another.
- Daily cleanup. With
cleanupon (the default), a successful run removes expired working artifacts at most once a day. Unexpired artifacts are never touched. - Manual control.
autoupdate statusshows the last run and pending notices,checkreports what the next run would do without running steps, andrunruns it in the foreground.
Alternatives
Section titled “Alternatives”- Operating-system schedulers (launchd, systemd timers, Task Scheduler). Rejected: one setup per platform, runs while nobody is working, and failures stay out of sight of the agents who would act on them.
- A fetch on every prompt. Rejected for its cost: a network round trip and
object transfer on each turn, multiplied by every agent in the checkout.
ls-remotereturns one line, and the stamp limits even that to once percheckMinutes. - Only a manual refresh command or skill. Kept as the fallback for a
project that leaves
autoUpdateoff, and for an agent that wants to update now. It depends on someone remembering, which is the problem this solves.
Consequences
Section titled “Consequences”- A checkout with agent activity stays within
checkMinutesof its upstream without any per-machine setup. - An idle checkout is not updated. The first prompt after a quiet period triggers the check, and that agent learns of any change on a later prompt.
- Update safety is only as good as the host’s steps. A step that discards local work would do so on activity; hosts must give it the same guards they give a manual pull.
- Projects that never configure
autoUpdatepay one config read per hook and see one more lazy command.