Agent workflows
This page is the shared operating manual for humans and LLM agents. Codebase work has exactly three top-level actions: create or groom an issue, create a PR, or review/update an existing PR. Everything else on this page is a supporting check inside one of those actions.
The three actions
- Create or groom an issue when the task asks for planning, scoping, or a work item that should outlive the current session. New issues require reading the relevant docs, code, and tests before writing the issue body; existing issues require reading from the opening comment forward before adding grounded plan detail.
- Create a PR when the task asks for new code, docs, configuration, tests, or a branch that can merge into
main. - Review/update a PR when the task starts from an existing PR, review comment, CI result, requested change, or "view this PR" request.
Do not invent a fourth agent workflow. Preparing, authoring a Restate handler, creating a legal workflow, running Markdown lint, checking GitOps, and consulting the councils are subroutines inside one of these three actions.
Create or groom an issue
Planning lives in GitHub issues, not local notes. For a brand-new issue, read the narrowest relevant docs, code, and tests first, then open the issue with grounded scope, test-driven steps, and the real files in blast radius. For an existing issue, read it from the opening comment forward, follow how the ask evolved, and comment the extra plan detail that a future worktree will start from.
If the issue encodes a decision worth keeping past the work, lift it into code, a doc, or the glossary when the PR lands; the issue is the working space, and the repository is the durable record.
Create a PR
The canonical ship path is gitops.md: branch, push, open a PR, enable squash auto-merge. Do not commit
directly to main.
Before changing files:
-
Rebase or otherwise confirm the branch is current with
origin/main. -
Prepare the task checkout and its host development state before the first edit:
cargo run -p cli -- dev worktree-env up --branch <topic-branch>The CLI fetches
origin/main. When a harness exportedNAVIGATOR_WORKTREE_PATH(or a compatible native worktree path), it creates the topic branch in that supplied checkout. With no supplied checkout, it creates.worktrees/<topic>beside the primary checkout. Continue in thetask checkoutpath the command prints; never create a nested worktree by hand. Keep new work out of the primary checkout and unrelated PR worktrees. -
Read
CLAUDE.md,AGENTS.md, and the most specific docs fromindex.md. -
Read
glossary.mdbefore using domain nouns. -
Read
access-model.mdbefore touching roles, participation, OPA, sessions, or visibility. -
Check the working tree with
git status --short --branch; never overwrite user changes. -
Pick the narrowest docs and code path that actually cover the task.
-
If the task changes English marketing or public Foundation prose, update the matching Spanish surface in the same PR according to
i18n.md; do not leave Spanish as a follow-up.
If the decision is architectural, legal-copy, or client-facing, use the relevant council in
agent-decision-councils.md after reading the facts.
When a dirty tree is ready to land:
-
Survey every change:
git status --porcelain,git diff,git diff --staged, and untracked files. -
Group paths by concern. One commit should have one blast radius.
-
Run the gate that matches the changed files before committing. If any Markdown files changed, run the Markdown gate across the workspace so CI-only wrap issues are caught locally:
cargo run -p cli --quiet -- validate .Always run the client-data gate — it guards the shipped data surfaces on every PR:
cargo run -p cli --quiet -- validate-no-client-data . -
If the PR changes Rust files or build/runtime configuration, run the full Rust gate:
cargo fmt cargo clippy --workspace --all-targets -- -D warnings cargo test --workspaceThen verify coverage locally — do not infer it from a green test run. Codecov is a hard merge gate (
codecov.ymlfails the PR when patch coverage drops below 90%), and a passingcargo testdoes not prove the number: CI measures it by re-running the same pass undercargo llvm-cov --workspace --lcov, and any test that skips in that pass contributes nothing. Harness-gated browser/e2e tests (thenew_client_or_skipsuites —reask_flow,browser_e2e, …) skip without chromedriver, exactly as they do in the coverage job, so code whose only exercise is such a test shows uncovered even though the suite is green. Spin up only what CI's coverage job spins up — Docker for the testcontainer DB (plus the OPA binary the policy tests shell out to), the same minimal topology, no KIND stack — and measure the changed lines before you push. A covering test that needs the full KIND stack (Keycloak, Garage, Restate, a browser) skips in that job just like the e2e, so it won't count either:cargo llvm-cov --workspace --lcov --output-path lcov.info # the same pass CI uploads to CodecovGive every handler, route, and branch a non-gated covering test — drive it through the router against a testcontainer DB (see
test-database.md) so it runs on every push — and keep the browser e2e as the live-walkthrough proof, not as the only coverage. If a line is genuinely unreachable in a test (a500infrastructure arm, a local-dev auth fallback), say so in the PR rather than letting it quietly fail the gate. -
Stage explicit paths for each group, not
git add -A. -
Use Conventional Commit subjects; use the PR title as the squash-merge commit title.
-
For any change to public or portal UI, always capture a live screenshot from the running app — boot
webagainst the persistent KIND deps (the fixture is usually already up; seeRUNBOOK.md) and capture with headless Chrome. Save it under/tmp/navigator-screenshots/and embed it in the PR description via thepr-image-uploadskill to get a URL that renders — a raw<img src="/tmp/...">renders broken on github.com. Do not self-host the artifact on a remote branch or commit it to the tree. Rendering tests are not a substitute for seeing the page served. If the page requires login, use the authenticated worktree flow inRUNBOOK.md:dev grant-staffmust target the sameDATABASE_URLas thewebprocess, and the browser session should come from Keycloak rather than hand-written cookies. -
Push and open a PR against
main; GitHub's merge queue lands it after the required checks pass. CI enables auto-merge on open, which enqueues the PR — do not rungh pr mergeyourself, and leave the decision of when it lands to the merge queue. -
Clean up task-owned local resources before ending the session. See Resource cleanup.
If the work should become multiple PRs, decide that before committing. Use the Engineering Council for real sequencing questions.
Grouping changes into commits
Partition the changed paths into the smallest set of coherent commits — each commit one reviewable concern with a single blast radius. Reviewers read commits, so a clean grouping is the deliverable.
Keep in one commit when the paths share a concern: a route handler, its view, and the test that covers it (TDD — the
test lands with the code it covers); a migration and the entity that depends on it; a generated artifact and its source
(docs/erd.svg with docs/erd.md). Split apart when the blast radius differs: a crate bump versus a vendored-asset
swap; a behavior-preserving refactor versus the behavior change built on it; two unrelated fixes found in one sitting;
CI or tooling (.github/workflows/, .agents/skills/) versus product code; docs-only edits unless the doc documents
that exact code change. While grouping, drop history the change leaves behind — "we used to…" narration, a
deprecated-but-kept flag, a dangling reference to a removed file — because code describes the present and git history
holds the past.
Write each subject as a Conventional Commit: <type>(<scope>): <subject>, imperative mood, lower-case start, no
trailing period, ≤72 chars. <scope> is the crate or area (web, store, cli, views, deps, mcp). Append !
after the type/scope or add a BREAKING CHANGE: body trailer for a breaking change. The PR title is the squash-merge
commit subject, so write it as the Conventional Commit you want in main's history.
| type | when |
|---|---|
feat | a new capability or user-visible behavior |
fix | a bug fix |
refactor | behavior-preserving restructuring (rename, move, extract) |
docs | docs, prose, or README only |
test | tests added or changed in isolation (usually folded into feat) |
chore | tooling, deps, skills, housekeeping |
ci | .github/workflows/ and CI plumbing |
perf | a performance improvement |
style | formatting only, no code change |
build | build system, Containerfiles, or Cargo manifests (non-dep) |
GitHub CLI authentication
gh drives every GitHub action here — gh pr create, PR review, and gh image uploads. Check auth before any remote
step:
gh auth status --hostname github.com
If auth is missing, use the browser device-code flow with SSH as the git protocol:
gh auth login --hostname github.com --git-protocol ssh --web
Relay the one-time code gh prints verbatim — never invent or transform it — and keep the login session alive until the
user finishes the browser step, then re-run gh auth status to confirm.
Review or update a PR
A PR review is not complete until every reviewer comment has been adjudicated against the real code and answered. main
is merge-only and lands via auto-merge once CI is green, so an unanswered — or answered-but-unresolved — comment is what
quietly keeps a green PR from merging. The deliverable is not "here is what I think"; it is "every open thread is closed
or has a decision on it, and the change was checked against running code." The flow has eleven steps; do them in order,
each assumes the prior one.
- Identify the PR — explicit number/URL if given, else the current branch's PR.
- Read the PR — metadata, the full diff, then the changed files at the head commit.
- Assess independently — form your own correctness + quality view before reading any comment.
- Bring up the PR worktree + right KIND environment — review against code that runs at this commit, not just the diff.
- Collect every comment — inline, summary, and review bodies, from every reviewer.
- Adjudicate each comment against the real running code — valid, invalid, or valid-but-won't-fix.
- Ask the user, per actionable comment, whether to fix it (recommendation first).
- Apply the approved fixes — prove each against the running app, with its covering test.
- Resolve every comment — reply via
gh, then mark real threads resolved. Mandatory. - Update the branch from
mainwhen it is behind and that blocks the merge. - Report — findings, every verdict, fix shas, branch state, and that all threads are closed.
Step 1 — Identify the PR
If the user named a PR (a bare number, a URL), use it. If they did not — a plain "review PR" — default to the PR for the current branch; do not ask "which PR?". The branch is the answer:
gh repo view --json nameWithOwner -q .nameWithOwner # the {owner}/{repo} for this checkout
gh pr view --json number -q .number # the PR whose head is the CURRENT branch
Only fall back to asking when the lookup genuinely can't decide: there is no PR for the branch (offer to create a
PR if the tree has unshipped work), or on main/detached/in another worktree — then list the open PRs
(gh pr list --json number,title,headRefName) and ask, unless the preceding conversation already pins one. Pass the
resolved slug to every gh call below as --repo <owner>/<repo>.
Step 2 — Read the PR
gh pr view <N> --repo <slug> \
--json title,body,state,author,baseRefName,headRefName,additions,deletions,changedFiles,mergeable,reviewDecision
gh pr diff <N> --repo <slug> # full diff; scope to the files you care about if it is large
The diff is the claim; the files at the head commit are the truth. Read the real files, not just the patch hunks — a comment can be wrong because of context outside the hunk.
Step 3 — Assess independently first
Before you read a single comment, form your own view so the bots don't anchor you. Focus on what breaks or rots:
- Correctness — does each changed path do what its name and the PR claim? Trace the real path; "it compiles" and "it looks right" are not evidence. Run or read the covering test.
- Tests that lie — an assertion that passes for the wrong reason (short-circuits before the code it names, or matches an always-present string). Flag these even when the bots miss them.
- Schema / migration ordering, transactional integrity, and auth checks (route through the authorization model in
access-model.md), plus the workspace invariants in../CLAUDE.md. - History the diff reintroduces — "we used to…" / "legacy" narration, a deprecated-but-kept flag or alias, a comment or test describing superseded behavior. Code describes the present.
- Quality — reuse, dead code, altitude — secondary to correctness.
Write this up as your own findings; you reconcile it with the comments in Step 6.
Step 4 — Bring up the PR worktree and right KIND environment
Adjudicate against the code as it runs at the head commit, not just as it reads in the diff. Materialize a dedicated worktree on the PR branch, fetching the PR ref first so the command works from a fresh checkout and for fork PRs:
git fetch origin pull/<N>/head:pr-<N>
cargo run -p cli -- dev worktree-env up --branch pr-<N>
The same command works from a primary checkout or an agent-supplied worktree. Continue in the printed task checkout
path; that checkout owns the PR branch, host port, and .devx descriptor. The current dependency tier and database are
shared across worktrees.
worktree-env up brings up the shared KIND dependency tier if it is not already reachable, migrates its navigator
database, and picks a stable per-worktree web port in .devx/env. Boot web against it (see
RUNBOOK.md) so that when you reach a comment you can actually run the covering test and click through
the affected page. web is a compiled binary — restart it after any fix, or you are looking at the old build.
worktree-env runs host web and the in-cluster workflows-service against the same shared navigator database, so
it can exercise Restate-backed flows. That agreement also means parallel worktrees share migrations and application
rows; choose fixtures deliberately and do not mistake a distinct host port for data isolation. This live surface is what
Steps 6 and 8 adjudicate and prove against; it is not optional context, it is how the review stays grounded.
Step 5 — Collect every comment
There are three comment surfaces on a GitHub PR; pull all three — reviewers split findings across them (Greptile, for one, puts unplaceable findings in its summary, not inline):
# (a) inline review comments — anchored to file + line, these form resolvable threads
gh api --paginate repos/<slug>/pulls/<N>/comments \
--jq '.[] | {id, user: .user.login, path, line, original_line, diff_hunk, in_reply_to_id, body}'
# (b) issue/PR-level comments — top-level, includes review SUMMARIES (the Greptile overview)
gh api --paginate repos/<slug>/issues/<N>/comments --jq '.[] | {id, user: .user.login, body}'
# (c) review bodies — the approve/request-changes top notes
gh pr view <N> --repo <slug> --json reviews -q '.reviews[] | {author: .author.login, state, body}'
--paginate is load-bearing: without it gh api returns only the first 30 items and silently drops the rest — and a PR
with more than 30 inline comments is exactly where leaving one unanswered is easiest. Read summaries in full — they
carry P-rated findings and "comments outside diff" that never became inline threads. We wire Greptile and human
reviewers today (plus whatever bot is added later); treat every distinct finding as a comment to adjudicate, wherever it
lives.
Step 6 — Adjudicate each comment against the running code
First filter the Step 5a set to thread roots — comments where in_reply_to_id is null. The non-null ones are existing
replies, not new findings; keep them for context (they tell you whether a thread is already answered) but only roots
enter the loop. For each finding, do not take the reviewer's word for it — open the cited file at the head commit and,
where the claim is about behavior, reproduce it against the live env from Step 4:
- Valid — the claim holds against the real code (and reproduces when run). Confirm severity; note the exact fix.
- Invalid / false positive — the claim is wrong (missed context, an intentional codebase-wide pattern, a "bug" that can't occur). Note why, with file:line evidence.
- Valid but won't-fix — real but not worth changing (matches a file-wide pattern, guarded elsewhere). Note the rationale.
State your verdict on each with the evidence, so the user decides from facts, not from a bot's confidence.
Step 7 — Ask the user whether to fix
For every comment you classified Valid (and any won't-fix you're unsure of), ask the user whether to apply the fix, recommendation first. Do not silently fix or silently skip — the user decides what lands. Invalid / false-positive comments need no fix question, but they still get a reply in Step 9 explaining why you're not acting.
Step 8 — Apply the approved fixes, proven against the running app
On the PR worktree from Step 4, fix exactly what was approved and prove it — don't reply "fixed" on the strength of a re-read:
- Add or update the covering test in the same change, and run it. For a "test that lies" finding, the fix is making the test exercise the path it names, then proving the assertion keys on something only the real path produces.
- For a UI or behavior change, click through it in the running
webon the topology that can exercise it (restart it first) and confirm the changed state. If a comment asks for a live walkthrough/screenshot, capture it and embed it in the PR body via GitHub'suser-attachmentsCDN (a raw/tmppath renders broken) — the embedded image is the fix. - Honor the workspace gate before committing (
cargo fmt,cargo clippy --workspace --all-targets -- -D warnings,cargo test,cargo run -p cli --quiet -- validate-no-client-data ., plus Markdown lint on any.md). When a comment is itself the failing Codecov check — or a fix touches code exercised only by a harness-gated e2e — verify coverage locally withcargo llvm-cov --workspace --lcovand add a non-gated covering test (through the router against a testcontainer) rather than replying "the e2e covers it"; see the coverage note in Create a PR. - Commit on the branch as a Conventional Commit referencing the finding, and push so CI re-runs:
git add <paths> && git commit -m "test(web): exercise the real client-DRI guard (Greptile P2 on #<N>)"
git push origin HEAD:<headRefName>
Step 9 — Resolve every comment (REQUIRED)
This is the point of the action. A PR is not reviewed until every comment has a reply and every thread you handled is marked resolved. Replying is not enough: branch protection requires conversations to be resolved, so an answered-but-unresolved thread still blocks auto-merge — the PR sits green but unmergeable. Do both, in order: reply, then resolve.
Reply to inline threads (use the comment id from Step 5a):
gh api repos/<slug>/pulls/<N>/comments/<comment_id>/replies -f body='Fixed in <sha> — <one line>.'
# or, for a won't-fix / false positive:
gh api repos/<slug>/pulls/<N>/comments/<comment_id>/replies \
-f body='Acknowledged, not fixing — <rationale with file:line evidence>.'
Reply to summary-only findings (no inline thread) with a top-level comment that names which finding it answers:
gh pr comment <N> --repo <slug> --body 'Fixed the P2 "<finding title>" from the summary in <sha>: <what changed>.'
Mark real review threads resolved — REST replies don't flip the resolved flag; that's a GraphQL mutation. List thread ids, then resolve each one you've answered:
gh api graphql -f query='
query($owner:String!,$repo:String!,$pr:Int!){
repository(owner:$owner,name:$repo){ pullRequest(number:$pr){
reviewThreads(first:100){
pageInfo{ hasNextPage endCursor }
nodes{ id isResolved comments(first:1){ nodes{ databaseId author{login} path line } } } } } }
}' -F owner=<owner> -F repo=<repo> -F pr=<N>
gh api graphql -f query='mutation($id:ID!){ resolveReviewThread(input:{threadId:$id}){ thread{ isResolved } } }' \
-F id=<threadId>
Match each thread to the finding by its first comment's databaseId — the same id from Step 5a, so it maps exactly.
Don't match on author + path alone: several comments by one reviewer on one file collide, and you risk resolving the
wrong thread while a real one stays open and blocks the merge. If pageInfo.hasNextPage is true, fetch the next page
(reviewThreads(first:100, after:<endCursor>)) before assuming you have them all. Resolve only after a thread is
genuinely handled; leave one open only when you are deferring to the user and they haven't decided — and say so in
the reply, since you are knowingly leaving the merge blocked.
Step 10 — Update the branch from main when it's behind
A PR can be fully reviewed, every thread resolved, CI green — and still not merge because the branch has fallen behind
main and branch protection requires the head to be up to date. Clearing that is part of the review:
gh pr view <N> --repo <slug> --json mergeStateStatus,mergeable -q '{mergeState: .mergeStateStatus, mergeable}'
BEHIND— out of date withmain. Fast-path it withgh pr update-branch <N> --repo <slug>(no local checkout needed). If that reports a conflict, the branch is effectivelyDIRTY: stop and ask the user before resolving locally — don't auto-resolve and push. Only when they ask, switch to the PR branch,git merge origin/main, resolve, re-run the gate, and push.BLOCKED— usually pending CI or an unresolved thread, not staleness; don't update-branch to "fix" it. Confirm withgh pr checks <N>and the Step 9 thread list; auto-merge lands it when the blocker clears.DIRTY— merge conflicts withmain. Flag it in the report; don't attempt an automated update unless the user asks you to resolve locally and re-run the gate.CLEAN/HAS_HOOKS— nothing to do.UNKNOWN/UNSTABLE— GitHub hasn't computed mergeability yet, or a non-required check is failing; neither is staleness. Re-query forUNKNOWN; forUNSTABLEconfirm withgh pr checks <N>and leave auto-merge to land it.
Only ever update the branch under review, never with a force-push, and never someone else's branch without saying so in the report. After an update, CI re-runs and auto-merge lands it when green.
Step 11 — Report
Summarize: your independent findings, every comment and its verdict (valid / invalid / won't-fix), what was fixed (with
shas) and how you proved it against the running app, whether the branch needed updating from main, and confirmation
that every thread now has a reply and the handled ones are resolved. Call out anything still open and why.
Reviewing operates on an existing PR: turning a dirty tree into a PR is Create a PR; shipping to prod is
the prod-deploy flow. Every fix you push clears the gate (fmt / clippy / test / validate-no-client-data / Markdown
lint) and ships with its covering test.
Supporting checks
Markdown lint
Use the workspace CLI, not a separate Markdown linter:
cargo run -p cli --quiet -- validate <path>
validate classifies each file automatically, so ordinary docs get the prose rules and notation templates get the
N-family rules. It walks the whole tree — root files such as AGENTS.md and CLAUDE.md are checked as prose — and
folds in the typed event pass and a .yaml/.yml parse over the same walk. Bare validate defaults to ..
CI runs the repository-wide classified pass on every pull request update:
cargo build -p cli --quiet
./target/debug/navigator validate .
That command builds the Neon Law Navigator CLI, checks every included Markdown file in the visible repository tree, and
applies the Template superset to files under templates/ or any Markdown file with questionnaire: or workflow:
frontmatter.
No client data in the repo
The repository holds the firm's OWN data and synthetic fixtures — never a client's. Only the firm's own contact
addresses (@neonlaw.com / @neonlaw.org) and Nick Shook's (shook.family) may ship; every other email must be a
reserved synthetic domain (example.com, or a *.example / *.invalid / *.test / *.localhost address, per RFC
2606 / RFC 6761), and phone numbers do not belong in shipped data at all.
The gate scans only the surfaces that ship data to people — store/seeds, templates, and web/content — because
that is where a real client's email or phone would leak. Source and test code carry synthetic fixtures (a@b.com, a
nick@gmail.com negative test) and are reviewed by humans and this checklist, not by the domain allowlist. Run it
locally, and let CI run it on every pull request update:
cargo run -p cli --quiet -- validate-no-client-data .
It reports each finding as path:line: NCD-EMAIL/NCD-PHONE and exits non-zero on any, so the ci gate fails fast —
next to validate / dev i18n, never a fourth CI workflow. When adding data that legitimately needs a non-firm
address, use a reserved synthetic domain rather than a real one.
Legal workflow authoring
Use this path when adding a new matter type or extending a template's workflow. Do not solve legal workflows with a one-off router handler when a template + questionnaire + workflow can express the matter.
- Write the composition
.featurefirst infeatures/tests/features/. - Create or edit the template under
templates/forms/...ortemplates/neon_law/<product>/.... - Add new questions to
store/seeds/Question.yaml. - Compose the workflow from documented step prefixes in
notation-authoring.md. - Add reusable
StepKindand dispatch code only when the existing step registry cannot express the work. - Put every external or non-deterministic side effect behind Restate durability.
- Add tests in the same commit as the implementation.
The core rule is still: the Template declares; Restate runs.
Restate handler authoring
The full architecture is durable-workflows.md. For Rust handler code, the one replay-safety
rule is load-bearing:
Every non-deterministic act belongs inside
ctx.run(...).name("stable-name").
That includes clocks, randomness, UUIDs, database writes, object storage, network calls, and third-party APIs. The
handler body may replay; ctx.run journals the result so replay reuses it instead of re-executing the side effect.
Use terminal errors for invalid input that can never succeed later. Use retryable errors for infrastructure failures. Do
not use native tokio::spawn, join_all, or channels for journaled steps inside a Restate handler; use Restate SDK
sequencing/combinators or keep the steps sequential.
GitOps and deploy
The branch-to-prod path is:
- PR merges by squash into
main. release-tag.ymlcuts aYY.M.Dtag.deploy.ymlpublishes all images to ghcr.io.- An operator rolls GKE onto the dated tag.
Read gitops.md, gke-prod.md, and cloud-operations.md before
changing CI, release, deploy, cluster, or production secret behavior.
Always roll navigator-web, workflows-service, and the optional navigator-git Deployment together. A version skew
between the public web surface, durable worker, and git writer is a production risk.
Resource cleanup
Neon Law Navigator is a large Rust monorepo; agents should assume disk and memory are scarce. Before ending a create-PR or review/update-PR session, clean up resources created for that task.
For Cargo builds:
- If a task did not change Rust files and only needed Markdown validation, do not run Cargo build/test commands that
create a worktree
target/directory. - If Rust checks or e2e tests created build artifacts in the task worktree, run
cargo cleanin that worktree after pushing the branch or updating the PR. - If you set a task-specific
CARGO_TARGET_DIR, clean that directory before handoff. Do not delete sharedCARGO_HOMEcaches or a shared target directory that other worktrees may be using.
For Docker, KIND, and browser e2e:
- The KIND dependency tier (Postgres, Keycloak, OPA, Garage, Restate) is a reusable dev fixture, not a
per-task resource — leave it running across sessions. At handoff stop only the host-side
webprocess and any task-owned browser drivers; do not runcargo run --release -p cli -- dev downas routine cleanup, since that deletes the cluster and forces a slow rebuild next time. Full teardown is for a deliberate clean rebuild only. If a port-forward died, re-runningdev upreuses the existing cluster. SeeRUNBOOK.md. - Remove task-created standalone containers and images when they are no longer needed. Reclaim Docker build cache after
image-heavy or e2e work with
docker builder prune --force --filter until=24h, or the narrowest equivalent that matches the resources you created. - Use
docker system dfbefore broad cleanup.docker system pruneremoves stopped containers, unused networks, dangling images, and unused build cache; add-aonly when you intentionally want unused images removed too. - Do not prune Docker volumes unless the user explicitly approves the data loss. Docker does not remove volumes by default for the same reason.
Measure before and after cleanup when disk pressure is part of the task (df -h ., docker system df, or both), and
report anything left running or left on disk.
Maintenance support
- Dependency refresh: follow the Rust crate and web asset sections in
rust-programming.mdand the vendored asset rules inweb/public/VENDOR.toml. - ERD refresh: regenerate
erd.mdanderd.svgtogether after schema changes. Government forms: use canonical issuing-authority sources and keep provenance ingov-forms.md. Disk cleanup: measure first, reclaim safely, and do not delete Docker volumes unless the user approves the data loss.