One local notify command accepts a source, event ID, and text, then
prints a private accepted or duplicate receipt. It rejects unsupported
targets and fields instead of redirecting input. The receipt does not
claim that an agent has processed the event.
Caller cancellation and a submission deadline stop pending socket I/O.
The command help states the root-only test scope, input bounds, and
fixed receipt capacity. Isolated socket checks cover intake, retries,
output failures, cancellation, and receipt privacy.
Root events stay in the durable agent queue until a turn boundary, so
accepted data cannot enter an active tool chain or its memory view.
Source and event IDs bind retries to the accepted text across restart
and completion. A fixed receipt cap rejects new keys without discarding
pending work or retry protection.
External data carries its receipt time and an explicit trust boundary.
Quoted bodies cannot create image attachments, and raw events do not
produce task-worker completion notices in Telegram. Delivery recovery
can repeat model calls and tool effects, not delivery log entries.
Agent queue writes sync the replacement file and its directory before
returning success. This gives local event acceptance a durable storage
boundary and stops queue writes when a replacement has an uncertain
outcome.
Telegram update failures and worker state-save failures return their
wrapped errors directly inside existing failure branches. A compaction
without a line also returns its known failure directly. Unreachable nil
returns and repeated conditions are removed without changing error
causes or successful paths.
A cancelled flush returns the direct context error while the durable
write is still pending. Releasing the sync then lets final cleanup
finish normally. This protects cancellation identity and shutdown
ordering while local failure context is added elsewhere.
The HTTP failure fixture checks whether it can take over the test
connection before closing it. A setup failure cannot silently become a
different network failure. Only the deliberate disconnect close remains
a justified nonfatal discard.
Forwarded failures from local setup, scans, persistence, and command
execution identify their operation without losing the original cause.
Successful results remain unchanged, and privacy-safe Telegram
diagnostics keep their existing contract. Tests cover nil results for
missing nodes and cause identity after read or sync failures.
Daemon, chat, schedule, and worker failures identify their local
operation while wrapping the original cause. Successful results keep a
nil error. Privacy-safe causes and direct context errors retain their
existing handling, and shutdown and retry decisions do not change.
Memory, tree, view, log, and chain failures name their local operation
while retaining the original error through wrapping. Combined result
checks keep their successful nil error instead of wrapping it. Error
classification, cancellation causes, and successful values stay intact.
Multiline callbacks and literals keep their required separators even
when the next operation has its own error assertion. The hook test
comment uses the repository reflow tool without changing fixtures. View
layout comments explain the current prompt-cache contract instead of
retaining historical measurements.
Cancellation, removed-client cleanup, typing hints, and cancelled HTTP
fake replies keep their nonfatal behavior with justified lint
directives. Ordinary test response writes and final pipe cleanup check
their errors. No blank assignment hides an unchecked error, and
application shutdown and retry paths stay unchanged.
The WhatsApp logout log records the event without a command for the
user. The existing user-facing notice still explains how to pair again,
so recovery advice reaches the user without becoming part of the event
name.
The commit procedure checks the final message file with every trailer
before signing it. The commit uses that same file without later trailer
edits. Commit prose fills 72-column lines; the source-comment reflow
tool has an 80-column limit and is not the commit formatter.
Hook regression tests distinguish a next word that does not fit within
72 columns from a genuinely underfilled paragraph. The checks include
trailers and formatted blocks, so the complete message is tested without
applying source-comment formatting to commit prose.
Daemon log formatting and single-consumer test helpers identify their
owning feature. Memory replay helpers identify the memory test owner.
WhatsApp tests reuse the shared discard logger instead of a duplicate
logger factory. The renamed functions keep their behavior.
Constructors, each receiver group, and standalone helpers follow the
order in which their callers use them. Type-aware references keep
unrelated methods with the same name distinct. Functions without an in-
file caller retain a stable relative order. Function text and behavior
stay unchanged.
Table cases describe their accepted, rejected, routed, or displayed
behavior instead of naming only the input. Assertions, inputs, and
production behavior stay unchanged; test reports identify the contract
of each case.
Comment paragraphs use the repository reflow tool, and event literals
put each field on its own line. The Telegram request comment
distinguishes method-bearing setup errors from safe network causes. Code
behavior and error values stay unchanged.
Multiline literals and calls have a blank separator before the next
logical block, except for their immediate error check. This includes
test case literals before loops. Event fields and empty callbacks use
multiline bodies. Whitespace and literal layout do not change behavior.
Model, memory, chat, tool, and view files put constants and variables
before types, then constructors, grouped methods, and helpers. Shared
test helpers use the same order, and ordinary tests keep tests first.
Declaration text and function bodies stay unchanged.
Daemon, storage, configuration, and build files put types before
constructors and grouped methods. Shared test helpers use that order,
and ordinary tests keep their tests-first order. Constants and variables
precede types so every declaration category is easy to find.
The declaration text and function bodies stay unchanged.
Memory tests hold replay and queued append scenarios, and daemon queue
tests hold routed WhatsApp scenarios. Their source filenames identify
the implementation they exercise without empty production files.
Helpers with one consumer stay with that consumer. All function bodies
and test behavior stay unchanged.
Command parsing and dispatch have their own source file beside their
tests. Help text, build version, output, and returned errors stay the
same. The entry point keeps run, which creates and cancels the signal
context before it calls the command parser.
The skills set declaration order, owning test filenames, multiline
separators, and explicit handling of ignored errors. Review covers the
rules that the current checks do not enforce, so a tool pass does not
stand for full style compliance.
Every commit uses the configured signing key and needs cryptographic
verification. Code and issue pushes need permission, as does a rewrite
of published history, so routine steps do not imply that consent.
The Nix package supplies pinned small-model and Whisper executable paths
for local voice transcription. The user instructions describe the local
model and executable overrides for other builds. Chat media comments
cover voice-note input, and voice test cases keep their fields explicit.
Owner voice notes from Telegram and accepted WhatsApp voice notes enter
shared audio storage and reach the agent as text. Downloads and local
transcription complete before durable message acknowledgement. Failure
markers keep later messages moving, and routed transcripts use the same
child queue and restart recovery as text. Other senders, view-once
messages, and music attachments do not become voice input.
Private content-addressed audio storage keeps voice notes before a local
Whisper process decodes them. Configured executable and model paths
override the Nix defaults. Missing tools and failed or cancelled
transcriptions produce safe markers instead of stopping message intake.
Plain text output keeps internal timestamps and disables carried text
context. Temporary transcript files stay out of backups.
A separate Nix package compares two pinned Whisper models on public
English and Portuguese Ogg Opus excerpts. It prints each transcript,
wall time, and peak memory so the local voice model can be selected
without an external service or private audio. A decode probe selects 16
kHz mono WAV conversion when direct Opus input is not supported.
bin/check fails on a Go comment line over 80 columns, counted as
bin/reflow counts them, with tabs as 8 columns. gofumpt does not wrap
comments, and the line length linter would also count code lines, which
the go-code skill keeps on one line, so ten long comment lines had
passed every check.
bin/reflow wraps each changed paragraph again, so that no comment line
passes 80 columns with tabs as 8 columns, as the docs skill asks. Ten
lines were over, from edits that skipped the reflow. Only comments
change.
Each WhatsApp send logs one line, message sent at info or message send
failed at warning with its safe cause, and an image shown in Telegram
logs image sent. A chat shows as a short hash, the same for received
images and sends, so the log tells chats apart with no phone number,
text, or caption. Before, a successful send left no trace in the
journal.
The test daemon keeps its data, and so its socket, in a short directory
under /tmp. t.TempDir holds the test name and the long TMPDIR of the
development shell, so a test with a long name failed to listen on a
socket path over 107 bytes, and bin/check failed in the shell unless
TMPDIR was set short by hand.
WhatsApp sends accept a stored image and use the text as its caption.
The send record includes the image name and precedes the upload, so a
repeated ID cannot upload or send again. Media errors keep safe causes
without URLs or keys.
The root asks for consent as for text sends, and image markers in tell
give children the shared pixels. The user can also ask to see a received
Telegram photo without a separate import.
The root can show a stored image as a Telegram photo and return its name
and path to terminal clients. A refused photo goes as a document, while
an uncertain send does not cause a second send. Children and subagents
have no image output tool.
Each operation limit ends in Timeout and names its operation. Telegram
file lookup, image download, and typing have separate names from message
sends. All wait values stay the same, including the server wait for
updates and the memory build wait.
Operation files own their duration constants without struct fields that
only supply shorter test waits. Fake time checks the full send, lookup,
batch, catch-up, notice, and schedule waits. Retry delays and batch
policy values stay the same.
Library callbacks use the context of WhatsApp.Run, so daemon
cancellation ends name lookups and image downloads. Each image keeps its
own deadline without the shorter name lookup deadline. Tests use their
caller context.
The go-code skill asks for contexts derived from the caller, with
context.WithoutCancel for work that must finish after a cancel and
context.Background only where no caller context exists. It also names
each operation limit after its operation with the Timeout suffix, in the
file that applies it. Handlers with a background context could not be
stopped by the daemon, and limits named only Time did not say what they
limit.
WhatsApp stores supported images before it acknowledges their messages,
so a stop cannot lose them. A download has its own limit of two minutes,
apart from the ten seconds of the name lookups. Batch text keeps the
image markers and captions on their own lines, so the agent and a routed
child can read the pixels. Failed images and other visual media keep
short text markers instead of stopping the batch. View-once messages
stay unread.
A failed download logs only a short chat hash and a safe cause, such as
an HTTP status, because the library error text can hold signed media
URLs. Tests use made-up images and a fake download client to check
storage, the limits, the markers, routed delivery, and restarts.
Accept /image PATH with an optional caption and quoted paths with
spaces. Resolve relative paths in the client and let the daemon store
the image before it submits the marker and caption. The socket carries
only the path, so image bytes never enter its events or the chat log.
Download the largest supplied photo up to 1568 pixels, or an image file,
into the shared store and submit its marker and exact caption. Refuse
unsupported or large images with a short notice. Retry network failures
without moving the offset, and stop on storage failures so images are
not lost. Keep tokens and pixels out of daemon logs.
Attach stored pixels to new user messages for the whole turn chain.
Later turns keep text markers and descriptions, which keeps the request
prefix stable. Leaf compaction sees pixels once per build and preserves
visible details. Single-message zoom returns text and image parts while
the durable log keeps only text.
Keep one immutable JPEG, PNG, or WebP file per image in the shared data
store. Validate size and format before atomic publication, and preserve
image files and modes in backups. The WebP config decoder needs x/image
because the standard library has no WebP decoder.
The README is a guide for the user: setup of the data directory, login,
Telegram, WhatsApp, the user service, and backups, then the client
commands, what the agent does, the log, and development. It drops the
internal files, formats, locks, queues, recovery details, and limits
that each feature had added, which the doc comments of their code hold.
It shrinks from 703 to 240 lines.
The docs skill says that the README is for the user and that a feature
adds at most a few lines to it, with internal files, locks, queues,
recovery, and limits in Go doc comments. Short alone was not enough:
each worker documented its feature in full, and the README grew to 700
lines.
The import command selects an existing direct child with --agent NAME
and uses its log, prompt, tools, cache key, and usage records. The root
stays the default target and supplies live cutoff history for a child.
Plans name the target without model calls or changes to its files.
Child rebuilds keep their staging directories and timestamped backups
outside children. Only complete child configs appear in that directory,
so a backup or interrupted rebuild cannot stop daemon startup.
Import plans use the first live message of each chat across the target
and root logs. An unrelated chat does not cut off an export, and only
imported days in the target count as held. This lets a child take its
history without copying newer live messages or skipping root-only days.
A routed batch enters the durable agent message queue as work and wakes
only its child. Saved log IDs prevent a second entry on restart, and a
receipt remains until inbox removal even if the child finishes first.
Receipts use batch numbers rather than a watermark because chats can
close out of number order.
Unrouted batches keep the root path. A missing child sends new inputs
and its queued external batches to the root. Routed events keep their
WhatsApp source without a subagent completion status, and daemon logs
name the destination without message text or chat IDs.
The root keeps a private source-to-child map and manages it through
route, unroute, and routes. Only direct children can receive a route,
and only WhatsApp chat sources are supported. The prompt requires user
agreement, and WhatsApp status lists the configured routes. Route logs
omit source IDs because they can contain phone numbers.
The root creates permanent children with its model and effort, sends
them durable messages with tell, and reads their views with zoom. A
child has a short capitalized name for its area, such as House, which no
child, subagent, or the root uses. spawn keeps its short tasks. Children
have no creation or WhatsApp tools.
The root sees each child name and area in its turn state, while its
shared prompt stays stable. The prompt directs mixed-chat area messages
to the right child with their sender, date, and words, and requires user
agreement before the root sends a child draft to WhatsApp.
A new child is prepared outside the children directory and published in
one step, so a stop never leaves a half-made child that blocks the next
start, and a failed creation frees its name. Runtime children join the
daemon lifetime and backup locks before they receive tasks.
Schedules record the agent that sets them and wake only its loop. Saved
schedules without an owner take the name of their agent, so root tasks
keep their behavior across a restart. A different stored owner stops
startup instead of sending the task to the wrong agent.
A parent can queue a user message for its own child through the daemon.
The child's final reply queues a work message for the parent with the
child's name in brackets. Each delivery wakes its recipient, including
one that is busy, and an empty final reply sends nothing.
The durable queue keeps inputs until their turn completes and saves a
child report with its input acknowledgments. Saved log positions prevent
repeat entries after restart. Interrupted turns can repeat model calls
and tools. Queue failures stop the loops, and backups lock the queue
before the agent files. Turn logs name their agent. Tools and prompts
stay unchanged.
Memory saves a queued message's position before it appends under the
write lock. Recovery reads that position or completes its append, so a
stop between the queue save and the log write cannot repeat the message.
The queue owns the saved position and stops delivery after a failure.
The agent guide in the README describes manual child configuration,
separate state and calls, startup checks, and backup coverage. It makes
clear that clients still address the root and that children have no
WhatsApp tools or parent message transport.
The daemon opens the root and each direct child with separate memory,
compaction, usage records, schedules, and subagents. Invalid child
configuration stops startup, and the ready event names the children.
Each child uses its own cache key and has no WhatsApp tools. Its turn
and compaction calls share the same prompt and tools.
Backups flush and lock every agent and its subagent logs during the
copy, so the archive holds the full state of the children as well as the
root. Clients still address only the root.
Child configuration names its parent and responsibility together. Its
prompt limits work to that area, reports only what needs attention,
allows an empty report, and requires drafts instead of WhatsApp sends.
Each child reads its own instructions file at each call.
The bot writes the end of a send outage to the log after the send
returns, so the fake server can hold the message before the line exists.
The test now waits for the line instead of reading the log once, which
made it fail now and then, also inside nix build.
The README explains backup consistency, secret archive contents, safe
restore, and the empty destination requirement. A user timer example
makes one local archive each day and keeps the last 14 after a
successful backup.
The backup command asks the running daemon for an archive. A flushed
memory snapshot holds the write locks during the agent copy, and SQLite
VACUUM INTO saves the WhatsApp session without raw database files.
Compression runs after the write locks are released.
Restore validates the archive in a staging directory before it installs
the files. The token lock excludes a running daemon, and a nonempty
destination fails without replacement of its data.
The archive writer installs a complete mode 0600 tar.gz file through a
temporary file and a rename. Extraction preserves regular file and
directory modes and rejects unsafe paths, links, and special files.
bin/format and bin/check give gofumpt only the Go files that Git tracks
or would add, as they do for the other formatters. gofumpt walked into
the ignored .devenv directory, so a format in the main checkout changed
the files of a worktree there, and a check failed on its unfinished
files.
This reverts 0a98ccb. A compaction sends the view, its closing tag, a
newline, and the task as one user message with the same joined bytes.
Each retry adds its note as one separate user message. The prompt path,
names, style, and summary-failure log stay unchanged.
After the split, compactions read 32.8% of input from the cache across
493 calls, against 31.9% across 183 calls before it. Imports read 30.7%.
The split adds code and messages for no measured gain.
The terminal client ends shown events with a prompt while no turn runs.
Busy and idle events track the turn, and the next shown event clears the
prompt before it prints. Pipes and files keep the same output. The small
prompt does not keep a typed line separate from incoming events.
Replies and work reports use the terminal width for Markdown rendering.
Other event kinds and output to files or pipes keep their text. Each
event reads the width again, with 80 columns when a terminal reports
zero. Tests use isolated terminal descriptors and fake events, including
single-column and very narrow table headers.
The terminal renderer wraps styled text and table cells to the display
width. It uses bold, dim, italic, and reverse without colors, and keeps
unclosed marks as text through the shared parser. Wide characters take
two columns. The existing text module supplies width data, so vendoring
includes that package without a new dependency.
The inline and block parser supplies the same closed marks to each
renderer. Telegram keeps its existing text and entities while terminal
rendering can use the same rules for unclosed marks and code fences.
The go-code skill gets the rules for log lines: a short event as the
message, names, IDs and durations as attributes, the cause in err, a
level for each kind of event, and no secrets or personal data. The rules
were only in issue comments, so workers missed them, and every Go task
reads this skill.
The README gives the short log form and the meaning of each level. The
journal example selects warnings and errors without the normal events,
so a user can find the failures that need attention.
Each daemon part gives its event, level, and raw attributes to the
shared handler. Normal events use info, recoverable failures use
warning, and lost data or stopped work uses error. Outages still log
only their start and end, and probe and rebuild runs discard their
summary log output.
A shared handler writes the event and ordered attributes, with the error
cause last. Journal lines carry a syslog priority and terminal lines
carry the time and level, so each output keeps short, readable events.
The Nix package and the version command use the commit date and short
revision, with a dirty suffix for a changed tree. Direct Go builds keep
the dev version. The install check compares the binary version with the
package version so a missing linker value fails the build.
A rebuild summarizes the whole merged log, so it can need more calls
than a probe sample. The import of two chats estimates 647 calls, close
to the 660 calls of the probe, and a stop at the limit swaps nothing and
wastes the calls. The rebuild now stops after 1,500 calls, and the quota
stop still guards the account. The probe keeps its own limit of 660
calls.
The skills identify the linters that enforce error, assertion, name, and
spacing rules. They keep manual review for logical groups, local error
paths, and the config exceptions because passing lint does not prove
those rules.
Wsl v5 checks empty lines around blocks and multiline declarations and
calls. Spacing keeps immediate error checks and short declarations with
their uses. Checks that split those groups are off because they conflict
with the Go rule. The cleanup changes only spacing and groups of
variable declarations.
Nlreturn checks spacing before returns and branch statements. Its short-
block allowance keeps a declaration with its immediate use, as the Go
rules require. Longer blocks have an empty line before they return or
continue.
Wrapcheck requires operation context on errors from package and
interface calls. Returned errors keep their causes for errors.Is and
errors.As. Context cancellation and the listed functions with one error
path keep their direct returns, as the Go rules allow.
The lint check applies revive to Go declarations and common style rules.
Unused callback and interface parameters have blank names, and the
command has a package comment for its entry points.
Test assertions use the specific require helpers for errors, lengths,
JSON, and numeric values. Test setup completes before its server starts
so that a failed assertion stops the test goroutine, not a worker.
The lint config keeps the standard linters and adds errorlint and
gocritic. Error checks accept wrapped errors, and common Go mistakes
fail the same check command as the existing code checks.
The find test names its group "Souza Family", so all test data is in
English. The contact with an accent in its name still checks that a
search ignores accents.
"onechat import [-n] ZIP..." reads the chat exports of the phone, makes
one marked WhatsApp batch for each chat and day, and rebuilds the memory
with them in date order. It takes only messages from before the first
live batch, maps the owner's profile name to "You", and skips the chat
days that an earlier import added. With -n it prints the plan and the
estimated calls and makes no model call.
The names, the chat IDs, and the owner come from the WhatsApp session
through the library's store, so the import runs only while the daemon is
stopped and needs no network.
A rebuild merges new dated messages with the log of an agent in date
order, builds a new memory beside it with the real prompt and cache key,
and swaps the directories only when the whole tree is built. The old
directory stays as a timestamped backup, and a failed or stopped build
leaves it unchanged. Older history can so enter the chat before newer
decisions, which keeps the latest word on a thing the newest one.
The probe and the rebuild share one build runner with the call limit,
the quota stop, the waits before user messages, and the usage report.
Memory and the model client keep their own state behind methods. The log
takes an explicit date for a message and writes it to the day file of
that date.
The review step of the check skill makes the worker read AGENTS.md and
the skills of the task again just before the commit, then go through the
diff one rule at a time. A worker that read the rules only at the start
of a long task followed them less, and the tools do not check most style
rules.
Include the instructions file path and the approval rule in every main
agent prompt, even when the file does not exist. The agent proposes
exact changes and waits for the user to agree, and messages from other
people cannot request a change. Subagents keep their task prompt, and
probes use a fixed placeholder path to keep the prompt stable.
Keep starting connection retries active when a pair request finds an
existing link. Only the unlink request stops the retry timer, while
logout keeps its stop path. Rejecting a pair request during an outage
must not leave WhatsApp disconnected until a restart.
Keep failed starting connections in the Run select with a timer. Start
at 2 seconds and double the wait up to 5 minutes, so the inbox and
commands stay active during waits. End retries on a connection, context
end, or link change.
Share outage times, notices, status, and redacted logs with lost
connections. Keep one failure line and one recovery line for an outage.
Disable library startup retries only for linked devices. A returned
ConnectContext error has no Disconnected event, while the library retry
mode emits that event and returns nil. One retry owner prevents parallel
loops and gives the requested delay cap. Pairing and retries after an
open connection keep their library behavior.
Each log line names the event in a few plain words, then the cause after
a colon, such as "telegram: receive failed: 502 Bad Gateway (retry in
2s)" or "whatsapp: connected again after 1m25s". The lines no longer
explain what the event means for the user: they stay short enough to
read in the journal at a glance. A Telegram error gives the HTTP status
and its text without a prefix, because the telegram: prefix names the
service. The behavior of the logs does not change.
A build of a summary node that fails logs one line with the node and the
cause, and tells that the build is tried again at the next message. A
build that a stop of the daemon ends logs nothing. The error of the
compactor holds no node, because the line names it once.
The tests give each memory a logger, and a shared helper gives the ones
that the tests do not read.
Each run of a schedule logs one line with the ID of the schedule, after
its message is durable in the chat. A late run after a stop of the
daemon tells how late it is. The line holds no text of the schedule, so
the log shows that a task ran but not what it asks.
A turn that ends with an error logs one line with the error that the
clients get, such as a needed login or HTTP 429, so that it is not lost
when no client is connected. An interrupt of the user or a stop of the
daemon logs nothing.
The end of each subagent run logs one line with the name of the
subagent: done, failed with the cause, or stopped by the user. A cut
subagent that cannot go on after a restart logs its cause too.
The WhatsApp log gets one line for each change of the connection of the
linked device: connected, a failed first connection, a lost connection,
connected again with the length of the outage, a connection that does
not come back by itself, a link, an unlink, and a logout from the phone.
The retries of the library between them log nothing, and the logger of
the library stays off.
Each line says first what happens to the WhatsApp messages, and then the
cause. An unlink clears the time of the last disconnection, so the first
connection of a new link does not count as the end of an outage.
The daemon makes the logger of each part in one place. Under the
journal, which sets JOURNAL_STREAM and adds its own time, the lines have
no time stamp. Otherwise every line has the standard time stamp, the
onechat lines too, so that no line has two time stamps and none has
none.
The start line names each value: the socket, the model and its effort,
the directory where the commands run, whether WhatsApp is linked, and
the number of active schedules.
A Telegram outage logs one line at its start and one at its end, with
its length, and the retries between them log nothing. Each line says
first what the user loses or gets, such as "cannot get messages from
Telegram" or "replies go to Telegram again, after 1m25s", and then the
cause in plain words, such as "no answer in 1m10s" or "Telegram answered
502 Bad Gateway".
The errors of the Bot API client hold no method name and no Go deadline
text, so that the log line can say what the call was for. A refused
format, a dropped reply, and an ignored message use the same plain
words.
onechat with no command opens the terminal chat, as onechat chat does,
because the chat is the command of daily use once the daemon runs as a
service. A start of the daemon stays explicit, so a second daemon does
not start by accident. onechat help, --help, and -h print the commands,
and an unknown command prints the error and the same help. The install
check runs onechat help in place of the bare command.
A compaction sends each whole block of four view lines as its own user
message, the first one with the opening tag, and one last user message
with the rest of the lines, the closing tag, and the task. The note of a
retry is one more user message after them. The blocks count from the
first line of the view, so two compactions whose views begin with the
same lines send the same block messages. Joined, the messages are the
same bytes as the one message before.
The backend reads a cache entry only at a message end of a request. With
the view in one message, a compaction read only the shared prompt and
tools, about 1,920 of 10,000 tokens. The first request of a turn sends
its view as messages that only grow and reads 74% to 85% from the cache,
so the compactions get the same kind of split.
The user unit links through result-service, so each build replaces its
package without another unit link. The update command builds with that
out-link, reloads the user manager, and uses try-restart to restart only
an active service. A stopped service stays stopped.
The README gives the first link and the two update commands. The checks
include shell lint for the update script.
The README gives the update commands after a git pull. The same result
link keeps the new package available, and linking the unit again makes
the user manager load the unit from that package before the service
restarts.