Reference
Build and restart registry: block-unregistered-build-or-restart
A gate on pre_tool_use:bash (tama hook block-unregistered-build-or-restart) that
refuses a build without an unused, unexpired entry recording the agent's statement that the whole
task is finished. Each accepted build consumes its entry, so the next build needs a new statement
and intent. A restart still needs an open entry but no task statement. Refusals start
BLOCKED: unregistered build: or BLOCKED: unregistered restart:, go to stderr
and exit 2 before the command runs. The registry is
~/.shared-hooks/build_restart_registry.json; tama build and Tama
Desktop share its writer.
Availability: this completion check is in source, not yet in
the installed hook release. Registration and installation require the device
owner's protected hook-release action. An old CLI may still record a build
without completed_task; that is not evidence of this gate running.
#Why it exists
A build that dies in the builder queue on cargo fmt --check, on one clippy lint, or on a
missing rollback declaration has spent twenty to forty minutes of a fleet builder on something checkable
locally in under a minute. The shape underneath is an agent making a minimal change, submitting, waiting
for the builder, reading the failure and making another minimal change — and restarting or installing
units nobody asked for along the way.
A build spends a fleet builder, of which there are two for darwin-arm64, and a restart
spends a service that other work is reading. So each is stated before it runs — what it is for, which
revision it carries, when the authorization stops — and entries are rationed. Builds use Stado's
fleet budget when available, or Tama's local rolling allowance otherwise. A user-approved exception
can exceed the daily count without changing the default policy; change-size checks still apply.
#The ration
| Limit | Where it comes from | What it stops |
|---|---|---|
builds_per_day | The policy file; without it the operator's number, 3, read from builds_per_day in numeric-provenance.json | The local fallback refuses another build after this count in rolling 24 hours. When Stado answers, its UTC-day fleet budget is authoritative. An explicitly approved and audited exception may exceed the count, not the other checks. |
restarts_per_day | The policy file only | Counted separately, so a spent build allowance never holds back a service that has to come back. |
minimum_changed_lines | The policy file only | A build is measured with git against the revision the last recorded build of that target carried. Under the threshold it is refused and told to travel with the next serious change; the same revision twice is refused as a build of nothing. |
window_minutes | The policy file only | How long one recorded intent authorizes, and the
ceiling on --minutes. |
The limits live in ~/.shared-hooks/build_restart_policy.json and are read with
tama build policy. A limit the file does not set has no value in code: recording an
intent is refused with <file> sets no <field>: the operator states it with `tama build
policy <option> <n>`, which needs DEVICE_HOOK_EDIT_APPROVED=1 set outside the agent
session. Tightening a limit needs nothing; setting one the file did not state, or loosening one
— more builds a day, a smaller change, a longer window — needs DEVICE_HOOK_EDIT_APPROVED=1
set outside the agent session, because an agent that can raise its own allowance has none. The local
rolling count comes from ~/.shared-hooks/build_restart_ledger.json. Ordinary intents are
retained for thirty days; approved exceptions remain as audit evidence. Closing an entry ends the
authorization and does not refund it or renew its consent. A ledger or policy that cannot be read
refuses the build.
The recorder's local forecast waits until usage drops below the current limit, including when
approved exceptions or a lowered limit have left usage above it. A zero limit has no timed reset.
The gate does not infer spent usage from open entries, because closing an authorization does not
refund an intent. The CLI and tama request builds/record share the recorder's refusal. These corrected
diagnostics require the updated native release described in the build
command.
One build entry authorizes one attempt against one target before expiry. The written completion statement is retained with the revision and reason; Tama requires the statement but does not independently prove that the task is finished. An agent must not record it while work remains:
tama build record --kind build --target skarbiec \
--reason "Validate the committed identity checks in the shared build" \
--revision 45a5c678042147f76241adf76fb7cf217e121ca6 \
--completed-task "The whole requested identity change is finished in code and documentation"
tama build record --kind restart --target brama-desktop-model-router \
--reason "the router holds a revoked credential and reads it once at start" \
--minutes 30
tama build list
tama build close --kind build --target skarbiec
tama build policy
tama build policy --builds-per-day 2 --minimum-changed-lines 80
| Field | What the command demands |
|---|---|
--kind | build or restart. Nothing else is an entry. |
--target | The product for a build, the service or unit for a restart. A build
whose command names no target of its own — stado build submit --source .,
stado release submit --source ., cargo build — is registered against the
repository the command runs in. |
--reason | Required and not blank, because the entry exists to be read later by somebody who was not there. |
--revision | Required for a build, and it must be a full forty-character source revision. A build that cannot name what it carries is not recorded. |
--completed-task | Required for a build: which whole task the agent has finished before building. A missing or blank statement refuses recording and spends no allowance. Restarts do not require it. |
--repository | Where the change is measured for a build. Without it, the repository the command runs in. |
--minutes | How long the entry stays open: one to the policy's window, which is also the value when the option is absent. Past that it authorizes nothing, so an entry that is never closed cannot become a standing permission. |
--approval-session, --approval-quote | For builds only, together. The quote must match Tama's captured current user message, and Brama must verify explicit permission to exceed the daily count for the named product or batch. A request to add this mechanism is not permission to use it. The entry retains the original message, its digest, session, capture time, product, revision and verdict. |
A registry file that is missing, empty or malformed holds no entries, which refuses every build and restart rather than letting them all through.
The CLI build approval --target NAME --revision FULL_SHA --json returns an unexpired
entry only when its evidence also exists in the audit ledger. Stado reads it before charging an
over-limit native build and stores the evidence with the run and platform in its canonical registry.
The same message cannot grant another Tama intent for that product, and the same approval cannot
charge another Stado run for that platform. The default count, expiry, duplicate-revision refusal,
minimum change, host admission and publication permissions are unchanged.
Tama Desktop exposes the required completion statement, recording, refusals, retained approval history and closing in Policy → Build registry. A used build entry remains visible but no longer authorizes another attempt. Closing blocks subsequent approval reads; it does not cancel an already accepted job. See the build command for the complete flow and request fields. Older Tama releases do not enforce this statement.
#What is refused
Whether a command builds or restarts is read by a model through Oko's model call
(block_unregistered_build_or_restart/judge-prompt.md), not from a list of programs and
subcommands: every build tool, script name or service manager such a list left out built or restarted
unregistered. A build compiles, bundles, packages or releases a product: a build tool's
build, release or install step (cargo build, swift build, xcodebuild,
stado release submit, a project's build script through its package manager), or a push of a
checkout that builds on every push. A restart starts, stops, restarts, reloads or
reinstalls a running service (launchctl kickstart, systemctl restart,
stado service restart). The model names each build's directory and each restart's service; the
build's target is the repository that directory belongs to, read from the checkout.
The refusal prints the tama build record line for that exact kind and target, so the
route out of it is the command itself. A question the judge leaves unanswered refuses the command with
TAMA_HOOK_INFRASTRUCTURE: block-unregistered-build-or-restart: review unavailable: …, as
asking a model describes.
A push is a build when the repository builds on every push: a Vercel-linked checkout
(.vercel/project.json or vercel.json), where Vercel starts a production
build, or a repository carrying a CI definition that runs a build or a suite on push,
pull_request or pull_request_target (under .github/workflows/,
.circleci/, .buildkite/, .woodpecker/, or a
.gitlab-ci.yml, azure-pipelines.yml, bitbucket-pipelines.yml or
.travis.yml). Whether this checkout builds on every push is read from the checkout and handed
to the model as a fact; such a push is refused until a build entry with the whole-task statement is
recorded for that repository. A push from a checkout with none of these, and reads such as
vercel inspect, pass.
The write gate (pre-write-edit) refuses the other half: a write or edit that adds such a
build or suite step to a CI definition firing on every commit is refused with the step it found, so a
build refused in the shell cannot move into a runner.
The model reads directory changes on the line (cd, git -C <path>) and
names the directory a build runs in; it is resolved from the working directory the payload names (or this
process's own when it names none).
#What passes
Reads of the same programs (stado build status, launchctl list), help requests,
formatting, linting, type-checking and tests that produce no product are not build commands for this gate,
and neither is a command that only mentions a build in data, such as grep 'cargo build' notes.md
or a commit message. Other gates may still refuse hand-run compilation or tests; passing this gate is not
permission to bypass the shared build process.
#Where the refusal comes from
Source: rust/crates/tama-hook-tools/src/ports/block_unregistered_build_or_restart/mod.rs,
with the judged reading in detect.rs and judge-prompt.md, where a command runs and
whether a push builds in directory.rs, and the registry reader in registry/; its
program:
rust/crates/tama-hook-tools/src/programs/block/safety/execution/block_unregistered_build_or_restart.rs,
run as tama hook block-unregistered-build-or-restart; the writer:
rust/crates/tama-cli/src/builds/mod.rs; the declaration the registration
reads: hook-declarations/block-unregistered-build-or-restart.json. The journey
cargo test --manifest-path rust/Cargo.toml -p tama-cli --test build_restart_judged runs the built
hook in a real git checkout under an isolated home: with the judge recorded as not answering a build passes
with the stderr note, and tama build record --kind restart --target system/com.example.unit
writes its registry entry; with TAMA_TEST_BUILD_JUDGE=1 and -- --ignored it asks
the real judge and requires cargo build refused naming the checkout,
launchctl kickstart -k system/com.example.unit refused naming the unit, a quoted mention and a
--help passed, and the restart passed once recorded. Registration is
two commands:
DEVICE_HOOK_EDIT_APPROVED=1 tama hooks declare --file hook-declarations/block-unregistered-build-or-restart.json
for the catalogue entry, the registry entry and the reseal, and
DEVICE_HOOK_EDIT_APPROVED=1 tama hooks release for the binary and the installed release.