Skip to content

Ruling log: planning and resumption

Sections 1 to 9 of the ruling log, moved here unchanged so that the log stays below repo.max_file_kb (ruling PL/4 there). The rules and the reading guide of the IDs stand in DECISIONS.md; the IDs keep their numbers and meaning.

1. Fundamental decisions of the lead

Fixed architecture decisions that all teams used in the same way.

ID Decision Reason Location
D-B1 Modular monolith in Go: one binary with strict, hexagonal modules and the roles api, worker, all; no microservices. The binary is called bothy (D-L1). A single host is the first target: one deployment, no distributed transactions, low latency, simple backup. The roles allow splitting off later without restructuring. ADR 0004, ADR 0008, 02 3.2
D-B2 Server in Go. OpenAPI 3.1 and AsyncAPI 3.0 are normative (spec-first); server stubs and clients are generated. SQL is typed (sqlc), migrations are versioned. Static binary, small images for several architectures, concurrency for WebSockets, mature OTel support. Python remains for the SDK and the Hermes adapter. ADR 0005, ADR 0013, ADR 0007
D-B3 PostgreSQL from day 1, no SQLite in the server; access only through the ports of the modules; address and port of the database are configuration. Full-text search, concurrency, managed Postgres services at all cloud providers, one database path instead of two. ADR 0006, 03 1
D-B4 Files behind an object storage interface (S3 API) with the drivers file system (default) and S3-compatible. The same code runs on a single host and in the cloud. ADR 0010, 12 4
D-B5 Apps with Flutter from one codebase for iOS, Android, macOS and Windows. The web target is only the admin and statistics console (with terminal since the addendum, D-T4), no chat in the browser. All four target platforms with one codebase; design system and API client are shared. ADR 0027, ADR 0028, ADR 0070
D-B6 Plugins and bots never run in the server process. Extension only through the public API: principals with token and grants, gateway WebSocket, signed webhooks, app manifest. Isolation and security; the product uses the public API itself from the start, Hermes Agent is only one client among several. ADR 0022, 11 1
D-B7 One function authorize(principal, action, resource) in the module authz checks every route, every event, every delivery, every push, every search, every file fetch and every agent invocation. Without view permission the server answers 404, not 403. A test fails on every route and every event without an action. One single decision point instead of scattered permission logic; no information about the existence of other people's objects. ADR 0017, 05 1, 07 4
D-B8 Event log with a global, commit-ordered sequence (counter row with a row lock in the same transaction); catch-up "everything from number X", filtered by authorize; in addition a sequence per thread. Every write request carries a client ID (UUIDv7); a repetition returns the original. bigserial is not commit-ordered; only this way does a catching-up client lose nothing. ADR 0009, 04 3, 04 5
D-B9 Identity through a provider interface: OIDC, header of a trusted reverse proxy (only with loopback), local accounts. Device binding with DPoP (RFC 9449); admin actions require a fresh step-up. No requirement for a particular network tool in the product; a stolen token is useless without the device. ADR 0019, ADR 0020, 08 2
D-B10 Agent runtimes run outside the server. The server knows agents as principals, runs as objects and one backend policy per agent; it never stores Claude.ai credentials or subscription tokens. Anthropic's terms forbid collecting, storing or brokering such credentials (quotes in 10 2). ADR 0022, ADR 0024, ADR 0025
D-B11 Telemetry with OpenTelemetry, export only through an OTel Collector. The audit log is separate from it (own table, hash-chained, own retention). No message contents in telemetry. Vendor-neutral; audit needs different guarantees than operational data; privacy. ADR 0034, ADR 0021, 17 2
D-B12 Distribution as OCI images for linux/amd64 and linux/arm64, Docker Compose as a one-command installation, OpenTofu modules per target. Configuration according to 12-factor, secrets through a provider interface. The server does not depend on macOS. The server should run the same way on a single host and in any cloud. ADR 0012, ADR 0011, 16 3
D-B13 Single tenant: one instance is one organization and one "server" in the Discord sense; no server bar, no invitation links for outsiders. Several tenants mean several instances. Smaller attack surface and simpler permissions; separation through installations instead of through code. 00 Instance, 01 3

2. Invariants

ID Decision Reason Location
D-I1 An agent with billing_mode = personal may only be triggered by its credential_holder. The server checks this transactionally on every change to grants, groups, owner list, operator and policy and rejects every change that would violate it. No owner exception (D-L22). A personal subscription serves only its person's own use. 10 3, 10 5, 03 6.4, ADR 0024
D-I2 The dispatch path checks again immediately before every agent invocation; if the triggering person does not match, there is no invocation (fail closed, audit). A second safety net next to the transactional check. 10 6
D-I3 The server signs every invocation with a short-lived invocation token. The adapter checks the signature and the configured mode against its proven reported mode and starts no run on a mismatch (billing_mode_mismatch). The server does not see the runtime; only the adapter can prove the reported mode. 10 7, ADR 0023
D-I4 Switching the policy is an admin action with step-up, reason and audit, global or per agent; to personal only if I1 holds afterwards. Cost bearer and terms change only deliberately. 10 9
D-I5 The server knows no credentials. The API key is with the adapter in its secret provider; the personal subscription stays in the runtime of the person. D-B10. 10 3
D-I6 If llm_credentials = personal is declared on a host, only credential_holder_id may open a terminal there, not even another owner. Checked transactionally on grants and on every open. A terminal allows any use of the login of the OS user; a personal subscription must not run for others (D-T2). 24 7.3, 03 6.8, ADR 0068

3. Rulings from the research

The lead decided the research before the teams. Evidence with URL and retrieval date is in the named ADRs and chapters; versions are pinned in toolchain.md.

ID Decision Reason Location
D-R-STACK Go, PostgreSQL, pgx with sqlc, migrations with goose, oapi-codegen (strict server, conservative spec without multi-type unions), coder/websocket, River for background jobs without Pro features, go-webauthn, OTel with otelhttp and otelpgx. LISTEN/NOTIFY only as a wake-up signal without content, fan-out in the process. Mature libraries with free licenses; goose works with plain SQL; a NOTIFY with content costs a lock at commit. ADR 0005, ADR 0007, R-AR-09, R-SY-15
D-R-OBJEKTSPEICHER File system driver as the default; the S3 driver uses only core S3 (PUT, GET, DELETE, multipart, presigned PUT). For self-hosted S3 the product documents SeaweedFS, not MinIO. Images in pure Go. Current state: metadata is removed losslessly according to D-L16. Broad compatibility without special features; reasons and evidence on MinIO in ADR 0010. ADR 0010, ADR 0047, R-FI-07
D-R-CLIENTS Clients for TypeScript (openapi-typescript), Python (openapi-python-client) and Dart (openapi-generator with dart-dio) are generated from the spec and compiled in the gate. Generated clients cannot deviate from the contract. ADR 0026, 06 14
D-R-DOKU MkDocs with Material, pinned; switch to Zensical only when a stable version and the features in use are proven. Diagrams with mermaid-cli. Material is in maintenance mode, Zensical is not yet stable. ADR 0045
D-R-PASSKEYS Device binding and step-up through a device key in secure storage with user confirmation; WebAuthn passkeys for the web console and for instances with a public domain; local accounts additionally with password and TOTP as a fallback that can be switched off. Native Apple apps need a publicly reachable Associated Domain for passkeys; private instances would otherwise have no step-up. ADR 0019, ADR 0020, 08 6
D-R-PUSH iOS and macOS: visible push with mutable-content, only IDs and a neutral placeholder; the Notification Service Extension fetches the content through the API. Android: FCM data message with IDs only. Windows: the app in the notification area holds the connection and shows local notifications. Push keys are instance configuration. No message content at Apple or Google. ADR 0031, 13 5
D-R-VERTEILUNG macOS with Developer ID, notarization and Sparkle 2; iOS through TestFlight; Windows as MSIX with .appinstaller; Android through a test channel of the publisher's choice. Current state: macOS only as a Developer ID edition (R-RL-06). Updates and package identity without a store requirement on the desktop. ADR 0037, 18 8
D-R-HERMES Reference adapter for Hermes Agent as a platform plugin; the progress is mapped to the run; the adapter recognizes the reported mode only by the type of the key, never by its value. Verified public extension points instead of text parsing (D-L7). 09 17
D-R-RUNNER The Claude Code runner starts claude -p with stream-json, one session per thread, the environment as an allow-list, follow-ups over stdin and stop by signal. Costs only as an estimate. Current state: the pre-check of the reported mode is binding (R-AG-20). Enforce conditions through the environment instead of assuming them. 09 18
D-R-ANTHROPIC The personal mode is intended only for runtimes in which a person uses their own subscription for their own use. The product does not collect, store or broker Claude.ai credentials. Whether a particular runtime is permitted is the responsibility of the instance operator. https://code.claude.com/docs/en/legal-and-compliance, retrieved 2026-10-10 [verified]; quotes and conclusions in 10. 10 2
D-R-TELEMETRIE Collector with exporters per backend; gen_ai.* attributes encapsulated in one place; telemetry never blocks the message path (queue, batch, fail-open, counter for dropped data); dashboards as files; SLOs with Sloth and burn-rate alerts. Vendor-neutral; unstable conventions stay replaceable in one place. ADR 0034, ADR 0035, signals.md 6
D-R-QUALITAET Dart locally with flutter analyze and custom_lint, publicly also SonarQube Cloud; one Sonar profile in the repository; actions pinned by SHA, zizmor and actionlint mandatory, no pull_request_target; Trivy only pinned, OSV-Scanner for dependencies; gitleaks via CLI; license gate. The local SonarQube Community Build does not analyze Dart; protection of the supply chain. ADR 0038, ADR 0039, ADR 0040, 23 7

4. Rulings of the lead

4.1 Deviations of the teams (L1 to L24)

ID Decision Reason Location
D-L1 One binary bothy with subcommands (serve, migrate, setup, config, import; W3 adds more); role via --role with api, worker or all. "server" in B1 means the server role. One image, one name. R-AR-01, 02 3.3, PLAN W3
D-L2 Health (live, ready) is on an internal listener outside the public API and is not in openapi.yaml; the public status page remains an API resource. Health is an operations contract; the public listener gets no path without login. 16 4.2, R-API-16, R-AZ-22, PLAN W2
D-L3 gateway, search and stats are separate modules. Current state: since the addendum, stats owns the table stats_minutes (D-V03). Own ports, dependencies and performance targets. R-AR-02, 02 4.2
D-L4 The source of the configuration values are the tables "Configuration values of this chapter"; the configuration reference is generated from them and checked in the gate. From M1 on, the schema in the code is the source. No name twice. One source per value. configuration.md 1, PLAN M1-08
D-L5 All repository content is English (D-W-94, owner, 2026-10-11). Before: descriptions in OpenAPI and AsyncAPI and runtime texts of the operations artifacts are English; planning documents German. Descriptions become comments in generated code, runtime texts are read by instance operators; since D-W-94 the same holds for every document. R-API-15, R-OB-09
D-L6 Token prefix bothy_; ADR 0044 lists it as a place with the working name. Secret scanners need a unique prefix. ADR 0061, ADR 0044
D-L7 Hermes adapter without a progress bubble: format_tool_event creates run steps; send_draft and the status fill the live box. Verified public override points instead of text parsing. R-AG-18, 09 17.3
D-L8 For runtimes with both modes, reported_billing_mode is missing; instead the adapter reports billing_modes_supported. The check per run is binding. The runner chooses the mode per run. 09 4.8, 10 8
D-L9 The typing indicator follows the run state; agents send no typing. Normative in the core contract. 04 12
D-L10 Two device keys: a DPoP key without confirmation per signature, a step-up key with confirmation. The Notification Service Extension must sign without a user interface. R-ID-04, R-CL-05
D-L11 Font Atkinson Hyperlegible, system font as a fallback. Unambiguous characters, easy to read, license OFL. typography.md
D-L12 Minimum versions Android API 26 and Windows 10 2004. Crash reports and Share Target require them. R-CL-12, 15 2
D-L13 Additional packages and files of teams E, F and H are adopted. Justified in the team rulings, section 5. R-CL-01, design/README.md, 23 3
D-L14 Own Postgres image with pgBackRest. WAL archiving needs the tool in the database container. R-OP-07
D-L15 Additional burn-rate level 3 as a ticket alert. Default of Sloth. ADR 0035, slos.md 3
D-L16 Image metadata of JPEG and PNG is removed losslessly from the original on upload (filter on segments and chunks); thumbnails are re-encoded. Privacy without loss of quality of the original. ADR 0047, R-FI-07, R-SE-06
D-L17 The portability test runs on fresh GitHub-hosted Linux runners. Current state: all jobs run on GitHub-hosted runners (D-W-92). A fresh Linux runner is the purpose of the portability test. ADR 0039
D-L18 The deny list rightly reports identities of the organization in commits. Before publication the public identity is fixed; older commits are rewritten or the history starts anew. Partly superseded by D-W-06: the owner has chosen the identity, rewriting the authors is no longer needed. White label of the product. public-switch.md A
D-L19 The target repository of the owner is a matter of the instance and is only in the operations repository; the product holds the placeholder OWNER. The recommendation of a neutral organization instead of a personal account is an owner question. Otherwise the account name would be in the Go module path. 02 6, public-switch.md A
D-L20 Dev isolation: one OS user alone is not enough; a VM or equivalent. A shared kernel and misconfiguration open the host. ADR 0046, R-DV-01
D-L21 Direct messages are invisible to owners; access only through the privacy or legal route with step-up and audit. Owner question with this default. Confidentiality between people. R-AZ-02, 07 SR6
D-L22 I1 without an owner exception: personal works only as long as the credential_holder is the only owner. Owners have trigger everywhere. ADR 0024, 10 3
D-L23 A revocation aborts running runs of the person concerned (fail closed), deviating from study C. Security before convenience. R-SY-11, 04 8.3
D-L24 Invocation token with EdDSA and JWKS instead of HMAC. The adapter cannot forge a token; keys can be rotated. R-SY-08, ADR 0023, ADR 0067

From the proposals of the teams, the lead determined ADRs 0047 to 0065; they are in the ADR index.

4.2 Further rulings of the lead without their own ADR

ID Decision Reason Location
D-LS-01 Automatic adoption of disablements from the identity provider (SCIM, Shared Signals) comes later with its own ADR. Until then the owner disables a person immediately with user.disable; a session lives at most until auth.idp_recheck_interval. 08 3.5, ADR 0019
D-LS-02 Platform attestation of the device keys later; today it is stored but not required (auth.require_key_attestation). Without attestation, hardware binding cannot be proven; it becomes mandatory only once the platforms supply it. 08 auth.require_key_attestation, ADR 0020
D-LS-03 An identity driver through the local interface of the network agent (WhoIs) is planned as a later driver, not in M1. A path without trust in headers; for the start the proxy header with loopback is enough. 08 2.4
D-LS-04 Checkpoints of the audit log are kept outside the database (audit.checkpoint_sink). Immutable storage (WORM) is a matter of operations; the operations documentation describes it: an S3 bucket with Object Lock in compliance mode in a separate account, retention at least retention.audit, until M7-22 a copy from the file system target by the owner (backup-restore.md 8). The hash chain alone does not detect a complete rewrite by the database administration. 19 SEC-AUD-2, ADR 0021
D-LS-05 Webhook as a third transport for agents. Adapters without a permanently running process. 09 4.2, 04 15.6
D-LS-06 Built-in incoming webhook formats only native and alertmanager. Little server code for foreign formats; everything else becomes an app. R-EX-08
D-LS-07 Reserved control commands. Unambiguous, translatable, never sent to the model. R-AG-13, 09 10.1
D-LS-08 An owner exception in I1 is rejected. Like D-L22. ADR 0024
D-LS-09 Icon set Phosphor. Location deviating from the list of the lead: the design documentation, not ADR 0030. Filled variant for states, distinctive look. icons.md
D-LS-10 Font according to D-L11. Location as for D-LS-09. Like D-L11. typography.md
D-LS-11 The switch to Zensical follows the criteria in ADR 0045. Like D-R-DOKU. ADR 0045
D-LS-12 Merge into main only by squash. GitHub does not sign the new commits of rebase merges; signed commits and a linear history go together only with squash. push-discipline.md
D-LS-13 A separate preview token per push registration; 08 is aligned with it. Two processes on the same refresh family trigger the theft detection. R-PN-07, 08 preview token
D-LS-14 An agent invocation with attachments is written only when all attachments are final. Agents never see unchecked bytes. R-SY-20, R-FI-09
D-LS-15 Search as a coordinator with read ports of the owning modules. Tables of other modules only through ports. R-SU-09, 02 4.3
D-LS-16 A backend policy per dev environment comes later; until then only organization. I1 is defined per agent. R-DV-05, R-DM-24
D-LS-17 Read-only channels (channels.read_only) as an addition to the core contract. Only a state on the channel can also keep owners from writing (read mirror of the import). R-DM-25, R-AZ-12
D-LS-18 Command-line tools log in as a device with platform = 'cli'; the general rules of 08 apply. Done: 08 section 6.4 specifies that a cli device has no step-up key and performs no step-up; actions with step-up go through the app or the web console (R-ID-17, ID-T29). A key in a tool signs without user confirmation; without attestation the server cannot tell it apart from a key in secure storage. No step-up is the safer choice, as with API tokens. 03 devices, 08 6.4
D-LS-19 Import through a source-neutral import interface of the server. The import tool runs outside the server (R-IM-01). 22 8, 06 applyImportBatch

5. Rulings of the teams

Every planning team reported its decisions beyond the basis. Most of them are R-IDs in the chapter today; the column "Location" leads there. Where the chapter changed the decision later, the row says so with "Current state:". Instance-specific rulings of the migration team are in the operations repository (D-IM-09).

01 Product

ID Decision Reason Location
D-PR-01 Four columns with a session column instead of a member list; no presence and no read receipts for people; no keyboard shortcut triggers an approval; questions from one's own runs notify even with "mute"; quiet hours make notifications silent; on mobile only emergency administration; accent only from the palette; link preview in the client later and only with opt-in. Reasons per ruling in the chapter. R-PR-01, R-PR-02, R-PR-03, R-PR-04, R-PR-05, R-PR-06, R-PR-07, R-PR-08, R-PR-09

02 Architecture

ID Decision Reason Location
D-AR-01 One binary bothy with subcommands, roles via --role, default all. One image for all roles; splitting them costs one line of configuration. R-AR-01
D-AR-02 Modules gateway, search, stats in addition to the modules from 03. Current state: stats owns stats_minutes (D-V03). Own ports and performance targets. R-AR-02
D-AR-03 The port of a module is the root package internal/<module> with its own DTOs. The domain stays changeable internally; a later process boundary needs no rework of the callers. R-AR-03
D-AR-04 Module dependencies form a graph without cycles; the reverse direction only through source ports or in-tx events. Separability and no hidden cycles. R-AR-04
D-AR-05 Cross-cutting ports in internal/kernel. As edges between modules they would create cycles. R-AR-05
D-AR-06 Unit of Work with a fixed order of phases before the commit. Enforces the lock order from 03 in one central place. R-AR-06
D-AR-07 No external calls inside transactions. Short serialized commit phase, safe retry. R-AR-07
D-AR-08 Push and outgoing webhooks are produced in the worker from the event log through consumers with a cursor. Producers know no consumers; the log already is the outbox. R-AR-08, ADR 0051
D-AR-09 River only in platform/jobs, own schema, no Pro features, job arguments only IDs. Dependency in version 0.x in one place; no further storage location for content. R-AR-09
D-AR-10 Ephemeral events between nodes only in mode multi, via NOTIFY with IDs. NOTIFY stays a wake-up signal without content. R-AR-10
D-AR-11 Gateway sessions only in the process; resume on another node ends with 4006. No shared session store; identify with a cursor catches up just as well. R-AR-11
D-AR-12 Rate limits in memory per process. No additional service; limits are protection, not billing. R-AR-12
D-AR-13 Health on the internal listener outside the public API. Current state: metrics only via OTLP, no metrics endpoint (ADR 0034). For the orchestrator without login, but not public. R-AR-13, D-L2
D-AR-14 Proof per request that authorize ran with the action of the route. Holds D-B7 at runtime. R-AR-14
D-AR-15 Configuration (instance operator, restart) separate from settings (owner, database, immediate); unknown keys abort the start. Clear responsibility; typos show up at start. R-AR-15, ADR 0011
D-AR-16 Migrations only via bothy migrate, never at start; the server checks the schema version. No simultaneous migration by several nodes, no restart loop. R-AR-16
D-AR-17 Fixed order at shutdown; WebSockets close with 4013. http.Server.Shutdown does not cover hijacked connections. R-AR-17
D-AR-18 In the request path, only a retry of the whole Unit of Work on deadlock and lock timeout. Other retries belong in jobs or clients. R-AR-18
D-AR-19 Architecture gate with go-arch-lint, configuration generated from the module register. One source for module boundaries. R-AR-19, ADR 0050
D-AR-20 Performance targets as named values, one load test or benchmark per target. No number without a test. R-AR-20, ADR 0063
D-AR-21 C4 diagrams as Mermaid flowcharts, containers and components with ELK layout. The C4 mode of Mermaid overlapped labels; ELK renders cleanly. architecture/README.md
D-AR-22 ERDs by module group; every table exactly once; complete columns only in 03. Readability; no second truth. architecture/README.md

06 API

ID Decision Reason Location
D-API-01 Path prefix /api/v1 for all surfaces, including gateway and incoming webhooks; server URL in openapi.yaml relative. One prefix; every self-hosted instance has its own host. R-API-01
D-API-02 No JSON null on the wire; removal through dedicated fields or modes. The Dart generator knows no null type; no three-valued logic in PATCH. R-API-03, ADR 0053
D-API-03 Conservative schemas (oneOf only with a discriminator, no anyOf, no type arrays) and one schema source in openapi.yaml. Compatibility with oapi-codegen and AsyncAPI parsers (own probes). R-API-05, R-API-06
D-API-04 Problem type relative with the full path, plus code. Self-hosted software has no shared host; RFC 9457 allows it. R-API-07
D-API-05 If-Match with 412 instead of expected_version with 409. HTTP standard (RFC 9110). R-API-08
D-API-06 Idempotency-Key on POST and PATCH with defined exceptions; only final responses are stored. A retry after step-up, renewal and throttling must work. R-API-09, R-API-24
D-API-07 Separate endpoints per login method; OIDC with the server as a confidential client and binding to the app. No unions; intercepted redirects are worthless. R-API-12
D-API-08 Uploads as a session made of parts. One client path for both storage drivers, resumption. R-API-13
D-API-09 Webhooks always with signature v1a, HMAC v1 can be enabled per subscription. Asymmetric is preferred; common libraries know only HMAC. R-API-14, ADR 0016
D-API-10 All repository content is English (D-W-94, owner, 2026-10-11); this includes the descriptions in the YAML files. Before: descriptions in the YAML files English. They become comments in the generated code. R-API-15
D-API-11 Health in the public API with action health.read. Superseded by D-L2: health lives on the internal listener. Draft of the team before the ruling. R-API-16
D-API-12 Run lifecycle via startRun, resumeRun, awaitRunInput, finishRun with a target state, and cancelRun. One action per step; no confusion between canceling and canceled. R-API-17
D-API-13 Generated code is checked in, the gate checks drift. Reviews see the effect; builds without the generator. R-API-18
D-API-14 Neutral titles of the contracts without product names. The name is configuration (ADR 0044). R-API-19
D-API-15 Scopes act like grants: without a grant 404, with a grant but without the scope 403 scope_missing. Same behavior as for people. R-API-21
D-API-16 One event envelope for gateway, REST catch-up and webhooks; delivery fields never in the log. One form for all channels. ADR 0052, R-SY-17
D-API-17 New limits as named configuration values. Current state: the names are unified by W1 and D-K-X3. No fixed numbers in prose. 06 20, PLAN W1

07 Permissions

ID Decision Reason Location
D-RE-01 Permission cache per principal with a global version counter and valid_until; every REST request reads the current version. No materialized view. One truth; expiry by time without a write; revocation takes effect across processes. R-RE-01, R-RE-02
D-RE-02 Tool class per agent (chat, scoped, host) with configured and reported tool class; class host is triggered only by an owner or by the person in their ready dev environment (SR2). Check at invocation, because readiness and groups change without a permission transaction. R-RE-05, 07 SR2
D-RE-03 Acting-for binding (SR9): an agent in a run has at most the permissions of the triggering person. Closes the confused deputy via the server API. R-RE-06
D-RE-04 Read horizon as a hint, not as a block. A block would make channel.create fail unexpectedly. R-RE-07
D-RE-05 The effective operator implicitly has write. No forgotten leftover when the operator changes. R-RE-04, R-AZ-13
D-RE-06 Agents and apps receive only upload as a capability; only the owner gives grants to them; manage revokes only what it could have created itself. No self-granting; grants to agents are data flows to processors. R-RE-03, R-RE-08
D-RE-07 Owner permissions only in a session, never via a personal API token (SR5). Current state: the exception is the check without a request, in which the owner status comes from the current owner list without a session (R-AZ-29, D-W-104). A stolen token without a device must never open the instance. R-AZ-14, 07 SR5
D-RE-08 The last active owner can be neither removed nor disabled (SR8). The instance must not become orphaned. 07 SR8

08 Identity

ID Decision Reason Location
D-ID-01 The server is the OIDC relying party and the authorization service of the apps; PKCE on both legs; mapping only via (issuer, subject) or an enrollment code, never via email. No provider tokens on devices; email is mutable. R-ID-01, R-ID-02
D-ID-02 Two keys per native device. Same as D-L10. R-ID-04
D-ID-03 Extensions get child sessions with their own refresh family. Otherwise simultaneous renewal would be detected as theft. R-ID-05
D-ID-04 Admin console following the BFF pattern with a cookie; after login only the own profile until step-up. No tokens in the browser; owners read almost everything through the console. R-ID-06
D-ID-05 The confirmation code for approving new devices is typed in. No blind confirmation of someone else's device. R-ID-08
D-ID-06 No recovery codes; recovery via a new enrollment code or a command on the host. One permanent secret fewer. R-ID-09

09 Agents

ID Decision Reason Location
D-AG-01 The effective adapter feature is the intersection of what the owner allows and what the adapter reports. The owner can switch features off, but cannot force anything the adapter cannot do. R-AG-01
D-AG-02 Threads keep their fixed operator; main threads use the current one. A frozen operator would mute channels after a change. R-AG-02, R-DM-21
D-AG-03 The trigger decision is deterministic: several mentioned agents are rejected visibly; without trigger there is an error message only on an explicit mention; another agent during a run is rejected. At most one active run per thread; the server does not guess. R-AG-03, R-AG-05, R-AZ-20
D-AG-04 Guest agents run without a session and are never personal. One session per thread; a personal guest could be triggered in channels that I1 does not check. R-AG-04, R-AG-22, R-AZ-19
D-AG-05 The server builds the context of a run with a role and a trust mark per message. Only authz knows the permissions of the authors; protection against prompt injection. R-AG-06, ADR 0054
D-AG-06 The adapter starts the runtime only after run.start has been accepted. At most one execution per invocation. R-AG-07
D-AG-07 run.start also applies to waiting_input -> running. An answer to a question is a trigger and needs I2 and I3. R-AG-08
D-AG-08 trigger_state: received at the server, queued at the adapter, new value cancelled. People see whether the agent has the request; a stop is not an error. R-AG-09
D-AG-09 After a stop, open follow-ups are discarded; after an error they are processed later. Stop means "nothing more". R-AG-10
D-AG-10 One-time rule per message; "Always allow" only for owners. An approval card has exactly one decision. R-AG-11, R-AG-12
D-AG-11 Built-in control commands with reserved names; no keyword in running text. Unambiguous, translatable, never sent to the model. R-AG-13
D-AG-12 Control commands run without a run; model commands run like a message and are rejected during a run. Clear separation of costs and live box. 09 10.4
D-AG-13 Autocomplete never calls a model and answers with an empty result after a deadline. Typing costs nothing and never hangs. R-AG-14
D-AG-14 Auto title with detection of placeholders at creation. Real titles are never overwritten. R-AG-15
D-AG-15 Voice input: the agent transcribes, otherwise a server port, otherwise a visible rejection. No silent loss of a voice message. R-AG-16
D-AG-16 Runs that have not started fail after run.queued_max_age with agent_unavailable. A job executed surprisingly late is worse than a visible rejection. 09 run.queued_max_age
D-AG-17 Hermes adapter: progress through public override points. Same as D-L7. R-AG-18
D-AG-18 Hermes adapter: three access layers (server, adapter with an allow-list, Hermes list only for personal). No restart on every grant. R-AG-19
D-AG-19 The platform key of the Hermes adapter never changes after commissioning. Hermes builds the session key from the platform name; a change separates every thread from its session. 09 17.4
D-AG-20 Runner: reported mode via a preflight check in the run environment; apiKeySource only as a cross-check. apiKeySource alone does not distinguish a subscription from other paths. R-AG-20
D-AG-21 Runner: --permission-prompts none. Unattended turns never hang on a permission prompt. R-AG-21
D-AG-22 All repository content is English (D-W-94, owner, 2026-10-11), including the planning-state READMEs under plugins/ and sdk/ and the package READMEs for the registries. Before: planning-state READMEs under plugins/ and sdk/ German, package READMEs for the registries English with the first release. Owner, 2026-10-11: the instruction was English (D-W-94). Earlier reason: planning documents were German. plugins/README.md, sdk/README.md

10 Backend policy

ID Decision Reason Location
D-BR-01 Cost booking: computed from the price table, which takes precedence over the amount from the adapter, otherwise unpriced; no currency conversion; in mode personal no amounts in the interface (switch billing.show_personal_estimates). Verifiable and the same for all adapters; personal amounts are not an invoice. 10 12.1, 10 12.2
D-BR-02 Cost reconciliation runs as a separate app outside the server, off by default; the admin key lives only there. A break-in into the chat server must not reach the admin access; the server never calls models. 10 12.4, ADR 0055
D-BR-03 After switching, new deliveries carry the new configured mode; adapters with the old reported mode fail visibly; set_all to personal only with an explicit list. No hidden queue, no accidental global switch back. 10 9.2
D-BR-04 Guided flow "switch first, then grant" as one composite request in one transaction; the way back to personal is not automated. One confirmed action; revoking permissions automatically would have too many consequences. 10 11, R-AZ-17

11 Extensions

ID Decision Reason Location
D-EX-01 Apps never get trigger and manage; manifests are uploaded, never fetched by URL; unknown fields are errors; the consent shows target hosts and data outflow. No granting of permissions by integrations, no SSRF, no typos that take effect silently. R-EX-01, R-EX-02, R-EX-03, R-EX-04
D-EX-02 Outgoing webhooks per Standard Webhooks v1a, strictly ordered per subscription, from the event log; SSRF protection. Incoming only native and alertmanager, mentions are not resolved. Verification with a public key, no silent loss, no notification abuse. R-EX-06, R-EX-07, R-EX-08, R-EX-09

12 Files

ID Decision Reason Location
D-FI-01 Upload always through a session with an upfront declaration, in parts of fixed size. Early rejection, resumable uploads. R-FI-01
D-FI-02 Mode direct is the default, presigned only with S3 and only after proof of the length binding. Private networks often cannot reach the storage. R-FI-02
D-FI-03 Retrieval only through the server, no presigned GET; short-lived tickets for players. authorize at retrieval time; revocation takes effect immediately. R-FI-03, ADR 0048
D-FI-04 One read pass for checksum, size, type and scan. Large files are read only once. R-FI-04
D-FI-05 Scan fails closed; AlertExceedsMax yes is mandatory. Otherwise unchecked content silently counts as clean. R-FI-05, R-FI-06
D-FI-06 Images are re-encoded in pure Go, orientation and ICC profile are kept. Current state: the original is cleaned losslessly per D-L16, only previews are re-encoded. Location data never leaves the sender. R-FI-07, ADR 0047
D-FI-07 A file is attached to at most one message. Deletion without reference counting. R-FI-08
D-FI-08 Agent invocation with attachments only when all attachments are final. Agents never get unchecked bytes. R-FI-09
D-FI-09 Transcript by the agent by default. No additional processor without a decision by the owner. R-FI-10
D-FI-10 No deduplication. No side channel about the existence of a file; safe deletion. R-FI-11
D-FI-11 The object storage port knows only Put, Get with range, Head, Delete, Multipart and presigned upload parts; objects are never overwritten. The default checksums of some SDK functions break several S3-compatible services; the server knows every key from the database. 12 4.1, 12 4.4

13 Push and notifications

ID Decision Reason Location
D-PN-01 Only the server decides about notifications, also for desktop clients. One place for all rules. R-PN-01
D-PN-02 Notifications of a run are bundled at the end of the run; input_needed ignores mute, not the quiet hours. What matters is "finished"; own runs should not hang. R-PN-02, R-PN-03
D-PN-03 Automatic following of threads when writing. Answers to one's own questions arrive. R-PN-04
D-PN-04 Quiet hours make notifications silent instead of muting them; bundling with the first notification immediately and the rest at the end of the window. Complete notification center without waking anyone. R-PN-05, R-PN-06
D-PN-05 Separate preview token per push registration. No mutual lockout through the refresh family. R-PN-07
D-PN-06 No FCM collapse_key; the app replaces notifications by tag. FCM keeps only a few collapse keys per device. R-PN-08
D-PN-07 Notification jobs are created in the triggering transaction. Superseded by ADR 0051 (D-AR-08): push is produced in the worker consumer push from the event log; the job catalogue in 02 follows this. 13 section 12 and R-PN-09 are aligned. Originally: exactly one notification job per commit without its own cursor. The lead rejected inserting jobs in the write path, because producers should not know their consumers. R-PN-09, ADR 0051, 02 12.3, 13 12
D-PN-08 A central push relay comes later; every instance uses its own keys. No collection point for metadata. R-PN-10
ID Decision Reason Location
D-SU-01 One tsvector with language stems and simple words; language from the author; query as OR over the instance languages plus simple. One index; identifiers and foreign languages stay findable. R-SU-01, R-SU-02, R-SU-03
D-SU-02 Candidate limit before the ranking, one result type per query, no total counts. Bounded cost; counts would be expensive and a leak risk. R-SU-04, R-SU-05
D-SU-03 Highlighting with random markers, output as segments, never HTML. ts_headline does not guarantee safe HTML output. R-SU-06
D-SU-04 File text extraction in pure Go, Tika optional as a separate container. No JVM requirement; parser risk outside the server. R-SU-07
D-SU-05 Search as a coordinator with read ports of the owning modules. Tables of other modules only through ports. R-SU-09
D-SU-06 Run steps and the live box are not searchable. Tool output contains secrets and noise. R-SU-08

15 Client architecture

ID Decision Reason Location
D-CL-01 Additional packages auth, device_key, admin_ui; Pub workspace without Melos. Shared logic for app, console and native extensions; one tool fewer. R-CL-01, R-CL-02
D-CL-02 Riverpod and drift on encrypted SQLite; the interface reads only from the local database and short-lived stores; outbox as a table. Reactive queries, transactions, generated migration tests, encryption. R-CL-03, R-CL-08, ADR 0029
D-CL-03 ES256 for all device keys; two keys per device. Secure Enclave and StrongBox support only P-256. R-CL-04, R-CL-05
D-CL-04 Notification Service Extension, Share Extension and Android push service native without the Flutter engine. Time and memory limits of the extensions. R-CL-06, ADR 0056
D-CL-05 Refresh only under a cross-process lock, the new token is stored before the lock is released. Current state: in addition a separate family per extension, the NSE never renews. Reuse of a refresh token revokes the family. R-CL-07
D-CL-06 Minimum versions Android API 26 and Windows 10 2004. Same as D-L12. R-CL-12
D-CL-07 Auto-update via Sparkle natively and MSIX with .appinstaller. The Flutter package for updates is outdated. R-CL-09, ADR 0037
D-CL-08 Client metrics via a dedicated server endpoint to the collector; crash reports only with the instance DSN. No official Dart OTel SDK; one authorized endpoint instead of an open collector. R-CL-10, ADR 0057
D-CL-09 Golden tests with alchemist, patrol for device E2E, integration_test for Windows and a fake server. System dialogs and notifications are core paths; patrol has no Windows support. R-CL-11, 15 20

Design system

ID Decision Reason Location
D-DS-01 Brand color spruce (green-tinted petrol), gorse (yellow) as the signal color for everything that waits for a person; neutrals with a cool tint; palette named after a highland landscape. Accepted by the owner on the preview images. Hardly taken in the field of team chats; works in light and dark; guards in the generator keep the ban list measurable. design/README.md
D-DS-02 In both themes the sidebar is a brand surface with its own roles (surface.sidebar*, text.on-sidebar*). Recognition without decoration; separates navigation from content. palette.md
D-DS-03 Typeface Atkinson Hyperlegible Next and Mono. Same as D-L11. typography.md
D-DS-04 Phosphor icons from a pinned npm package via semantic names, not via a Flutter package. Filled variant for states; the Flutter package is not maintained. icons.md
D-DS-05 Token format DTCG with a resolver and three modifiers (theme, density, motion). Open standard; no copies per theme. ADR 0030, design/README.md
D-DS-06 Generator in the Python standard library, no Style Dictionary. Gate without network and without a further supply chain. ADR 0030, tools/design-tokens
D-DS-07 Contrast pairings and palette guards in one rule file, checked per theme. AA and parts of the ban list become tests instead of intent. contrast.md
D-DS-08 Numbers of the tokens appear only in generated pages; handwritten pages name token names. No fixed numbers in docs that no test holds. tokens.md
D-DS-09 Accent per instance, space colors and tag colors only as palette keys, never as hex. Only checked combinations pass AA. R-PR-08, ADR 0044
D-DS-10 People round, agents as a rounded square; live box with a spine in the color of the state. Distinction without color; state at a glance. components.md

16 Operations

ID Decision Reason Location
D-OP-01 Two listeners; the internal one never trusts headers. Health and internal webhooks need neither a public path nor trust in headers. R-OP-01
D-OP-02 Header identity only with the public listener on loopback, otherwise the provider stays off and /readyz reports it. Enforces the condition from D-B9 instead of assuming it. R-OP-02
D-OP-03 Tailscale as a sidecar with a shared network namespace in kernel networking, tailnet policy only port 443, Funnel forbidden. Only this way does the server demonstrably listen only on loopback. R-OP-03, tailscale.md
D-OP-04 Unknown configuration keys abort the start. A typo never silently sets a default. R-OP-04
D-OP-05 Secrets as references (secret_ref) and as files per service, never as environment variables. Child processes and docker inspect show no values. R-OP-05, ADR 0011
D-OP-06 Escrow of the secrets to an owner key is mandatory. Without the pepper, the signing key and the backup key a restore is worthless. 16 5.4
D-OP-07 pgBackRest for physical backup, pg_dump for portability, restic for objects; off-site append-only. Proven tools instead of a custom build; protection against ransomware. R-OP-06, R-OP-07, R-OP-08, R-OP-09, ADR 0058
D-OP-08 Automatic restore test. A backup is only proven once it has been restored. R-OP-10
D-OP-09 Expand/contract with a way back of exactly one version; migrate requires a fresh backup. A way back without rolling back the database. R-OP-12, R-OP-13
D-OP-10 Feature flags with a kind and a cleanup date, never for security boundaries. No dead flags, no hidden security switches. R-OP-14, ADR 0064
D-OP-11 Status page from internal checks. Works without the optional observability stack. R-OP-15
D-OP-12 Environments dev, staging, prod; default prod; production data never in dev or staging. Safe default; a copy would be a data protection incident. R-OP-16
D-OP-13 Grafana admin password via the $__file{...} resolution. Checked locally: the variant with the file suffix did not work with the binary. backends.md
D-OP-14 Auxiliary services with pinned image versions as Compose variables; Renovate keeps them current. Repeatable installation. configuration.md 9, ADR 0041
D-OP-15 Operations diagrams next to their documents. Own directories, no paths that belong to other areas. operations/diagrams, observability/diagrams

17 Observability

ID Decision Reason Location
D-OB-01 Fan-out in the collector via forward connectors, every backend an overlay. Several backends without conflicts when merging. R-OB-01, ADR 0034
D-OB-02 Data protection twice (at the source and in the collector) with a canary test. A code error alone does not bring content to the outside. R-OB-02
D-OB-03 People only as an HMAC pseudonym in spans and logs, never on metrics; in-app statistics without values per person. Debugging yes, performance monitoring of individuals no. R-OB-03
D-OB-04 Tail sampling in the collector; the SDK records everything; a client cannot force sampling. Errors are never lost by chance. R-OB-04, R-OB-05
D-OB-05 Histograms with fixed boundaries, every SLO threshold is a boundary. An SLI from buckets is exact only at boundaries. R-OB-06, ADR 0035
D-OB-06 correlation_id is the trace ID of the triggering request. Connects events, audit and traces without a second ID. R-OB-07
D-OB-07 All repository content is English (D-W-94, owner, 2026-10-11), including the runtime texts in operations artifacts. Before: runtime texts in operations artifacts English. Instance operators worldwide. R-OB-09

18 Release

ID Decision Reason Location
D-RL-01 Version and changelog per component, tags <component>/vX.Y.Z, no latest; apps: the same build moves from beta to stable; signature, SBOM and attestation. Independent releases; what ships is what was tested. R-RL-01, R-RL-02, R-RL-03, R-RL-04, ADR 0036
D-RL-02 Apps are white label; each organization builds and signs them itself with distribution.yaml. Push keys belong to the instance; no central push forwarding. R-RL-05, ADR 0037

19 Security

ID Decision Reason Location
D-SE-01 Signed checkpoints of the audit log outside the database, canonical form according to RFC 8785. The hash chain alone does not detect a complete rewrite. 19 SEC-AUD-2, ADR 0021
D-SE-02 Clients do not load third-party resources from content. No leak through image URLs, no tracking of readers. R-SE-05
D-SE-03 One egress client with an IP check at connection setup; a lint forbids other HTTP clients. SSRF protection in one place, also against DNS rebinding. R-SE-04

20 Privacy

ID Decision Reason Location
D-DP-01 IP addresses in the audit only as HMAC with destroyable keys. Investigable for a period, afterwards unlinkable without breaking the chain. R-DP-03
D-DP-02 Defaults: no default retention period for content, but a required reason in the configuration (retention.content_none_reason, otherwise a warning; changed by D-W-22); account deletion pseudonymizes; deletion journal outside the database; no statistics per person; read state never for others. The controller sets the retention periods; a restore does not bring back deleted data; codetermination-friendly. R-DP-01, R-DP-02, R-DP-04, R-DP-06

21 Dev isolation

ID Decision Reason Location
D-DV-01 Isolation only with a hypervisor boundary; a Linux VM per person as the default; readiness as a record with checks, red team and owner approval; other people's environments only with an organization key in this milestone. Only the VM holds against injection and misconfiguration; I1 is defined per agent. R-DV-01, R-DV-02, R-DV-03, R-DV-05, ADR 0046

22 Import and migration

ID Decision Reason Location
D-IM-01 The import is a separate process that uses an import interface; Discord only with GET; the server never talks to Discord. No server-side loading of third-party URLs, no Discord token in the server; a dry run provably writes nothing. R-IM-01, ADR 0043
D-IM-02 The dry run is the default and writes nowhere; contradictory options end before any call. A forgotten switch must not create anything. R-IM-02
D-IM-03 A separate Discord bot account with read permissions only, never the token of a production agent. Rate limits per bot; minimal permissions. R-IM-03
D-IM-04 Mirror channels are read-only, even for owners. A channel never has two authoritative locations. R-IM-07, R-DM-25
D-IM-05 Imported threads carry the planned operator. Otherwise they would remain without an agent after the switchover. R-IM-08
D-IM-06 Earlier sessions are bound exactly once on the first trigger, fail open. A failure must never silence the person. R-IM-10
D-IM-07 Import without notifications and without an unread backlog. Current state: through threads.unread_floor_seq instead of read states per reader (D-K-I4). Old content should not nudge anyone. R-IM-05, R-SY-24
D-IM-08 Reactions and later edits are not carried over. Cost per emoji and message; the settle rule makes late edits rare. R-IM-09
D-IM-09 Rulings of this instance (installation, agents, pilot) and the scope of the private deny list are not in the product. White label; the product only knows the mechanism. ADR 0002, ADR 0042

23 Quality and repository

ID Decision Reason Location
D-QA-01 make check consists of exactly gates, denylist and sonar; the CI jobs call exactly these targets. Parity between local and CI by construction. R-QA-01
D-QA-02 Superseded by D-W-92: every job runs on a fixed GitHub-hosted label; public-only jobs (CodeQL, Scorecard, Pages) with their own condition; a tool enforces both. The repository is public from the first push; a switch in runs-on no longer exists. R-QA-03, R-QA-04, ADR 0039
D-QA-03 Merge only by squash. Same as D-LS-12. push-discipline.md
D-QA-04 Semgrep only with the project's own rules. Registry rules are not version-bound and have their own license. R-QA-05
D-QA-05 SonarQube only for product code; ruff, mypy, bandit and pytest check tool code. Otherwise a SonarQube server would be required for every planning change. R-QA-06
D-QA-06 Shared quality gate and profile "Bothy way" as files, with an alignment tool for local and cloud. Same conditions in both places. ADR 0040, sonarqube-local.md
D-QA-07 SonarScanner as an npm package. The same scanner locally and in CI, without Java and Docker. toolchain.md
D-QA-08 Release binaries of the tools with a checksum lock per platform. Protection against rewritten release assets. toolchain.md
D-QA-09 Trivy held at a version proven safe, only for images and IaC; OSV-Scanner checks dependencies. Supply chain incident at Trivy; newer versions not proven safe. toolchain.md
D-QA-10 Link check as a gate only offline. Third-party servers make gates non-reproducible. R-QA-09
D-QA-11 Docs site pinned, GitHub-style anchors, build without --quiet. --quiet suppresses warnings in such a way that --strict no longer fails. ADR 0045
D-QA-12 No .sonarcloud.properties; analysis only through CI. Both ways at the same time interfere with each other. R-QA-10
D-QA-13 The deny-list scan checks all commits of HEAD without a remote; identities only against the private list; the output masks matches. The first push is the most dangerous one; CI logs must not reveal the list. R-QA-08, ADR 0042
D-QA-14 DCO check from the first remote on. New contributions need proof of origin. R-QA-07
D-QA-15 gitleaks over all commits reachable from HEAD, over all refs before switching to public; narrowly scoped exceptions for example values; own rules with tests. Example values need a machine-readable marking. gates.md
D-QA-16 Explicit switches for contributions from forks, visible in the output. Forks have neither the private list nor a SonarQube server. R-QA-02
D-QA-17 Server images are tagged only after the scan of the digest. A vulnerable image never gets a version tag. release.yml, toolchain.md
D-QA-18 Attestations only in the public state. On the repository's plan, attestations exist only when it is public; keyless signatures would expose a private repo. R-RL-04, ADR 0062
D-QA-19 Known findings in development tools that are not shipped are ignored with a reason and an expiry date. Not exploitable in deployment; the expiry date forces a new check. osv-scanner.toml
D-QA-20 mermaid-cli pinned to major version 11. All diagrams were checked with it; a change comes as a Renovate PR. toolchain.md
D-QA-21 The ADR index is generated and checked in make docs. The index never goes stale unnoticed. ADR 0001, adr/README.md
D-QA-22 Component contract: the root Makefile finds components by their manifests; release components and tags are in one place. One place for the component list; missing parts fail instead of being skipped silently. 23 3, components.py
D-QA-23 CodeRabbit with data minimization; the CLI only with an explicit switch. The CLI sends the diff to the service. push-discipline.md
D-QA-24 List of allowed licenses; LGPL and GPL in statically linked parts only with an ADR. Compatible with Apache-2.0. licenses.md
D-QA-25 Guard tests keep the numbers of the table in 23 and the versions in toolchain.md equal to the configuration and the pins. No fixed numbers in docs that no test holds. test_config_values.py, test_toolchain_doc.py

6. Core contract

6.1 Decisions of the core report

The decisions of the core contract beyond the basis are recorded as R-IDs in chapters 00, 03, 04 and 05. Later numbers were added with the integration, the gaps and the addendum.

Chapter Decisions Location
03 Data model R-DM-01 ff. 03 8
04 Sync protocol R-SY-01 ff. 04 17
05 Actions R-AZ-01 ff. 05 8

6.2 Open items of the core report

ID Decision Reason Location
D-KB-01 auth.dpop_nonce_ttl is defined in 08. One name, one chapter (D-L4). 08 18
D-KB-02 The periods of the retention classes are defined in 20; 03 only names the classes. Periods are a privacy decision. 20 4.1, 03 3
D-KB-03 Language selection and ranking of the search vectors are defined in 14. Search owns this subject. 14 3
D-KB-04 A central configuration reference is generated from the chapter tables. Same as D-L4. configuration.md 4
D-KB-05 Device keys only with ES256 (and EdDSA for server keys), until a Windows spike proves whether Windows Hello delivers ES256. Not proven; another algorithm family without a need would be attack surface. 03 device_keys, D-K-C12
D-KB-06 ADRs 0047 to 0065 from the list of the lead exist. During the integration they were only referenced. ADR index

6.3 Requests from the teams to the core contract

Every request from a team appears here exactly once with decision, reason and location. "adopted" means: changed in the core contract and carried over into the dependent documents. "same as A7" refers to the row D-K-A7.

Team A (architecture)

ID Request Decision Reason Location
D-K-A1 Table event_consumer_cursors for worker consumers adopted Push and webhooks need durable progress without inserting jobs in the write path (ADR 0051) 03 4.10, R-DM-27; 04 7.6; 02 4.2; ERD 6
D-K-A2 Name the River tables as technical tables adopted Complete list of the tables, owner platform/jobs 03 4.13; 02 4.2
D-K-A3 Fencing column on agent_consumer_cursors adopted 4015 must be enforceable across several api nodes 03 holder_session_id with a conditional UPDATE; 04 15.1
D-K-A4 Catch-up filter for new channels in visible spaces adopted Otherwise channels that are created after the cache is built are missing 04 7.2 ($visible_spaces, $fully_visible_spaces, $instance_wide), 04 8.1 (structure events in log order)
D-K-A5 Expiry of grants without access.changed adopted Clients must learn of a revocation also on expiry Job authz.expiry_sweep (02 12.3), system actions grant.expire, group.member.expire (05 4.4), event access.expiring, expiry_reminded_at (03)
D-K-A6 Moving a thread: copies of channel_id, files adopted. Current state: moving also changes message_attachments.channel_id (R-DM-43). Copies must not diverge 03 threads (moving changes messages, read_states, drafts), files.scope_channel_id applies only until attaching; files follow the message
D-K-A7 Path prefix /v1 versus /api/v1 adopted: /api/v1 everywhere One surface, one prefix 04 (all paths, wss://<host>/api/v1/gateway), 06, 08, 09, 12, 13, 15, docs, SLO and dashboard files
D-K-A8 Machine-readable action catalogue adopted as a direction, implementation later Remove duplicate lists only once a generator exists 05 rule 3 refers to ADR 0049 (list of the lead); until then alignment in the docs gate; redocly.yaml generated from 05
D-K-A9 Action for health endpoints rejected Ruling L2: health on the internal listener outside the API 05 R-AZ-22, 06 R-API-16, openapi.yaml without /health/*
D-K-A10 Modules gateway, search, stats in 03 adopted Complete module list (L3) 03 module table (without tables), 02 4.2
D-K-A11 agent.read for the agent itself adopted The adapter needs changes to the configured values 05 4.10 and section 6 (agent.updated reaches the agent)
D-K-A12 Unique anchor from run to message adopted Otherwise the ERD and the rule do not match 03 messages: partial index UNIQUE (run_id) WHERE run_id IS NOT NULL
D-K-A13 Ephemeral events with several api nodes adopted Typing and the live box must reach all nodes 03 6.6 (NOTIFY for ephemeral events in mode multi), 04 12 item 3

Team B (API)

ID Request Decision Reason Location
D-K-B1 New actions health.read, instance.settings.read, identity.read, self.read, agent.enable, file.transcript.submit, command.autocomplete.respond, webhook_in.read, local_credential.set adopted except health.read health.read is dropped according to L2 05 4.2, 4.3, 4.8, 4.10, 4.11; redocly.yaml without the block "proposed"
D-K-B2 Paths without /api in 04 adopted same as A7 same as A7
D-K-B3 No JSON null in the envelope adopted dart-dio has no null type 04 2.2, 03 principle 13 (ADR 0053)
D-K-B4 Invocation token and redelivered as delivery fields adopted Tokens are issued anew for each delivery 04 2.2 and 15.2 (ADR 0052), never in the log
D-K-B5 412 with If-Match instead of 409 with expected_version adopted HTTP standard (RFC 9110) 03 principle 10, 04 13.3, 06
D-K-B6 Repeating run.start with the same token adopted Lost responses must not leave a run orphaned 03 runs.started_jti, 04 15.3
D-K-B7 Transition waiting_input -> running adopted Questions need a continuation 04 15.3 (resumeRun under run.start), 05
D-K-B8 Autocomplete to the agent adopted Agents and apps own commands 04 15.7, 05 sections 6 and 7, op command.autocomplete_result, REST response
D-K-B9 Reason for trigger_state = failed adopted as trigger_state_reason (also for cancelled) Merged with D1 03 messages, 06, openapi.yaml
D-K-B10 Password with TOTP and enrollment codes adopted Baseline R-Passkeys requires the fallback 03 local_credentials, enrollment_codes, auth_sessions.auth_method
D-K-B11 Grace period for a refresh retry adopted A crash between rotation and saving must not log the person out 03 refresh_token_families.rotated_at, auth.refresh_reuse_grace (03 9), only with the same DPoP key
D-K-B12 Personal events for apps, scopes for metadata adopted Apps own commands and components 05 2.6 and 6
D-K-B13 agent.read for the agent itself adopted same as A11 same as A11
D-K-B14 Pending device adopted: tokens, but 403 device_pending except for the self lookup The app can wait without a new login 05 2.5, R-AZ-16, 07 4.1, 08, gateway 4016
D-K-B15 HMAC signature v1 per subscription adopted Common libraries only know HMAC 03 webhook_subscriptions.hmac_enabled, hmac_generation, hmac_rotated_at; webhooks.md
D-K-B16 Resource of template.manage adopted Personal templates have no channel 05 4.7: user for personal templates, channel for channel templates

Team C (permissions, identity, security, privacy)

ID Request Decision Reason Location
D-K-C1 Check on capabilities for agents and apps adopted No bot needs more than upload 03 grants (capabilities <@ ARRAY['upload']), R-DM-20
D-K-C2 Write level of the operator adopted: operator rule The operator must be able to write without its own grant 05 2.3 and rule 1, R-AZ-13; 07 2.9
D-K-C3 Tool class adopted Guard rule SR2 needs typed data 03 agents.tool_class, reported_tool_class; 05 agent.tool_class.set, claim tool_class; openapi.yaml setAgentToolClass
D-K-C4 Dev isolation adopted as a milestone Isolation is a separate condition next to project readiness; the routes come with the milestone 03 dev_environments (milestone), agent_sessions.dev_environment_id, runs.dev_environment_id; 05 4.13; claim dev_environment_id
D-K-C5 SR8 also on user.disable adopted The last owner must not be disabled 03 owners (invariant), 07 SR8
D-K-C6 Limit grant.revoke, grants to agents and apps only by owners adopted Delegation must not reach beyond one's own level 05 4.4
D-K-C7 Owner permissions only with a session adopted. Current state: the exception is the check without a request (R-AZ-29). Stolen tokens must not open the instance 05 2.3, R-AZ-14
D-K-C8 Action access.preview adopted Plain-text preview of every permission action 05 4.2; openapi.yaml previewGrant (rl.admin)
D-K-C9 System actions, access.expiring, expiry_reminded_at adopted Every audit entry names a known action 05 1 rule 10, 4.2, 4.4; 03 grants, group_memberships; event in 05 6 and asyncapi.yaml
D-K-C10 Allowed values of deny_reason partially: list from 07 4.1 normative, without CHECK An audit entry must never fail because of a new code; a test closes the list 03 audit_events, R-DM-28
D-K-C11 Identity tables adopted Local accounts, invitations, OIDC flow 03 local_credentials, enrollment_codes, auth_login_flows; auth_method extended by password_totp, enrollment_code, device_pairing
D-K-C12 Two device keys; RS256/PS256 partially: purpose and unique adopted, further algorithms open Whether Windows Hello delivers ES256 is not proven (open question) 03 device_keys
D-K-C13 Columns on auth_sessions adopted Extensions and the console need their own sessions 03 client_component, parent_session_id, cookie_hash, revoke_reason with inactive
D-K-C14 refresh_token_families.dpop_jkt adopted RFC 9449 binds refresh tokens to the key 03
D-K-C15 Glossary step-up adopted A device key with user confirmation counts 00 "Step-up", 05 2.4
D-K-C16 auth.dpop_nonce_ttl to 08 adopted One name, one chapter (L4) 04 18 removed, 08 18
D-K-C17 user.enable for pending -> active adopted Provisioned OIDC accounts 05 4.3
D-K-C18 Audit checkpoints adopted Anchor the chain outside the database 03 server_signing_keys.purpose with audit_checkpoint, table audit_checkpoints, JCS (RFC 8785); ADR 0021
D-K-C19 IP only as HMAC adopted Data minimization 03 audit_events (ip_hmac with key ID)
D-K-C20 Automatic approval of one's own exports adopted Right of access without waiting time 03 gdpr_requests.approved_by NULL means system
D-K-C21 I1 per dev environment not now A separate decision of the milestone "dev environment" 03 dev_environments.billing_mode only organization, R-DM-24; 21 K-DV-2

Team D (agents, backend policy, extensions)

ID Request Decision Reason Location
D-K-D1 trigger_state_reason adopted A rejection at acceptance needs a place 03 messages with check
D-K-D2 trigger_state = cancelled adopted A cancellation is not an error 03, 00, openapi.yaml TriggerState
D-K-D3 Rejection while another agent has an active run adopted Follow-ups belong to one's own run 05 5 item 8, R-AZ-20, 07 4.1 run_active_other_agent
D-K-D4 Main thread without a fixed operator adopted Effective operator at the time of triggering 03 threads (check), R-DM-21
D-K-D5 run.start from waiting_input, invocation_jti, billing_mode_expected per delivery adopted One start check for both transitions 03 runs, 04 15.3, 05
D-K-D6 run.context.read and run_context.v1 adopted Only the server knows the trust markers 05 4.10; 09 4.6; openapi.yaml getRunContext
D-K-D7 runs.plan, last_api_retry adopted The live box shows the plan and retries 03 runs, 04 11.1, RunPlan, RunApiRetry
D-K-D8 trigger_kind, triggering_command_id adopted Origin of every run 03 runs (check), openapi.yaml Run
D-K-D9 Header typ and claims of the token adopted The adapter checks type and place 05 5 (claims table)
D-K-D10 restricted_to = owner, one-time rule per message adopted Approvals by owners, no double handling 03 message_components
D-K-D11 command.autocomplete_requested, command.autocomplete_result adopted same as B8 04 6.2 and 15.7, 05 6 and 7, asyncapi.yaml
D-K-D12 file.transcript.submit adopted Transcripts by the agent 05 4.8
D-K-D13 agent.triggerers.read adopted Allow list in the adapter 05 4.10; openapi.yaml getAgentTriggerers
D-K-D14 Webhook transport for agents adopted Adapter without a running process 03 webhook_subscriptions.agent_id (exactly one), 04 15.6, openapi.yaml /agents/{agent_id}/webhooks and agent events under webhooks:
D-K-D15 agents.tool_class adopted same as C3 same as C3
D-K-D16 Operator actions require write of the agent (operator_lacks_write) rejected The operator implicitly has write; a second condition would be contradictory none; the operator rule from C2 applies
D-K-D17 App commands without agent.invoke, events for apps adopted Apps have no agent runtime 05 4.10 and 6, R-AZ-18
D-K-D18 supports_title, supports_files_out adopted Features of the adapter 03 agents.operator_config
D-K-D19 external_session_ref optional in run.start adopted Some runtimes only know the session later 04 15.3; the first agent_session.report creates the row
D-K-D20 Reason run_active for model commands adopted No second turn during a run 05 command.invoke, 07 4.1
D-K-D21 Guest agents only organization adopted Guest channels do not count among the I1 targets 05 5 item 3, R-AZ-19
D-K-D22 I1 targets with threads of the agent adopted (with I7) Done threads reopen 03 6.4, index (operator_agent_id, channel_id)
D-K-D23 resolve_i1 adopted Guided flow without an intermediate state 05 1 rule 6, R-AZ-17; openapi.yaml
D-K-D24 Audit on a change of the reported mode adopted A deviation must be traceable 03 agents (store and audit immediately), 05
D-K-D25 cost_entries.source = unpriced adopted Display "Price missing" 03
D-K-D26 Price set instead of price_id adopted One calculation uses several price rows 03 cost_entries without price_id, price_table immutable after use, R-DM-23; ERD 5
D-K-D27 Cache write 1 h adopted Different prices per duration 03 price_table.unit cache_write_1h_mtok, tokens_cache_write_1h; openapi.yaml
D-K-D28 cost.reconcile.submit adopted A job cannot perform a step-up (ADR 0055) 05 2.6 and 4.12 (scope costs:reconcile), 10 12.4; openapi.yaml submitCostReconciliation
D-K-D29 billing_evidence adopted Evidence of the reported mode without key values 03 runs, heartbeat schema; openapi.yaml BillingEvidence
D-K-D30 app.suspend, app.resume adopted Emergency stop without losing the authorizations 05 4.11; openapi.yaml suspendApp, resumeApp
D-K-D31 No manage for apps adopted same as C1 same as C1 (only upload)
D-K-D32 webhook_out only EdDSA adopted Standard Webhooks v1a only knows Ed25519 03 server_signing_keys (check for invocation and webhook_out)
D-K-D33 Path prefix adopted same as A7 same as A7
D-K-D34 Only triggering messages become follow-ups adopted The 05 rule and 09 agree with this 04 15.4
D-K-D35 trigger_state becomes queued on the first delivery adopted Unambiguous transition 04 15.3

Team E (apps and design)

ID Request Decision Reason Location
D-K-E1 Two device keys adopted The NSE must sign without a user interface same as C12 (L10)
D-K-E2 Step-up also with a device key, step_up_method adopted same as C15 03 auth_sessions.step_up_method, 00, 05
D-K-E3 password_totp adopted same as B10 same as B10
D-K-E4 client_metrics.submit adopted with class rl.write No separate class needed (ADR 0057) 05 4.9, 02 4.6 (module stats), 15 17; openapi.yaml submitClientMetrics
D-K-E5 Color and icon keys adopted Contrast gate only for palette values (ADR 0044) 03 instance_settings, spaces, forum_tags; openapi.yaml examples and descriptions; configuration.md 6
D-K-E6 Keys of user_settings adopted One source per key normative in 01 11 and 13 3.3; 03 user_settings refers there
D-K-E7 Grace period for refresh adopted same as B11 same as B11
D-K-E8 Evaluated flags in instance.read adopted feature_flag.read stays with the owner 05 instance.read, 04 6.3
D-K-E9 Style roles for buttons adopted Agents do not set free colors 03 message_components (roles instead of colors)

Team F (operations and observability)

ID Request Decision Reason Location
D-K-F1 Sync epoch adopted Otherwise a new timeline silently overtakes old cursors 03 instance.sync_epoch, 6.7, R-DM-26; 04 6.3, 7.3 to 7.5; 16 8.4; openapi.yaml (snapshot, EventPage, gateway frames)
D-K-F2 trace_context adopted Chain the spans of the adapters 03 event_log.trace_context, 04 2.2; openapi.yaml EventEnvelope
D-K-F3 correlation_id as a W3C trace ID adopted One ID across logs and traces 03 event_log, 04 2.2
D-K-F4 backup_runs adopted The console and alerts read runs 03 4.12 (module status, written by bothy-backupd), 02 4.2; ERD 6
D-K-F5 instance.min_server_version adopted Safe way back by one version 03 instance, 02 3.3 (bothy migrate)
D-K-F6 stats_daily not now No need without a measurement admin-statistics.md 3 names the additive migration, in case the load test requires it
ID Request Decision Reason Location
D-K-G1 file_upload_sessions, file_upload_parts adopted Resumable partial uploads 03 4.7, ERD 4; openapi.yaml getUpload
D-K-G2 files.purpose adopted Avatar, logo and export need read rules 03 (check with scope_channel_id), 05 2.2, openapi.yaml File.purpose
D-K-G3 Unique message_attachments(file_id) adopted Deletion without reference counting 03, R-DM-22
D-K-G4 file.transcript.submit adopted same as D12 same as D12
D-K-G5 Audit on malware detection adopted Security event 05 file.upload, 07 4.1 malware_detected
D-K-G6 Deferred writing of the invocation, port ReleaseInvocation adopted with a different mechanism A port call from files to runs violates the dependency direction (02 4.4) 04 15.3: in-tx event files.FileFinalized, which runs subscribes to; job runs.invocation_hold_timeout (02 4.3, 12.3)
D-K-G7 Variant playback not now Comes with the transcoding milestone as an additive migration 12 17 item 8
D-K-G8 channels.default_notification_level adopted Channel default 03, 13 3.1; openapi.yaml Channel, ChannelUpdate
D-K-G9 push_registrations.auth_session_id, preview_token_hash, values of disabled_reason adopted Logging out ends pushes 03, ERD 6
D-K-G10 notification.created adopted Desktop and in-app notifications 05 6, 04 2, asyncapi.yaml, NotificationCreatedEvent
D-K-G11 focus.idle adopted Open, unused windows 04 6.2, GatewayFocus
D-K-G12 notification_pref.read rejected Same permissions, fewer actions 05 R-AZ-21 (settings.read covers it)
D-K-G13 Search vectors adopted Authority lies with 14 03 5
D-K-G14 threads.search_language adopted Language per thread 03 threads
D-K-G15 file_text_extracts adopted Search in file contents 03 4.7 and 5, ERD 4, job files.extract_text
D-K-G16 Scope for search.query adopted Apps read only with a scope 05 2.6 (messages:read)
D-K-G17 Module search adopted same as A10 same as A10
D-K-G18 Path prefix adopted same as A7 same as A7

Team H (quality and repository)

ID Request Decision Reason Location
D-K-H1 Fixed token format with prefix and checksum adopted (L6) Secret scanners detect leaked product tokens 03 api_tokens, 08 12, 00 "API token", ADR 0044; gitleaks rule bothy-api-token with a test in test_gitleaks_rules.py
D-K-H2 Named throughput threshold adopted No gate without a named value 02 (perf.event_commit_min_per_s), 04 3.2 (ADR 0063)

Team I (import and migration)

ID Request Decision Reason Location
D-K-I1 import.apply, rl.import adopted A mirror runs for days without a step-up per batch 05 3, 4.12, 10; openapi.yaml applyImportBatch, createImportFile, setImportCursor
D-K-I2 channels.read_only adopted Owners have trigger everywhere; only a channel state blocks them 03 channels, R-DM-25; 05 rule 1.8 and channel.read_only.set (step-up); openapi.yaml setChannelReadOnly; problem code channel_read_only
D-K-I3 Operator and times of imported threads adopted Imported threads keep their agent 03 4.11
D-K-I4 thread.updated per batch, imported content not unread adopted with a different mechanism Readers without a row in read_states would otherwise not be covered; one row per thread instead of per reader 04 15.8; 03 threads.unread_floor_seq instead of read states per reader
D-K-I5 Meaning of end_reason = import, binding rule adopted Resuming earlier sessions 03 agent_sessions, 05 agent_session.report (bind_failed), 09 9.5
D-K-I6 cost_baseline_micros adopted Cumulative totals from the runtime 03 agent_sessions
D-K-I7 I1 target set incomplete adopted Error in the core contract 03 6.4 (threads of the agent, open or done); L22 without an owner exception
D-K-I8 forum_tag, state dry_run adopted A dry run writes nothing 03 import_items, import_sources; openapi.yaml without startImportDryRun

Alignment of all documents

ID Item Decision Location
D-K-X1 Rulings L1, L2, L4, L6, L16, L21 to L24 applied 02 3.3 (command register), 16 4.2 and 06 (health), configuration reference, ADR 0044, 12 7.1 (lossless removal of image metadata, ADR 0047 only as a reference), 07 SR6 and 05 R-AZ-02 (DMs), ADR 0024 (no owner exception for I1), 04 8.3 (L23)
D-K-X2 Configuration reference (L4) tools/docs/config_reference.py generates section 4 of docs/operations/configuration.md from all chapter tables, rejects duplicate names and checks in make docs tools/docs, Makefile, gates.md, tools/README.md
D-K-X3 Duplicate and renamed configuration names one source per name; storage.* is now file.*, files.scan.* is now file.scan.*, stats.min_group_size is now privacy.stats_min_group_size, push.apns.key_ref is now push.apns.private_key_ref; Compose follows Chapter tables, compose.yaml
D-K-X4 OIDC flow, pending device 08 follows the API (begin/complete, client_verifier, login_code); device pending with tokens and 403 08, 06, openapi.yaml
D-K-X5 Notification keys and voice recording notify.* normative in 13 3.3; transcription normative in 12 (voice.*) 01, 09, 13, 15
D-K-X6 File tickets instead of presigned GET presigned GET is dropped (ADR 0048), tickets via createFileTicket 06, 12, openapi.yaml
D-K-X7 Search openapi.yaml follows 14: type, sort, multiple filters, segments instead of HTML highlighting, candidates_capped, searchThreadTitles; errors via validation_failed and cursor_invalid 14, 06, openapi.yaml
D-K-X8 Import interface server dry run is dropped; routes complete, leads, batches, files, cursor; job importer.run and queue import are dropped because the import runs in the tool 22 8, 02 12, 05 import.manage, openapi.yaml
D-K-X9 Problem codes new: channel_read_only, tool_class_mismatch, maintenance; I3 and tool class at start as invariant_violation (409) 05, 06, 07 4.1, openapi.yaml
D-K-X10 Push preview and push paths getPushPreview in the API; 13 names the paths from openapi.yaml 13, openapi.yaml
D-K-X11 Generated tables 06 (endpoints, ops, events, problem codes) and webhooks.md regenerated from the contracts; module READMEs regenerated from 02 06, webhooks.md, services/server/**/README.md
D-K-X12 ERDs and diagrams module ERDs with all tables and foreign keys from 03, component diagram with the edge importer -> agents, all SVGs re-rendered docs/architecture
D-K-X13 Example values in openapi.yaml token examples in the format bothy_..., no secret-like values, color keys of the palette openapi.yaml
D-K-X14 Denylist rule for Snowflake IDs digits after a decimal point do not count (coordinates in SVG), with a test tools/denylist/generic.rules, test
D-K-X15 ADR index regenerated with make adr-index (it was outdated before the integration) adr/README.md
D-K-X16 Device pairing (08 5.3) table device_pairings, routes beginPairing, completePairing (actions auth.login.begin, auth.login.complete) and approvePairing (device.approve, step-up); auth.pairing_code_chars 03 4.2, 08 5.3, 05, 20, ERD 1, openapi.yaml
D-K-X17 Second device key in the API DeviceRegistration.step_up_key, Device.step_up_key_thumbprint (L10) openapi.yaml
D-K-X18 Gitleaks rule for product tokens bothy-api-token with a test (follows from H1) .gitleaks.toml, test_gitleaks_rules.py

7. Addendum 2026-10-10: terminal, visualization, showcase

After the teams had finished, the owner gave an addendum: the product is a "command center for AI agents in chat form" (R-PR-13) with the terminal as a core function from M2, native visualizations and a Grafana panel in the same design language, and everything visible must meet the showcase standard. The lead decided the design with rulings T1 to T11; before that, a fresh reviewer read it against the specification and the owner's wording. Where T1 to T11 contradict the first draft, they apply. The writers then wrote chapters 24 and 25, the ADRs 0068 to 0070, the core contract and the plan; their own rulings follow in 7.2 to 7.5.

7.1 Rulings of the lead (T1 to T11)

ID Decision Reason Location
D-T1 Guard rule SR10: a person who is not an owner gets a terminal only on a host assigned to them (hosts.assigned_user_id) or on their own ready dev environment (from V2). Grants to others can be created; the preview says that they have no effect. On the server host, the host service refuses sessions of non-owners. 21 and ADR 0046 reject the OS user as the sole boundary between people; a shared home directory shares LLM logins. 07 SR10, 24 8.2, R-TM-05
D-T2 I6 with owner exception: on a host with llm_credentials = personal only credential_holder_id, not even another owner; there only an individual grant to the holder, no group grant. ADR 0068 distinguishes this from ADR 0024 and D-L22. In addition, with none or organization the host service does not start when it detects a subscription login of the coding agent; the profile claude builds its environment like the runner. The pattern of I1 would make personal hosts impossible with two owners; a group synchronization cannot be refused. A terminal is access to someone else's host, not an inherited level. 07 SR11, 24 7.3, 24 7.4, ADR 0068
D-T3 Protection goal of the end-to-end encryption: relay, proxy, logs, database dump, an api process that reads along. Not: a malicious instance operator running the server. Every device pins the host's key on the first opening (fingerprint visible), with a hard warning on a change; a re-pairing creates an audit entry and an owner notification. Noise NK only proves the key that the server distributes; 19 assumes the server is trustworthy. 24 5.1, R-TM-03, R-SE-12
D-T4 A new ADR partly supersedes ADR 0028: terminal in the web console. terminal.open there only with WebAuthn userVerification=required, no fallback path; hardening (Trusted Types, strict CSP) as a control in 19. The owner requires the terminal in the console and passkey confirmation; otherwise 08 allows step-up without a passkey in the console. ADR 0070, 08 6.3, 19 11a
D-T5 In this version, "Claude Code sessions" means: interactive claude in the terminal, separate from the runner. Taking over a thread in the terminal only becomes possible once the runner and the host service run as the same user in a V2 environment (V3). The runner's session lock; the runner user holds secrets of the instance operator. 24 11.3
D-T6 Multiple nodes: the server instructs the host service over the control connection to open one data WebSocket per session with the session ID in the path; the proxy routes both paths of the same session to the same node by hash. 02 forbids a broker; the event log is not suited for byte streams. 24 9, R-TM-09, R-OP-19
D-T7 The live statistics measure on their own: the server counts its own SLI values with bounds from the Sloth files, minute aggregates in a separate table with its own retention; stats.live and the timeline read from it; the traffic light is called "own measurement". R-OB-08 and 17 forbid reading from the telemetry. 25 3, R-OB-10, R-DM-38
D-T8 The product shows a backend view: billing mode, configured and reported mode, switches, costs and the rate-limit state reported by the adapter. Data about individual subscription seats or credentials does not belong in the product. The product knows no credentials (I5). 25 6.2, R-VZ-08, ADR 0069
D-T9 Handshake: the terminal session token is valid exactly once (the host service remembers its ID until expiry); session ID and protocol version are in the Noise prologue; the PTY starts only after the first valid transport message of the client. Otherwise the relay can replay the first packet and create PTYs that nobody operates. 24 5.3, R-TM-02
D-T10 Keys of the host service as a file with permissions 0600 in the state directory, on Windows with DPAPI; keychain only where available. The login of the coding agent for service users via a long-lived token; the build verifies this path ([assumed] until then). The keychain of a service user without a login session is not unlocked. 24 4.2
D-T11 The Grafana plugin stays unsigned until V1 and is provisioned locally. Signing needs a Grafana Cloud account; that is an owner question in OPEN-QUESTIONS.md (default: unsigned locally, account at publication). The owner decides on an external account. 25 8.3, R-VZ-11

7.2 Findings of the reviewers, fixed by the writers

The writers assigned these IDs themselves; they stay.

ID Decision Reason Location
D-T12 Host service with two OS users: the service user holds keys, connections and Noise; the PTYs run as the session user (host_service.session_user) without read permission on the state directory, connected over a local channel with verification of the peer. New doctor check HD8. Current state: a separate session user per person with its own PTY helper (host_service.session_users), D-W-57. Otherwise every person with a terminal can copy the keys, impersonate the host from elsewhere and read along in later sessions. 24 2.2, 24 4.2, 16 4.4, R-TM-12
D-T13 A second live control connection with the same keys disables the host (duplicate_connection, audit, notification to the owners, re-enabling only by re-pairing); if the older one does not answer within terminal.host_duplicate_probe_timeout, it is replaced. Silent displacement would give an attacker with copied keys every later session; the probe avoids false alarms after network changes. 24 3.4, R-TM-13
D-T14 HD2 rejects service users in groups with admin permissions, including docker, lxd, libvirt, disk, plus host_service.extra_forbidden_groups; the Docker socket must not be writable. According to the vendor documentation these groups give root-level permissions; the owner requires a user without admin permissions. 24 2.2
D-T15 HD6 (subscription login) runs in the account of the session user before every PTY and every host_service.credential_check_interval; a hit ends all sessions of the host with host_policy_violation. Current state: HD6 runs in every session user and since D-W-57 is only an additional check; the load-bearing measure is the separate session user per person. A subscription login can come into existence during a session and would otherwise be used by all later sessions (I6). 24 7.4, R-TM-14
D-T16 Every session in its own process group, on Linux in its own cgroup, on Windows in a Job Object with KILL_ON_JOB_CLOSE; the end terminates the whole tree. Long-running processes remain an honestly documented limit. A revocation should also end background processes. 24 6.5, R-TM-15
D-T17 Quick reattach without step-up never applies in the web console; pinning in the console does not protect against a malicious server (there only require_local_confirm). The console has no device key, and the server delivers its code itself. 24 6.2, 24 5.5, ADR 0070
D-T18 An expired or consumed token closes with 4101, every other token or handshake error with 4104. Cases without an attack must not be reported as tampering; AsyncAPI and the chapter contradicted each other. 24 5.3, R-TM-16
D-T19 Noise test vectors from two independent implementations (cacophony, noise-c), with commit hash, in the test directory of the host service. The Noise specification itself contains no test vectors; the conformance test needs a documented source. 24 5.2, ADR 0068
D-T20 The mapping from SLO to metric of the own measurement is a Sloth label bothy_sli_metric per SLO; slos.md shows it as a column; PLAN M6-11 sets the labels. One source with the SLOs instead of a second list. 25 3.3, slos.md 2, PLAN M6-11
D-T21 Live topology, backend view, delivery and timeline also in the mobile app; only TV mode has no mobile version. The owner requires the views for the console and the apps, "nothing gets cut down". 25 1, 01 3, PLAN M6-16
D-T22 Golden matrix of bothy_viz with the densities comfortable, compact and touch. An untested density would be a gap. 25 7.3, PLAN M6-12
D-T23 New jobs stats.retention and terminal.sweep in the job catalogue. Sessions of a failed node need a fallback; the names were missing from the catalogue. 02 12.3, 24 6.3
D-T24 M7-28 includes a changelog summary per release with a check, architecture graphics readable in light and dark mode and a one-minute test with a fresh reviewer. The owner names architecture graphics, changelog and "understand in one minute". PLAN M7-28

7.3 Terminal

ID Writer ID Decision Reason Location
D-T25 N-T-01 A host is not an entry in principals but a separate principal kind host only in the principal context, authenticated by an Ed25519 challenge. A host needs neither a handle nor a grant subject; a new kind in principals would have affected groups, grants and mentions. 07 3, 05 4.14, 24 4.1
D-T26 N-T-02 Additional actions host.pair, terminal.attach, terminal.stream and the system action terminal.terminate; host.connect covers the control and data connection. Every route and every op needs exactly one action; server-side terminations name a known action. 05 4.14, 02 4.6
D-T27 N-T-03 grants.level is empty exactly when target_kind = 'host'; there capabilities = {terminal}; in the API level is therefore no longer required. A target without levels in the same grant table; a placeholder level would be misleading. 03 grants, R-DM-35
D-T28 N-T-04 Step-up fresh: younger than terminal.step_up_max_age, only passkey or step-up device key, consumed via auth_sessions.step_up_consumed_at. Owner requirement "renewed passkey confirmation on opening", taken literally per opening. 05 2.4, 08 6.3, R-DM-37
D-T29 N-T-05 Quick reattach from the same device in the same login session within terminal.quick_reattach_window without a new step-up; another device always needs a fresh one. Dead zones should not train people to confirm blindly; the exception is tied to the device key. 24 6.2, R-TM-07
D-T30 N-T-06 A host counts as the server host when the owner declares it (hosts.declared_server_host) or the host service reports it. There is no reliable automatic detection; the declaration upholds SR10 even without detection. 24 2.3, 07 SR10
D-T31 N-T-07 Pairing requires typing the first terminal.fingerprint_confirm_chars characters of the fingerprint; failed attempts are limited. As for devices (R-ID-08): a blindly confirmed host would be a path for an intercepted code. 24 3.2
D-T32 N-T-08 Pairing code in the token format of ADR 0061 with its own kind; the gitleaks rule detects it. Secret scanners should find this code too. ADR 0061, 24 3.2
D-T33 N-T-09 Separate purpose terminal of the server signing keys for terminal session tokens, kept apart from invocation. An invocation token must never count as a terminal session token. 03 server_signing_keys, 24 5.3
D-T34 N-T-10 Two new rate-limit classes rl.terminal (per person) and rl.host (per host). Distinct abuse patterns; no existing class fits. 05 3
D-T35 N-T-11 Separate close codes for the three terminal channels; gateway codes apply there only where the channel names them. The reaction of the client differs from the gateway. 04 6.4, asyncapi.yaml
D-T36 N-T-12 New problem codes terminal_denied (with a reason), i6_violation, host_unavailable, fingerprint_mismatch. Same pattern as agent_invocation_denied with a reason. 06 5, openapi.yaml
D-T37 N-T-13 Operations that return secrets or one-time tokens are exempt from Idempotency-Key (x-idempotency-exempt: auth_flow). They must not be kept in the idempotency store; a retry deliberately creates new ones. R-API-24, R-API-26
D-T38 N-T-14 Release component host with its own tags; the host service does not update itself; a version that is too old is rejected. Separate release channel; a self-update would need write permissions of the service user on its binary. 18 9.1, components.py
D-T39 N-T-15 The host service measures idle time; in addition, the server ends the session after terminal.idle_timeout plus terminal.idle_grace without bytes from the device. A tampered host service must not keep sessions open. 24 6.3, R-TM-11
D-T40 N-T-16 The I6 check is 03 section 6.8, the tables are 4.14; guard rules SR10 and SR11 are in 07. No renumbering of existing, widely referenced sections. 03 6.8, 03 4.14
D-T41 N-T-17 Terminal metrics in the signal catalogue; terminal events do not go to webhooks. Without an entry in the catalogue the code must not send the metric; terminal states are irrelevant for apps. signals.md 4.3a
D-T42 N-T-18 Maintenance mode blocks all terminal actions except terminal.read_own. Fail closed; an exception would need a change to ADR 0065. 05 2.7

7.4 Visualization

ID Writer ID Decision Reason Location
D-V01 VZ-R1 stats.live is an ephemeral event on the existing gateway with the ops stats.subscribe and stats.unsubscribe (action stats.read); no separate WebSocket channel, no new action. The gateway already brings authentication, permission checks and close codes. 25 5, R-SY-26
D-V02 VZ-R2 The web console fetches the live frame via REST (getStatsLive, every viz.live_poll_interval). The console does not depend on sync_engine (ADR 0028); one schema for both paths. 25 5.1, R-VZ-06
D-V03 VZ-R3 New table stats_minutes in the module stats with additive upsert and daily partitions; separate retention class stats, never shorter than the SLO window. Ruling D-T7; addition needs no coordinator across nodes. 03 4.15, R-DM-38, R-DP-08
D-V04 VZ-R4 New cross-cutting port kernel/measure, implemented by stats; the measuring modules report twice (OTel and own measurement). A kernel port avoids cycles because stats sits above the measuring modules. 02 4.5, R-VZ-02
D-V05 VZ-R5 Closed metric list with exactly one sli.* metric per SLO; tools/observability/sli_bounds.py generates thresholds and targets for Go and Dart. One source for Prometheus and the own measurement. 25 3.2, 25 3.3, R-VZ-03
D-V06 VZ-R6 Measurement fails open with counted loss; the traffic light is called "own measurement" and does not alert. Statistics never slow down the message path; alerts stay in Prometheus. 25 3.4, R-VZ-04
D-V07 VZ-R7 No counters about people and no triggering people in the live frame, timeline and stats_minutes; the live stream can be switched off (stats.live_enabled). In channels with a single person an inference would otherwise remain possible. 25 4, R-VZ-07
D-V08 VZ-R8 New optional field rate_limit (state and resets_at) in the heartbeat of the adapter. The backend view per D-T8 needs the rate-limit state, without quotas or subscription names. 09 4.8
D-V09 VZ-R9 In the product only a product-neutral backend view. The product knows no credentials (I5); data about individual subscription seats does not belong in the product (D-T8). 25 6.2, R-VZ-08
D-V10 VZ-R10 No kiosk token; TV mode runs in the session of a person, costs hidden by default (viz.tv_show_costs). A token without a person and a device would be access without step-up. 25 6.6, R-VZ-09
D-V11 VZ-R11 bothy_viz draws with its own primitives (CustomPainter) instead of a chart library; every view has a table view. No library has these custom shapes; accessibility. 25 7.1, R-VZ-10
D-V12 VZ-R12 Grafana plugin in the directory plugins/grafana-bothy-live with the ID bothy-live-panel and its own release component; unsigned until V1 and allowed only with this ID; queries as a generated file. Ruling D-T11; the ID follows the scheme of create-plugin. 25 8, dashboards.md 5
D-V13 VZ-R13 Embedding in the console only with an auth proxy, frame-ancestors, Cross-Origin-Resource-Policy and Cross-Origin-Embedder-Policy on Grafana responses; iframe with sandbox. The console is cross-origin isolated; frame-ancestors replaces the disabled clickjacking protection. 25 9.2, R-SE-13
D-V14 VZ-R14 New token roles for visualizations and a TypeScript output of the tokens for the plugin; a build ticket changes the token files. Tokens are generated, not copied (ADR 0030). 25 10, components.md 11
D-V15 VZ-R15 bothy_viz with fixtures and goldens as a parallel track against a mock server; server parts and views in M6, plugin in M7, signing after V1. Owner requirement M6 and M7 with a parallel track. 25 13
D-V16 VZ-R16 The generated blocks in 06 were extended by hand with the new operations. The generator only comes in M1; without this addition the tables would be outdated at once. 06 16, 06 17

7.5 Integration into chapters, plan and briefs

ID Writer ID Decision Reason Location
D-N01 N3-01 Product description "command center for AI agents in chat form" with six pillars; agents Hermes Agent, Claude Code and others. Owner wording; determines the home page and the showcase. R-PR-13, roadmap.md
D-N02 N3-02 Flutter package terminal_ui next to bothy_noise and bothy_viz, without a dependency on sync_engine and local_store. Desktop and console need the same widget; the console has neither a gateway nor a local database. R-CL-14, 15 3
D-N03 N3-03 Key pinning before opening, against getHost; a mismatch only after confirmation, with pin_changed = true; pins in a separate local table, in the console until logout. The audit of the opening needs the confirmation; after opening it would be too late. R-CL-15, R-CL-16
D-N04 N3-04 Stream authentication of the web console via the cookie session of the WebSocket upgrade with an origin check; tokens only for native apps. The console holds no tokens in the browser (BFF). 15 23.1, threat-model.md 6.7
D-N05 N3-05 Terminal block in M2 (M2-38 to M2-48), views in M4, M5 and M6, release of the host service in M7, terminal in the dev environment in V2; terminal red team at the end of M2, M4, M5, M6. Owner: the terminal is core from M2; clients build against a checked contract. PLAN M2-38, PLAN P-20
D-N06 N3-06 The multi-node relay of the terminal comes in M2, independent of the rest of the multi-node operation. Ruling D-T6; otherwise the contract for multiple nodes would remain unchecked. PLAN M2-47, PLAN P-25
D-N07 N3-07 Visualization track M6-10 to M6-12 after the early M4 tickets; server measurement, statistics API and views M6-13 to M6-17; plugin M7-24 to M7-26; signing in V3. Owner requirement of a parallel track; order by the dependencies (P-19). PLAN M6-10, PLAN P-21
D-N08 N3-08 Showcase as M7-27 (demo instance with made-up data) and M7-28 (images, video, README, docs home page, release template) instead of as a V1 ticket. Must be finished before V1; M7 is a prerequisite of V1. PLAN M7-27, PLAN P-22, PLAN P-26
D-N09 N3-09 M1 briefs M1-02, M1-03, M1-25, M1-27, M1-28 and M1-64 minimally aligned with the addendum entries of the core contract; terminal actions stay deferred until M2. Without the alignment these tickets would have ended BLOCKED (P-11). PLAN P-24, Brief M1-27
D-N10 N3-10 02 section 6 is the only list of the places with the working name, grouped by kind; protocol identifiers change only with a new protocol version; the plugin ID stays bothy-live-panel. Grafana requires the ID to match the account; a name change must not silently break the terminal protocol. 02 6, ADR 0044
D-N11 N3-11 In the terminal all keys go to the shell; on mobile an extra keys bar; detach in the background; terminal content never in the local database, crash reports, metrics or logs. Shells use Ctrl and Alt; the app switcher snapshot would show content. R-PR-10, R-PR-11, R-PR-12, R-CL-17
D-N12 N3-12 In the table of event types and ops of 02, gateway stands in the row stats without backticks. The register check from M1-06 reads every name in backticks as an op. 02 4.7
D-N13 N3-13 Performance targets and limits of the terminal in the client as named configuration values in 15. No fixed numbers in prose; M4-38 needs a measurable quantity. 15 22

8. Rulings of the gap integration

Before M1, all items from three lists of spec gaps were decided (contradictions between chapters, briefs and API documents). The results are not copied here but stand where they apply: as an R-ID in the section "Decisions of this chapter", as text in the chapter, in the API documents, as P-<n> or W<n> in the plan or in the briefs.

8.1 Decisions per chapter

Chapter Decisions Location
00 Glossary no table of its own; terms are normative 00
01 Product R-PR-01 onward 01 13
02 Architecture R-AR-01 onward 02 19
03 Data model R-DM-01 onward 03 8
04 Sync protocol R-SY-01 onward 04 17
05 Actions R-AZ-01 onward 05 8
06 API R-API-01 onward 06 19
07 Permissions R-RE-01 onward 07 12
08 Identity R-ID-01 onward 08 16
09 Agents R-AG-01 onward 09 19
10 Backend policy no table of its own; invariants I1 to I6 in the text 10 3
11 Extensions R-EX-01 onward 11 14
12 Files R-FI-01 onward 12 16
13 Push R-PN-01 onward 13 15
14 Search R-SU-01 onward 14 13
15 Client architecture R-CL-01 onward 15 21
16 Operations R-OP-01 onward 16 15
17 Observability R-OB-01 onward 17 12
18 Release R-RL-01 onward 18 13
19 Security R-SE-01 onward 19 15
20 Privacy R-DP-01 onward 20 11
21 Dev isolation R-DV-01 onward 21 9
22 Import R-IM-01 onward 22 17
23 Quality R-QA-01 onward 23 10
24 Terminal R-TM-01 onward 24 16
25 Visualization R-VZ-01 onward 25 14
Plan P-01 onward and contradictions W1 onward PLAN 19, PLAN 7

Check when writing this log: every R-ID that the gap logs name stands in the decision table of its chapter; a sample of items that only change text in a chapter or in the API documents was verifiable at the named location.

8.2 Rulings outside the chapters

These decisions of the gap integration stand in no chapter, because they concern the build order, tools or operations docs. The number is that of the gap item.

ID Decision Reason Location
D-G-33 The project's own check scripts are listed in gates.md, pinned third-party tools in toolchain.md. The test of toolchain.md holds the versions against the pins. tools/README.md, gates.md
D-G-39 M1 rejects violations of I1 with 409 i1_violation; the guided flow resolve_i1 comes additively with M2-21. No contradiction between milestones; the field is additive. PLAN M2-21
D-G-55 One list of reason codes in kernel/auditing.Reason; authorize.Reason without constants of its own. Kernel ports must not import each other (02 7.2). Brief M1-28
D-G-96 Metric bothy.secrets.refresh.failures with an alert and a runbook. Otherwise a silently failing secret refresh is only noticed at expiry. signals.md
D-G-99 The GenAI conventions are encapsulated in internal/platform/otel/genai; the state of the convention is marked as open, not invented. Cannot be determined without network access. signals.md 6
D-G-113 No structured field per action in actions.yaml; a source code test per action keeps them in agreement. A later extension of ADR 0049 without a need in M1. Brief M1-28, ADR 0049
D-G-118 One counter bothy.authz.errors with a reason for unknown_action and authz_not_checked, with an alert. Internal errors of the authorization must be visible without going out as a problem code. signals.md
D-G-124 CONTRIBUTING.md is corrected with the first Go module in M1-01. Today the file is correct because there is no Go code. Brief M1-01
D-G-125 quality.perf.timeout is added to 23 with M1-01 and its watchdog test. Entered now, the test would fail without a Makefile target. Brief M1-01
D-G-126 The action catalogue is reconciled once against a frozen copy. A permanent test on live data would not be reproducible. PLAN M1-02
D-G-127 System actions are in no list of redocly.yaml. System actions have no route. redocly.yaml
D-G-136 The PromQL of the dashboards is checked as a temporary rule file with the pinned promtool. M1-68 reads all dashboard expressions anyway; no new wave. PLAN M1-68
D-G-153 Personal API tokens are off by default until M1-47; M1-47 registers the switch. A switch needs the flag registration. PLAN M1-47
D-G-259 Until they are enabled, fields of later milestones answer with 400 validation_failed with errors[].code not_enabled; every plan row names the field that it enables. A client recognizes the state unambiguously. Brief M1-56
D-G-280 Until M2-05 and M2-20, active_runs in the snapshot is empty and agents have state: unknown. M1-63 wires the port empty, because the table runs only exists from M2-05 on. Brief M1-63, PLAN M2-05
D-G-301 P-04 also applies to gateway ops (section gateway_ops with enabled and later). One mechanism for REST and gateway. PLAN P-04
D-G-304 bothy.event.fanout.lag is the age of the oldest event not yet distributed, 0 when idle. This way the alert fits without a change. signals.md 4.2
D-G-305 Errors of the clients at the gateway count as client_error and do not consume the error budget. The budget measures the server. signals.md 4.1
D-G-310 Instruments of libraries run through an SDK view with an allow-list of attributes. Otherwise, for example, server.address from the Host header would get into the metrics. signals.md 2
D-G-330 Test overlay with the file exporter only for tests, not in the release bundle, checked with otelcol-contrib validate. The canary test needs a readable output. privacy.md
D-G-331 The installation Compose stays unchanged until M7; the smoke overlay leaves out keys of later milestones. An installation must not fail on keys that the schema of the milestone does not know. PLAN P-06
D-G-346 Before the history scan, every identity that is not the one chosen by the owner is removed from the allow-list of authors (D-W-06). Only then does a scan without a finding prove that no other identity is in the history. public-switch.md A
D-G-347 The four closing documents are created in M0-04; until then only the references to them remain open. Exit criteria of M0 without a chicken-and-egg problem. PLAN M0-04
D-G-DEP Dependencies of the M1 tickets are minimal (direct only); waves are computed from them. Transitive dependencies in every row would go stale with every change. briefs/README.md

9. Ruling log of the resumption

The third pass of the gap integration aborted without writing a log; its changes lay uncommitted in the working tree. Planning was then resumed.

ID Decision Reason Location
D-W-01 The resumption checked every item of the aborted pass against the state in the working tree (done, partial, open, to be rejected, with the location of the evidence) and completed the items that were not complete. No item counts as done just because a file was touched. An aborted writer leaves changes without proof; only the check per item proves the state. Commits from "docs(kern): core contract extended after gaps part 3" to "docs(briefs): PLAN and M1 briefs aligned after gaps part 3"
D-W-02 Decisions of the first two passes are not reverted. The briefs build on them; reverting them would be a new decision without a new reason. Section 8
D-W-03 The owner addendum (terminal, visualization, showcase) was added in the same resumption: design with the rulings T1 to T11, then core contract, chapters 24 and 25, ADRs, plan and briefs. The planning state before the resumption contained neither; the owner requirement is binding. Section 7, PLAN 2
D-W-04 One writer in the main tree at a time; reviewers only read; every finding of a reviewer is fixed and seen by the next fresh reviewer. No overlapping changes; no finding counts as fixed without a fresh look. PLAN 4
D-W-05 The closing documents (SPEC, DECISIONS, OPEN-QUESTIONS, RISKS, README, navigation, CHANGELOG) are written last; until then their absence is the only expected warning in the strict docs build. They summarize the state and can only be correct afterwards. PLAN M0-04, D-G-347
D-W-06 Commit identity: the owner chose the identity of the commits; it counts as a public identity in the sense of public-switch.md step 2. The private deny list allows it only in the author and committer fields; every other occurrence of the same domain stays forbidden. Rewriting the authors is dropped. Before the first push, only the rewrite with git filter-repo --replace-text for the old sample values of the gitleaks exception remains; afterwards the exception is deleted (M0-08). Current state: D-W-36 and D-W-48 (M0-08 also removes every hit of the history scan, with its own tool per location). Identities in allow-authors.txt that are not the chosen one are removed before the history scan. The product repo names neither the name nor the address. Current state: D-W-93 (what is published is a root commit without parents; the rewrite before the first push is dropped). Owner decision (D-L18); the white label in the content is preserved. public-switch.md A
D-W-07 Design accepted on the preview images: dark petrol, yellow accent, working name Bothy. Owner acceptance. design/previews, D-DS-01
D-W-08 Ticket numbers are only names. The build order is set by the dependencies, for M1 by the waves in briefs/README.md; dependencies never point to a later milestone and never form a cycle (PLAN P-19, reworded). Existing dependencies on higher numbers of the same milestone stay. The earlier version of P-19 required dependencies only on lower numbers; some tickets violated that. Renumbering would break the names of the briefs and many references without changing the order, which the waves already show completely. PLAN P-19, briefs/README.md
D-W-09 New doctor check HD9: the session user of the host service must not be able to list any home folder of another user; checked as a read test, not via groups. Current state: HD9 checks only home folders of users that are not session users; the home of a session user is checked by HD11 (D-W-88). On macOS the default group staff opens other users' home folders; the read test works the same on every platform. 24 2.2, R-TM-17, TM-T24
D-W-10 The integrations from requirement 19 are build tickets: reference app code hosting (M2-49, TypeScript SDK), reference app ticket system (M2-50, Python SDK), cron path with a test in M2-27, alerts in M7-20. Tests use only fake APIs with fixtures from the vendors' documentation. Finding L-01 of the critic: 11 only announced the apps, no ticket built them; the requirement asks for them explicitly. Real services in tests would be external accounts. 11 10, R-EX-11, PLAN 8
D-W-11 Installation in one command via the setup wizard install.sh (POSIX shell on the host, release artifact with built-in version and checksum of the bundle), ticket M7-29; the portability test installs with exactly its options (source of the script by phase, D-W-59). No wizard in the container; no random secrets in .env, secrets-init still generates them; off-site stays optional with a warning. Finding L-02: requirement 21 and the addendum require one command and a setup wizard; no ticket built it. A container would need the Docker socket (root on the host); secrets in .env would contradict R-OP-05. 16 3.1, R-OP-20, Installation
D-W-12 Entry screens E1 to E7 (server address, login, enrollment code, device waiting, pairing, app lock, logged out) shared by all apps; the console uses E2 and E7. Finding L-19: M4-18 built these flows without screens. 01 5.6, R-PR-14
D-W-13 Terminal view as a component of the design system with its own roles color.terminal.* (surface, text, cursor, selection, 16 ANSI colors from the existing palette); the roles color.viz.* go into the tokens already now, M6-10 adds the check for color vision deficiency. New preview images terminal and statistics; the accepted images stay unchanged (tokens.py previews --view). Current state: D-W-46 (D-W-41 afterwards regenerated seven accepted images) and D-W-77 (the new images fall under the acceptance). The three grays ANSI 0, 7 and 15 as well as the cursor need contrast ui, all other terminal colors text. Finding L-20: the terminal was missing from the design system, color.viz.* stood in components.md without a token. Only an addition: the accepted design (D-W-07) does not change. Full text contrast for all three grays cannot be reached in both themes at once. components.md 12, contrast.md, PLAN M6-10
D-W-14 Docs home page with a showcase standard (image light and dark, six pillars, quick start, reading path per role); links to repository files are relative, the MkDocs hook builds the URL from repo_url. New gate in make linkcheck: no link outside code to the placeholder repository OWNER. Finding L-22: the home page had no image, no quick start and a dead link; lychee checks offline and did not see it. Home page, gates.md, tools/docs/placeholder_links.py
D-W-15 One contract for expired and replaced invocation tokens: the adapter acknowledges after the acceptance into the inbox, not after run.start; a retry with a new token replaces the stored one; a token error at start is always 403 invocation_token_invalid with reason expired, superseded or invalid; an expired token that is still current is renewed by the new action run.token.renew (POST /runs/{id}/invocation-token), which repeats all checks before a delivery. The earlier codes invocation_token_expired (401) and stale_invocation_token are dropped. Finding L-07: three incompatible contracts; a start longer than agent.ack_timeout triggered a redelivery whose token replaced the old one and which the duplicate detection discarded, and the run hung until run.queued_max_age. The renewed token is not a new authorization, because the server decides anew. 09 4.3, 09 4.5, 04 15.2, 06, AG-28, AG-29
D-W-16 Tool class in the protocol chapter: claims tool_class and dev_environment_id in 09, check point "reported class not higher than configured class" in the adapter, required field tool_class_used in startRun and resumeRun (server: 409 tool_class_mismatch), reasons tool_class_mismatch, tool_class_owner_only and channel_read_only in 09 5.2; the runner maps chat, scoped, host to --permission-mode and --tools, host is bypassPermissions. Finding L-08: 06 promised a 409 at start without a field in the body; unknown reasons would have been stored as adapter_error; the class full did not exist. 09 4.5, 09 18.2, AG-30
D-W-17 With llm_credentials = personal, bothy-host does not touch any subscription login: no token in the state folder, no reading, no handover. The holder of the login logs in themselves in the account of the session user (/login or their own token from claude setup-token in a file of the session user); the profile claude then starts via that user's login shell. Ruling T10 applies more narrowly: the long-lived token remains the path for a user without a login session, but it lies with the session user. Finding L-03: the earlier version put the token in the state folder and passed it into the PTY; the product would have stored and brokered a Claude.ai login, against 10 consequence 3 and I5. 24 7.4, OQ-03, TM-T25
D-W-18 The PTY helper is a system service under the session user that the session user cannot change, stop or replace: binary, service definition and channel endpoint owned by root or Administrators, respectively, endpoint from the service manager, verification of the peer by user and process ID, memory protection; new check HD10, HD8 additionally by the service user. Owners see a warning before opening on the host of another person; the audit names foreign_assignee. New threat T-TM-12. Finding L-04: a helper owned by the session user could be replaced; check results could be forged and later sessions could be read along. The same user ID remains one trust zone; the warning says so honestly. 24 2.2, 24 8.4, TM-T26, Threat model
D-W-19 Authorizer.Scope intersects with ActingFor as with ViewAs; the search of an agent in a run works on the intersection with the grants of the triggering person, also for the candidate limit, candidates_capped and cursor. Finding L-05: SR9 requires the intersection for every reading action in a run; without it, the candidate limit and the page marker would reveal something about channels that the triggering person must not read. 14 4.3, 07 7.4, M1-28, M2-14, M3-14
D-W-20 Problem codes are a closed list: chapters, briefs and plan name only codes from x-problem-codes; finer reasons stand in reason (for authorize, the reason code from 07 4.1) or in errors[].code of 400 validation_failed. The codes so far named only in chapters are mapped, for example 413 payload_too_large instead of file_too_large, 409 conflict with reason quota_exceeded, 503 unavailable with reason storage_full or search_timeout, respectively, 400 validation_failed with errors[].code patch_path_forbidden, 403 forbidden with reason owner_only. The check tools/docs/problem_code_refs.py runs in make docs. Finding L-10: about a dozen codes and statuses (507, 422 outside idempotency) were attached to no operation; the same class of error without a gate. 06 5 item 7, R-API-28, openapi.yaml createUpload, uploadPart, completeUpload, bindAgentSession, 12 3, 09, 14 7
D-W-21 The enum AppScope contains costs:reconcile; which actions a scope allows is stated only in 05 2.6, chapter 11 now names only the typical use; M1-03 checks the enum against the scopes of the catalogue (sub-check app-scopes). Finding L-09: the reconciliation app could neither request nor receive its scope, and 11 differed from 05 on the actions. 05 2.6, 11 5, Brief M1-03
D-W-22 retention.content = none requires a reason in the configuration (retention.content_none_reason); without a reason, bothy config check and the startup log warn. The earlier confirmation in the console with audit is dropped. The job of the class security is named retention.security as in the job catalogue. Finding L-12: the confirmation had neither an action nor a storage location, and the value is read only at process start; privacy.retention_sweep stood in no job catalogue. 20 4.1, R-DP-01, DP-T13, PLAN M7-05
D-W-23 webhook.retry_max_age, webhook.secret_rotation_overlap and webhook.receiver_tolerance stand in the value table of 11; webhook.receiver_tolerance with place "Receiver" (recommendation, no server value, no environment variable). A test ensures that value tables stand only in the chapters. Finding L-11: the generator reads only docs/spec/; the values were missing from the configuration reference and from the future schema. 11 16, webhooks.md 9, tools/docs/tests/test_config_reference.py
D-W-24 Gateway: a separate close code 4018 forbidden for a valid login without permission (for example scope_missing), 4004 now only for invalid credentials; in 04, the fields of hello are named heartbeat_interval_ms and resume_window_ms as in the schema; heartbeat and reauth consume nothing from rl.gateway_connect; the RL class for "no class" is named none in the catalogue as in the API. Terminal: 4101 is named terminal_token_stale; 24 section 13 explains all codes 4100 to 4114; a host service without login within terminal.host_auth_timeout gets 4113. Finding L-28: when a scope was missing, 4004 led to a pointless token renewal; names and explanations differed from each other. 04 6.3 and 6.4, 24 13, asyncapi.yaml, Brief M1-64, PLAN M2-43
D-W-25 The day boundary of the statistics is the configuration value stats.time_zone (default UTC). Durable events without a webhook stand with a reason in the list "Not subscribable" of webhooks.md; M1-03 checks that every durable event stands in exactly one of the two places. authorize in 07 has the branch for step-up fresh. bindAgentSession carries expected_current_session_id and take_over. Upload responses in the chapter follow openapi.yaml (200 instead of 204 or 202, respectively). Finding L-29 and alignment during finding L-10: planning remnants ("ticket with team E"), a missing rule for 15 event types, pseudocode without the level fresh, fields only in the chapter. 17 Configuration values, admin-statistics.md 5, webhooks.md 7, 07 4.2, 09 9.4
D-W-26 The module terminal is in the module register already in M1: M1-05 creates doc.go, the README and the entry with tables and lists from 02, M1-06 checks it like every module; it is built only in M2 (P-24 now also names M1-05 and M1-06). The module READMEs are aligned with 02 (ports, tables, dependencies, actions, events). Finding L-13: M1-05 did not know the module, M1-06 checks the register against 02; the ticket could not turn green. M1-05, M1-06, PLAN P-24
D-W-27 Briefs name no count that a list next to them or a chapter already fixes; tests read the list or the chapter (modules from 02, deferred actions in M1-28, cases of 05 rule 1.6 in M1-29, rate-limit classes in M1-19). The AZ-P7 case for group.member.sync belongs to M1-42, because the sync of the claim groups only comes into being there. Finding L-14: fixed numbers contradicted the lists (twelve instead of all deferred actions, seventeen cases for eighteen actions); the acceptance criteria could not be met that way. M1-28, M1-29, M1-42, M1-19
D-W-28 A brief calls only make targets that the Makefile already has or that a ticket of its dependency closure creates. New edges: M1-06 and M1-08 before M1-13, M1-03 before M1-17 (instead of M1-02, now transitive), M1-23 before M1-32 (the ticket changes the Dockerfile); M1-23 before M1-73 is thereby transitive. The waves stay the same. tools/docs/brief_deps.py checks in make docs the index, waves, graph, plan column, header lines of the briefs and the table of targets; --write regenerates waves and graph. Finding L-15: the launcher starts a ticket as soon as its own dependencies are merged; 36 calls of make config-doc-check, make register-check, make api-checks and make image lay outside the closure. briefs/README.md, PLAN 7
D-W-29 Briefs check for en and em dashes with tools/hooks/text_rules.py instead of grep -P; every brief with a new migration names make migrations-lock; M1-47 refers to 18 section 2 "Components", M1-63 checks the content of ADR 0059 instead of its existence. Finding L-27: grep -P fails on macOS with exit 2 and empty output, which counted as passed; without a lock line the migration lint fails only in the gate. M1-01, M1-47, M1-63
D-W-30 V1 and V2 have a test strategy and a red team section, V3 has scope, test strategy and red team per ticket; V2 requires DEV-T08 (V2-07). M5-07 uses voice.max_duration; M3-13 builds no second migration lint, but passes the one from M1-01. Finding L-26: the sections were missing, the key did not exist, and the lint was planned twice. PLAN 15 to 17, PLAN M3
D-W-31 Superseded by D-W-92. What remains: signing secrets lie only in the environments release*; jobs with this environment run, like all jobs, on GitHub-hosted runners, each in a new VM, and take no values from other jobs outside of conditions (D-W-52, D-W-72); new threat T-SC-05. Finding L-06: a dependency PR must not change anything that a job with signing secrets uses; requirement 15 requires "no secrets beyond what the job needs". ADR 0039, R-QA-12, release.yml, Threat model
D-W-32 Before every push and every local merge, an independent, read-only review of the diff; proof .review/<sha>.md (git-ignored) with the last line Urteil: JA, checked by the pre-push hook tools/hooks/review_gate.py. The overall review per milestone and before going public stays. Contributors from forks switch it off visibly with REVIEW_SKIP=1; the maintainer checks before the merge. Current state: D-W-50 (report as text in the pull request). Finding L-16: requirement 23 requires an AI review before every push, PLAN 3.9 one per ticket; the docs named it only per milestone, and make review enforced nothing. The independence itself cannot be checked by machine (RK-85). red-team-gate.md, push-discipline.md, R-QA-13, CONTRIBUTING.md
D-W-33 Documentation in code as a gate per language: ruff D (convention google, tests excluded) for tools/ from now on and for the Python SDK, revive exported for Go (M1-05), public_member_api_docs and dart doc for Dart (M4-01), typedoc for the TypeScript SDK (M2-30); API references of the SDKs and Dart packages in the docs site; the server documents its public interface through openapi.yaml. The build rules 1 to 5 stand in every brief byte for byte as in the block of docs/briefs/README.md; a test holds that. "docs site" instead of "MkDocs" stays: that is the wording of the lead's template, and ADR 0045 keeps the generator replaceable (superseded by D-W-54: the block translates the owner requirements literally). Finding L-17: in the tool code, docstrings were missing on most public symbols, no gate checked them, no ticket generated API docs, and the briefs quoted the rules in ten versions. 23 Documentation in code, R-QA-14, briefs/README.md, tools/docs/tests/test_brief_rules.py, PLAN M2-29, M2-30, M4-01
D-W-34 denylist-main.yml checks every push to main (range before..after) with the private list from the environment denylist; it is the only exception to the push ban, only for exactly main, listed in check_runners.py as POST_MERGE_WORKFLOWS. Procedure on a hit: push-discipline.md "Deny-list hit on main". A merge queue with a required check was rejected. Finding L-18: fork PRs saw only the generic rules, and after the merge nothing checked; requirement 22 requires a CI job that fails. A squash can also change the message after the checks. Merge queues exist on GitHub.com only for public repositories of organizations and for private ones with GitHub Enterprise Cloud, not for a personal account (https://raw.githubusercontent.com/github/docs/main/data/reusables/gated-features/merge-queue.md, retrieved 2026-10-11) [verified]. denylist-main.yml, R-QA-15, ADR 0042
D-W-35 Superseded by D-W-92: no rule anymore; the named values and their test are removed. The rule applied only to runs in the private state, which does not exist. ADR 0039, R-QA-16
D-W-36 M0-08 cleans the whole history: every hit of make denylist-history (as of 2026-10-11 only old versions of planning files, hits from the private list and generic rules, none in identities) and the old gitleaks sample values, with one git filter-repo --replace-text over all refs; the replacement file is written by a new scanner option --replacements-out outside the repository. Acceptance: 0 hits. The history is not rewritten in this planning; only M0-08 does that. Squashing the planning branch into one commit was rejected. Current state: D-W-48 (--replace-text alone is not enough; a tool per location). Current state: superseded by D-W-93 (the owner publishes a root commit without parents; the planning history stays private and documents the decisions there). Finding L-24: the scope of M0-08 named only the gitleaks sample values, the acceptance required more. The planning history documents decisions; a squash would lose it. PLAN M0-08, public-switch.md, RK-77
D-W-37 Allowed commit identities count only as author, committer and in a Signed-off-by line that repeats exactly the author or committer of the same commit. Co-authored-by and other people's sign-offs are checked by the private list like any location. Only Signed-off-by and Co-authored-by are identity lines (private list only, because contributors sign off with their own address); Reviewed-by and similar lines are message text with all rules. Finding L-25: the scanner excluded six kinds of lines and allowed the identity there completely, against D-W-06 ("only in the author and committer fields"). Applying the generic rules to sign-offs would block every external DCO contribution; this part of the finding is rejected. tools/denylist/denylist_scan.py, tools/denylist/README.md, ADR 0042
D-W-38 Language: all repository content is English (D-W-94, owner, 2026-10-11). Before: community and showcase files English (README.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, GOVERNANCE.md, SUPPORT.md, CHANGELOG.md, templates under .github/), as well as build briefs, comments and messages in operations artifacts (infra/, D-L5) and tool code; planning documents under docs/ German. Finding L-30: the rule stood only in the team instruction, not in the log; infra/ had German comments and German :? messages in compose.yaml, although instance operators worldwide read them. English is the language of the contributors of a public project. CONTRIBUTING.md, infra/, D-L5, D-OB-07
D-W-39 Diagrams in the design system: tools/design-tokens/tokens.py generates design/build/mermaid.config.json (Mermaid theme base, colors and font of the light theme) and docs/design/site-theme.css (colors of the docs site and its Mermaid blocks per color scheme). make diagrams renders with it and checks that every .svg is rendered with the theme, that no diagram exceeds the aspect ratio MAX_ASPECT and that .mmd files name only token colors. render_diagrams.py --write renders again. Finding L-23: all diagrams used Mermaid's default theme with purple, although the design system forbids purple as an accent; erd-6 was about 6:1 and unreadable at page width. architecture/README.md, tools/docs/render_diagrams.py, quality/gates.md
D-W-40 The ERDs live only in docs/architecture/erd-*.mmd, one diagram per module (erd-6 split into erd-6-sync, erd-8-audit-privacy, erd-9-import-status-statistics, erd-10-notification); chapter 03 section 7 only refers to them. tools/docs/erd_check.py (part of make diagrams) checks against 03: every table exactly once in full, every entity a table from 03, every foreign key drawn as often as there are foreign keys between the two tables; the only exceptions are bookkeeping columns (name ends in _by, target principals). Finding L-23: the ERDs existed twice (architecture and 03), none was complete (audit_ip_keys, stats_minutes were missing, foreign keys were missing in 03), and nothing checked the promise "every table exactly once". 03 section 7, tools/docs/erd_check.py
D-W-41 A separate guard for tinted backgrounds (*.subtle, *.chip-bg): in the warm hue range only with a clearly visible hue (chroma at least the value in design/design-rules.json). color.warning.subtle in the light theme therefore uses palette.amber.100 instead of amber.50. The preview shows run states with the words from components.md instead of the enum values, and the image of the main view no longer cuts off a message below the live box. Finding L-30: the cream guard applied only to neutrals and surfaces; as a surface, amber.50 looked like cream. Raw enum names are not UI texts; the cut-off fragment looked like a bug in the showcase image. design/design-rules.json, tools/design-tokens/tokenkit/rules.py, design/contrast.md, docs/design/previews/
D-W-42 ADR 0045 names the end of support for Material (commitment from 9.7.0 for at least twelve months, so 2026-11-11 at the earliest) and three triggers for the switch to Zensical: Zensical 1.0; a security vulnerability in the shipped site without a fix after the end of the commitment; a required Python or MkDocs version that Material does not support. Finding L-30: the ADR planned the switch without a date and without a trigger for the case that Zensical is not mature in time. ADR 0045
D-W-43 Build rule 3 ("API documentation is generated from code") applies to the server like this: one source, openapi.yaml and asyncapi.yaml (ADR 0013, spec-first); server stubs and clients are generated from it, and the API reference of the docs is produced from these files. For SDKs and Dart packages, the rule applies literally (D-W-33). Owner question OQ-22. Finding L-30: the owner's requirement and ADR 0013 read as opposites; build sessions could generate OpenAPI from the routes and thereby overwrite the normative contract. Both paths end at one source; spec-first keeps the contract checks (x-authz-action, lint) ahead of the code. ADR 0013, briefs/README.md
D-W-44 run.token.renew and run.start accept no consumed token (jti equal to runs.started_jti) for a waiting run; repeating a start returns the result only while the run is running. run.token.renew additionally accepts the token from before the last renewal (new column runs.renewed_from_jti; every delivery and every start set it to NULL) and then issues again. The error stays superseded, no new reason. The duplicate number R-AG-22 in 09 is resolved (resumption rule now R-AG-24). Findings K2-02 and K2-03: after running -> waiting_input, the consumed start token still stood in runs.invocation_jti; a renewal with it would have continued the run without an answer from a person (against R-AG-08). A lost response of run.token.renew left the run hanging until run.queued_max_age. superseded already asks the adapter for the right thing (start nothing, wait for the token of the response); a new code would only bring more cases. 09 4.5 and 4.5.1, 04 15.3, 03 runs, 06, openapi.yaml renewInvocationToken, AG-31, AG-32
D-W-45 HD6 calls the coding agent only via the absolute path host_service.claude_bin, with an environment of the PTY helper; HD10 checks this file like the binary of the helper. Without a value, HD6 checks only the login locations and warns with auth_status_unchecked. Forgeability through the data of the same user ID stands as a residual risk in T-TM-08 and RK-84. Current state: D-W-57 (HD6 only an additional check, fixed search list, HD10 also for the folder chain and interpreter). Finding K2-19: the session user could replace claude auth status from their PATH with a program that conceals the subscription login. A fixed, non-writable path closes this route, as with HD10. 24 7.4, Threat model T-TM-08, RISKS RK-84, TM-T27
D-W-46 After the owner acceptance (D-W-07), D-W-41 regenerated seven accepted preview images (comparison c04ab79 against 0bc7083): components-light and components-dark (run states with words instead of enum values; a code block example now stands in the column "Components on messages"; in the light image, the offline banner with amber.100), layout-light and layout-dark (the message below the live box is no longer cut off, the code block fragment drops out there), mobile-light (offline banner with amber.100), palette-light (swatch of color.warning.subtle) and mobile-dark (only rendering noise of single pixels, no visible difference). These seven images are content corrections within the accepted design (D-W-07) and are accepted: the owner decision "Design accepted on docs/design/previews" applies to the images in this folder; the planning received them in this version on 2026-10-11, after the regeneration on the same day. A re-acceptance is not needed. The images terminal-* and viz-* were added after the acceptance (D-W-13) and are accepted as well (ruling T14, D-W-77). Finding K2-07: D-W-13 said "the accepted images stay unchanged"; that was true for its commit, but no longer after D-W-41. Color and shape (dark petrol, yellow accent) stay the same; what changed are one signal token in the light theme and contents of the preview. design/previews, D-W-13, D-W-41, D-W-77, Open questions, Decided
D-W-47 The setup wizard delivers a usable instance in the profile direct: after docker compose up --no-start it enters the gateway of the Compose network as /32 in BOTHY_TRUSTED_PROXIES, then starts and checks GET /api/v1/instance/discovery over the published port. New option --storage fs\|s3 (sets profile s3 and BOTHY_STORAGE_DRIVER); --scan and --observability take on\|off. The command without a terminal (curl ... \| sh -s -- --yes ...) stands verbatim in 16 3.1 and install-compose.md; the portability test runs exactly this command. A second run sets the value only while it is at the default. Finding K2-01: with the default (loopback only), the server rejected every request over the published port (SEC-TLS-1); the portability test required s3, the script had no option for it; the command without a terminal was not documented. --no-start creates the network without starting api with a wrong value. 16 3.1, 16 11.1, install-compose.md 1.1, PLAN M7-29, M7-14
D-W-48 M0-08 uses the matching git filter-repo tool per location, all in one run: --replace-text for file contents, --replace-message for commit and tag messages, --paths-from-file with lines old==>new for paths, --strip-blobs-with-ids for binary blobs with a hit, plus --prune-empty never. For this, --replacements-out writes a folder with text.txt, paths.txt and blobs.txt. A fixture repository with hits in all locations proves 0 hits with the same number of commits. Current state: D-W-93 (only needed if a history is published after all). Finding K2-12: --replace-text changes only blobs and skips blobs with NUL in the first 8 KiB (source git-filter-repo, function _tweak_blob); messages need --replace-message. A paths file with only renames keeps all other paths (without a filter line the selection is not restrictive). Without --prune-empty never, a commit that contained only a removed blob would be dropped. https://github.com/newren/git-filter-repo/blob/main/Documentation/git-filter-repo.txt and https://github.com/newren/git-filter-repo/blob/main/git-filter-repo, retrieved 2026-10-11 [verified]. PLAN M0-08, public-switch.md A, D-W-36
D-W-49 make docs checks with tools/docs/register_ids.py that every ID in DECISIONS.md, RISKS.md and OPEN-QUESTIONS.md is defined exactly once and that the overview and the sections of the open questions name the same IDs. The duplicate RK-82 (diagram SVGs) is now RK-87; RK-82 stays with the setup wizard, because it stood there first. Current state: extended to all tables in docs/**/*.md by D-W-60. Finding K2-14: RK-82 was assigned twice, and no gate found it. Rule 1 of this log (IDs stable, never duplicate) needs a test. tools/docs/register_ids.py, gates.md, RISKS
D-W-50 The report of the independent review per push goes as text into the section "Review report" of the pull request template; .review/ stays git-ignored. Current state: LG/1 (the report stays local; the pull request names commit and verdict in fresh words). Finding K2-18: a git-ignored local file cannot be linked in the pull request. Committing the report would mix it with the reviewed diff and change its own SHA. CONTRIBUTING.md, Template, red-team-gate.md, tools/hooks/review_gate.py, D-W-32
D-W-51 Values of the tool bothy import (import.*) stand in a separate YAML file with nested keys (option --config) or in the environment by the naming rule, never in the server's file. Finding K2-06: an operations template mixed flat dot keys and tool keys in the server file; the server rejects both. Chapter 22 did not name the format of the tool file. 22 19, 22 10, PLAN M8-04
D-W-52 No value of the job resolve (without environment release*) reaches a shell or action of a job with environment release* in release.yml. Each of these jobs resolves the tag again itself with tools/ci/components.py release (path only from RELEASE_COMPONENTS), compares all five outputs with needs.resolve.outputs in an if: and aborts on a mismatch; afterwards it uses only its own result. publish downloads the build artifacts via artifact-ids from the outputs of the release jobs, not by name or pattern. check_runners.py enforces both (rules release-untrusted-output, release-artifact-by-id). Current state: release-untrusted-output is an allow list (D-W-61). Finding K2-04: make -C "$COMPONENT_PATH" in the build with signing secrets took the path unchecked from the job resolve; a compromised job without signing secrets could thus have set its own path including a Makefile. The same route existed through the pattern dist-*, because every job of a run can upload artifacts. A comparison in if: does not insert a foreign value into a command. https://raw.githubusercontent.com/actions/download-artifact/9000827ccba6bdab643e8b6fd33ac0654aef8333/action.yml (input artifact-ids) and https://raw.githubusercontent.com/actions/upload-artifact/cf430e030ddbb5b0abf93d22962f4752f3646cd9/action.yml (output artifact-id), retrieved 2026-10-11 [verified]. release.yml, tools/ci/check_runners.py, ADR 0039, Threat model T-SC-05, R-QA-12
D-W-53 check_runners.py resolves runs-on: ${{ matrix.<key> }} for each combination of the static strategy.matrix (with include and exclude by GitHub's rules); every runner label found this way goes through the same rule. A matrix behind runs-on that is computed only at run time is a finding. The portability test runs as a matrix on ubuntu-24.04 and ubuntu-24.04-arm, as 16 11.1 and M7-14 require. Current state: The rule is named runner-not-github-hosted (D-W-92). Finding K2-11: portability.yml ran only on amd64, although 16 11.1 requires amd64 and arm64. ubuntu-24.04-arm is a standard runner for public repositories (https://docs.github.com/en/actions/reference/runners/github-hosted-runners, retrieved 2026-10-11) [verified]. portability.yml, ADR 0039
D-W-54 The block "Fixed build rules" in docs/briefs/README.md and all briefs is a literal English translation of the owner requirements: "Python API docs in MkDocs", "stages are only the order", no German word. A note after the block explains that "stages" are the milestones and that "MkDocs", after the planned switch to Zensical, means the docs site from mkdocs.yml. test_brief_rules.py keeps the block identical in all briefs and checks these words. Supersedes the "docs site" part of D-W-33. Finding K2-13: The owner requirements demand the items word for word; "docs site" and "etappen" deviated. A template of the lead does not take precedence over the owner requirement (precedence: the owner's brief before plan and templates). briefs/README.md, tools/docs/tests/test_brief_rules.py, D-W-33, ADR 0045
D-W-55 The statistics preview shows for delivery what the own measurement knows: the share of live deliveries within the threshold (25 6.3), with Y-axis values and a dashed target line. The generator reads target and threshold from the SLO delivery-latency in infra/observability/slo/sync.yaml (tokenkit.emit_preview.delivery_slo); a test compares with a second evaluation of the same file. viz-light.png and viz-dark.png are regenerated and accepted (D-W-13, Ruling T14, D-W-77). Finding K2-15: The preview named "Target 250 ms" for "Delivery p99"; this value does not exist for delivery (250 ms is perf.catchup_page_p99), and the own measurement stores no percentiles but shares up to the threshold. 25 6.3, design/previews, tools/design-tokens/tests/test_previews.py, slos.md
D-W-56 render_diagrams.py --write writes into every .svg, directly after the <svg> tag, a comment with the SHA-256 of the .mmd and of design/build/mermaid.config.json. make diagrams recomputes it and reports every .svg without a stamp or with a different stamp as stale; a test runs the check over all checked-in diagrams. All SVGs are re-rendered and stamped. Finding K2-17: The gate checked only theme and aspect ratio; a changed .mmd without a new render was noticed only in review. The stamp depends only on the inputs, not on the browser version, and also detects a theme change; a comparison of the text nodes would not find the theme change. tools/docs/render_diagrams.py, quality/gates.md, RISKS RK-87, D-W-39
D-W-57 Ruling T12 of the lead (after critique round 3, 2026-10-11): one session user per person and host, never shared, not even after an assignment ends. New table host_session_users (name unique per host across all rows), terminal_sessions.host_session_user_id, guard rule SR12 (without an assignment 403 terminal_denied with reason: no_session_user, also for owners), 409 conflict with reason: session_user_taken, claim osu in the terminal session token, binding of osu to the first person in the host service (session_user_unknown, session_user_mismatch), one PTY helper per session user (host_service.session_users instead of host_service.session_user). Assignment at confirmHost (session_users) and updateHost (add_session_users, revoke_session_users). HD6 stays as an additional check (Ruling T2 still applies), calls the coding agent with a fixed search list as PATH, and HD10 also checks the folder chain and interpreter of host_service.claude_bin. I6, T1 (SR10) and T2 stay unchanged. Supplemented by D-W-62 (Ruling T13, no shared locations) and D-W-63 (reported names). Findings K3-01 and K3-06, third round of the same error class after K2-19: An oracle in the account of a shared session user could be forged or (macOS without host_service.claude_bin) was switched off, and a subscription login in it was used by every later session of other people. Separate accounts make "no other person's subscription login" true independently of any check. The proposal from K3-01 to treat the empty value under macOS as an error is rejected: It closes only this instance. 24 2.2, 24 7.4, 07 SR12, 03 host_session_users, ADR 0068, openapi.yaml confirmHost, updateHost, Threat model T-TM-08, T-TM-11, RISKS RK-22, RK-84, PLAN M2-39 to M2-45, M2-48, TM-T28, AZ-M61
D-W-58 run.token.renew checks "not consumed" as runs.started_jti IS DISTINCT FROM jti (NULL counts as not consumed); after the start AG-32 expects 409 already_started, because step 2 (state) checks before step 3 (jti), and only after running -> waiting_input 403 invocation_token_invalid with reason: superseded. Findings K3-09 and K3-03: In SQL, jti <> started_jti yields NULL for NULL (run never started yet) and would have rejected every renewal before the first start; AG-32 contradicted the check order from 4.5.1, AG-29 and 04 15.3. 09 4.5.1, 04 15.3, 05, AG-32, D-W-44
D-W-59 Portability test: The options after sh -s -- from 16 3.1 are normative, not the URL. The source of the script is a parameter (PORTABILITY_INSTALL_SOURCE): in the private phase gh release download <tag> (install.sh and bundle) with the job's token and the new option install.sh --bundle <file>, in the public phase the documented curl command. release.yml calls portability.yml (workflow_call) after a successful publish and only for product tags vX.Y.Z; server tags do not start it, a server version is checked by the product release that installs it. runners-check allows only workflow_call and workflow_dispatch for portability.yml; in both start types the job checks out refs/tags/<tag>, so that test code and release match. The public path is proven by public-switch.md step 21. Current state: Only one job of release.yml calls portability.yml, without a matrix (D-W-73); the interface of make portability and the place of the options are in 16 11.1 (D-W-74). Current state: The repository is public from the first push (D-W-92); portability.yml uses only the source url and does not log in to the registry. Finding K3-02: GitHub shows releases only with read permission, the anonymous curl fails in the private phase; at a start by tag push, install.sh did not yet exist under the tag, and never under server/v*; OWNER is replaced only by V1-03. This way M7-14 can turn green in the private phase. workflow_call instead of workflow_run: The tag comes from github.ref_name of the same run, and the test starts only after publish. 16 3.1, 16 11.1, M7-14, M7-29
D-W-60 tools/docs/register_ids.py no longer checks only the three registers, but every table in docs/**/*.md: If the first column holds exactly one ID (prefix with -, number at the end, for example R-AG-22, T-SC-05, SEC-TERM-01, ID-T01, M1-23, D-W-01, RK-82, OQ-07, plus decision IDs with a word instead of a number such as D-R-STACK), that is its definition, and it may appear this way only once in the whole repository. A table that only names IDs puts them in a later column. Short local numbers without - (M1, D1, W1) are not IDs in this sense. Current state of D-W-49: extended by this ruling. Current state: also checks the cell count of every table row (D-W-71). Finding K3-05: Duplicate IDs occurred for the second time (RK-82 in RISKS, R-AG-22 twice in 09 at efca7f7); D-W-49 covered only the registers. Chapters and log cite IDs without a file (SPEC 13 (Change rule) item 4, rule 1 of this log), so uniqueness applies to the whole repository. The test keeps the old 09 state as a fixture. tools/docs/register_ids.py, tools/docs/tests/test_register_ids.py, gates.md, D-W-49
D-W-61 The rule release-untrusted-output in tools/ci/check_runners.py is an allow-list: Outside of if:, a job with environment release* may use, in an expression ${{ }}, only needs.<job>.result and needs.<job>.outputs.<name> of another job with environment release*. Every other form of needs is a finding, including object filters (needs.*.outputs.x), toJSON(needs.resolve), index access (needs.resolve['outputs'].path) and other spelling (NEEDS). Current state of D-W-52: enforcement tightened by this ruling. Current state: Only the if: of the job and the if: of each step are exempt; a key if elsewhere and keys themselves are checked (D-W-72). Current state: the rule recognizes release jobs in every spelling of the environment name, an expression as the name is a finding (D-W-110). The deny-list recognized only needs.<job>.outputs and toJSON(needs); valid expressions such as filters and index access passed. An allow-list also excludes forms that nobody thought of. tools/ci/check_runners.py, tools/ci/tests/test_check_runners.py, D-W-52
D-W-62 Ruling T13 of the lead (after critique round 4, 2026-10-11): On a host there is no location that one session user can write and another can read or execute, if it carries code, settings or logins. host_service.claude_workdirs names the working directories per session user; each belongs to exactly one (mode 0700, no folder above it writable by others); the index of the working directory (workdir_index, claim wdi) counts only in the list of the own session user, the host service resolves it only in the folders of osu; with organization, the CLAUDE_CONFIG_DIR of the profile claude is fixed in the home of the session user (.config/bothy-host/claude); programs belong to root or Administrators. A check HD11 checks this with one rule for every configured path and the home including its folder chain, before every PTY and periodically, and blocks only the affected session user (close_detail = shared_location); at a location for all it refuses the start. Shared code only via git. The rationale of RK-22 now names T13 as a prerequisite; new risks RK-88, threat T-TM-13, test TM-T29, control SEC-TERM-13. Current state: HD11 also checks the chain files and the ownership of the programs including their folder chain (D-W-68); the list of working directories applies after HD7 (D-W-69); terms per platform and drive root (D-W-80), program of the profile claude (D-W-81), CLAUDE_CONFIG_DIR with personal (D-W-82), path resolution (D-W-83). Shared working directories (project settings with hooks, apiKeyHelper and env, CLAUDE.md in the folder chain) and a shared CLAUDE_CONFIG_DIR connected the accounts separated by T12 again; this is the same error class as T12 at other locations. Instead of one check per location (the critique's proposal: extend HD7 and HD9), one general rule with one check applies, so that the next location does not have to be found individually again. Limit honestly named: HD11 does not see folders outside the configuration. 24 2.2, 24 5.3, 24 7.4, 16 4.4, 19 SEC-TERM-13, ADR 0068, Threat model T-TM-08, T-TM-13, RISKS RK-22, RK-88, R-TM-22, PLAN M2-42, M2-44, M2-45
D-W-63 The host service reports the names of its session users with the display names of their working directories at host.pair and with every report (hosts.reported_session_users, schema host_reported_session_users.v1); after every login it sends report before it accepts session.open. confirmHost and updateHost assign only reported names, otherwise 400 validation_failed with errors[].code = session_user_not_on_host; an assignment takes effect for SR12 only as long as its name is reported, otherwise 403 terminal_denied with reason: no_session_user. 4104 with session_user_unknown remains for a token with a name that the host did not report in its last report. The status number follows 06 (400 for rule violations, 422 only for idempotency_key_reuse), not the critique's proposal 422. The server did not know the names; a typo by the owner was noticed only on opening, as a hard tampering warning 4104, contrary to R-TM-16, and the selection in screen A8 had no source. 24 2.2, 24 5.3, 07 SR12, 03 hosts, 01 A8, openapi.yaml HostReportedSessionUser, pairHost, confirmHost, updateHost, asyncapi.yaml hostControl, R-TM-23, R-DM-41, TM-T28 (e), PLAN M2-39, M2-41, M2-43, M2-44, M4-37
D-W-64 On a violation, HD6 and HD11 give the name of the affected session user in hosts.reported_doctor (field session_user); the console also shows the person of its assignment. The standstill of a host through the subscription login of a single person stays (Ruling T2 unchanged) and is recorded as threat T-TM-14 and risk RK-89 with an instance operator step in 16 4.4. Current state: one entry per affected session user (D-W-70); the second instance operator step takes the session user out with session-user remove instead of only locking it (D-W-67); a chain file in a shared folder on the path is a second way to the standstill (D-W-80); an open home of a session user is not one (D-W-88); the names reach the console also on a hit at start (D-W-91); a long-running process that reopens the home of a former session user after remove is a third path, and HD9 names this user; one rule for names applies to every check that a person can make fail, also to HD10 (D-W-109). Since T12 a subscription login affects only the account of its person, but a hit of HD6 brings the whole host to a standstill, and doctor did not name the account. The name is no secret; the server knows it from the assignment. 24 2.2, 24 7.4, 16 4.4, openapi.yaml HostDoctorCheck, R-TM-14, TM-T21
D-W-65 The session user of a dev environment is created by provisioning; only an owner with host.manage (step-up, reason, audit) can assign it, when confirming the host. There is no system action for it. 21 let provisioning assign, but only host.manage assigns, and that requires owner, step-up and reason. A system action that grants access to a host without an owner would be a new way around the permission model; the confirmation needs the owner anyway. 21 7.1, R-DV-07, DEV-T08, PLAN V2-07
D-W-66 terminal_sessions references host_session_users(id, host_id, user_id) with the composite foreign key (host_session_user_id, host_id, user_id) (for this, unique (id, host_id, user_id)). A foreign key only on id allowed a session user of another host or another person; a code error would then write someone else's osu into the token, and the host service would bind a new name to the wrong person. 03 terminal_sessions, R-DM-40, ERD part 7, PLAN M2-39
D-W-67 bothy-host session-user remove <name> (with administrator privileges) removes the name from host_service.session_users and its entry from host_service.claude_workdirs, removes the service of its PTY helper and restarts the host service, which reads the list only at start. The binding of the name to its person stays in the state directory, and session-user add rejects a bound name (T12). Current state: remove also closes the home (D-W-88), add also restarts the host service (D-W-90); after remove the instance operator deletes the user together with its long-running processes, locking alone is not enough (D-W-109). The instance operator ends the standstill through HD6 by removing the login or, after the owner has ended the assignment, by taking the session user out this way; locking the user alone is not enough. After the revocation of a person who is not an owner, the console advises taking their session user out and locking or deleting it. Finding R5-02: The second way from D-W-64 (end the assignment and lock the user) did not end the standstill by its own rule. HD6 runs in the PTY helper of every name from host_service.session_users, whether it is assigned or locked on the host, and there was no command that removes a session user. 24 2.2, 24 6.5, 24 7.4, 16 4.4, Threat model T-TM-14, RISKS RK-89, R-TM-24, TM-T21, PLAN M2-42, M2-45
D-W-68 HD11 checks the one rule from 24 2.2 item 6 for two kinds of locations. Location of a session user: the location, every folder on the path and, in these folders, the chain files (CLAUDE.md, CLAUDE.local.md, AGENTS.md, .mcp.json and the folders .claude and .git with their content, that is, what the coding agent and git load from the folders above the working directory) belong to no other session user and are writable by no other; checked before every PTY and periodically. Location for all (programs): the location and every folder on the path belong to root or Administrators and are writable by no session user, as with HD10. The limit is now stated correctly: An entry that another session user creates while a location is open is found by HD11 only at the next start of the service or call of doctor; a file that a later version of the coding agent additionally loads from the folder chain, only after the list is extended. Current state: "root or Administrators" now means trusted owner per platform (D-W-80); every path is resolved before the check (D-W-83). Findings R5-03 and R5-04. R5-03: 2.2 required ownership by root or Administrators for programs, HD11 only "no session user"; the stricter rule is chosen, because D-W-62, R-TM-22, 16 4.4 and HD10 already name it and HD11 cannot check the permissions of another account. R5-04: A CLAUDE.md in a folder above the working directory that belongs to another person passed HD11, although the rationale of T13 names exactly this file. A rule for all entries of these folders does not work, because the home folders of the other session users are in /home; hence a fixed list with sources (24 2.2), which M2-42 checks against the current documentation. 24 2.2, 16 4.4, 19 SEC-TERM-13, ADR 0068, Threat model T-TM-13, RISKS RK-88, R-TM-22, TM-T29 (f) and (h), PLAN M2-42
D-W-69 The host service reads host_service.claude_workdirs, like host_service.session_users, only at start. A folder that is listed there for two session users drops out of both lists (HD7). pair and report report the lists after HD7, and the host service resolves wdi in exactly the reported list of osu. Finding R5-05: It was open which entry drops out for a duplicate folder and whether the reported list is the one before or after HD7. If the server counted the index in a different list than the host service, a person would get a different folder or 4104 with profile_unknown without any attack, contrary to R-TM-16. Out of both lists, because neither side is recognizably the right one. 24 2.2, 24 5.3, 24 11.1, 03 hosts, openapi.yaml HostReportedSessionUser, TM-T29 (c), PLAN M2-42, M2-43, M2-45
D-W-70 hosts.reported_doctor is a list (default '[]', like doctor in the API). If HD6 or HD11 fails, it holds a separate entry with its name for each affected session user; check and name together are unique. Console and audit name each of these session users. Current state: one rule for names applies to every check that a person can make fail; so HD10 with a wrong peer and HD9 with a former session user also name the account (D-W-109). Finding R5-08: The data model provided one entry per check with exactly one name. HD6 can trigger in several accounts at once and then brings the whole host to a standstill; the instance operator must know every account (D-W-64). The default '{}' also did not match the list in the API. 03 hosts, 24 2.2, 24 7.4, 24 10.1, openapi.yaml HostDoctorCheck, 01 A8, 20, TM-T21, PLAN M2-42, M4-37
D-W-71 tools/docs/register_ids.py additionally checks that every row of a table in docs/**/*.md has as many cells as the header of its table. It counts cells like Python-Markdown: \| and | in code do not split. T-TM-13 and T-TM-14 are now in register 6.7 of the threat model. Finding R5-01: Two rows of the seven-column register 6.7 stood in the five-column STRIDE table 5.16; the renderer dropped the extra cells without a warning, tests and residual risk of both threats were missing, and register_ids.py found nothing, because the IDs were unique. The check keeps the state 4a9ced3 as a test case and counts all rows of the repository the same way as Python-Markdown. tools/docs/register_ids.py, tools/docs/tests/test_register_ids.py, gates.md, Threat model 6.7, D-W-60
D-W-72 release-untrusted-output skips only the if: of a job (jobs.<id>.if) and the if: of its steps (jobs.<id>.steps[*].if). Every other key if (for example in env of the job or of a step, or in with) is an ordinary value and is checked. The rule checks keys as well, without settling whether GitHub evaluates expressions there (cautious, as with needs in strings). Current state of D-W-61: made more precise by this ruling. Finding C4-03: The exception applied to every key if at any depth. A value of the job resolve in env: {if: ...} reached the shell of a job with signing secrets as $if, and in with: {if: ...} an action, without a finding. GitHub evaluates only the two conditions, without passing the value to a shell or action. tools/ci/check_runners.py, tools/ci/tests/test_check_runners.py, gates.md, ADR 0039, Threat model T-SC-05, R-QA-12, D-W-61
D-W-73 New rule reusable-release-caller in tools/ci/check_runners.py: A release workflow (RELEASE_WORKFLOWS: release.yml, portability.yml) is called only by release.yml, per target from exactly one job and without strategy. Every form of uses with .github/workflows/<file> is recognized (./, $/ and <owner>/<repo>/...@<ref>), because the check does not know the name of its own repository. A manual start (workflow_dispatch) stays allowed. Current state: a start by workflow_dispatch from a workflow is blocked by the rule actions-write (D-W-111). ADR 0039 and D-W-59 named release.yml as the only caller, but only the triggers of portability.yml were checked. A pull request workflow with uses: ./.github/workflows/portability.yml stayed without a finding and would have started, on every pull request, a release test that belongs to no release of this run. The same gap existed for a second call and for a matrix on the calling job, which GitHub allows ("Jobs using the matrix strategy can call a reusable workflow", https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows, retrieved 2026-10-11) [verified]; therefore the rule applies to all three ways. tools/ci/check_runners.py, tools/ci/tests/test_check_runners.py, ADR 0039, gates.md, 23 4, D-W-59
D-W-74 The interface of make portability is in 16 11.1: PORTABILITY_TAG switches on the portability test from tests/portability; without it the target stays the smoke test from M1-73; PORTABILITY_INSTALL_SOURCE (url or release-download) chooses the source of install.sh; with release-download, PORTABILITY_RELEASE_DIR names the folder with install.sh and bundle. An invalid value aborts before the installation. The options after sh -s -- are stated exactly once in tests/portability/install-options, created by M7-29, read by M7-14; the check from M7-29 compares this file with 16 3.1. With url, the test builds the URL from GITHUB_REPOSITORY and the tag, not from the documentation. Current state: the address from which install.sh downloads the bundle is set by release-build (D-W-95); the test itself shows the logs on failure (D-W-96); an invalid value aborts before the test downloads, pulls or installs anything, because it pulls and checks the server images before install.sh (D-W-99). portability.yml passed PORTABILITY_TAG and PORTABILITY_RELEASE_DIR, which were described nowhere, and M7-29 required the options in portability.yml, where they are not. M7-29 creates the file, because it is built before M7-14 and its check would otherwise have nothing to compare. Without PORTABILITY_TAG the smoke test stays, because M1-73 and the exit of M1 use make portability that way. The URL is built from GITHUB_REPOSITORY, because the last product tag in public-switch.md step 21 may have been created before V1-03 and then still names OWNER. 16 11.1, 16 3.1, portability.yml, 23 3, PLAN M7-14, M7-29, D-W-59
D-W-75 install.sh takes BOTHY_VERSION (the server version) unchanged from .env.example of the bundle and does not write its own version into it (16 3.1 step 4). The promotion follows from this: A server beta server/vX.Y.Z-beta.N is checked by the portability test of a product beta vA.B.C-beta.M whose bundle sets BOTHY_VERSION to it; the numbers of the two tags are unrelated. Current state: Who writes BOTHY_VERSION and BOTHY_REGISTRY into the bundle and when the build aborts is set by 18 2.1 (D-W-76). Finding C4-13: environments-and-flags.md 1.3 coupled server beta and product beta through the same placeholders, although every component has its own version (18 section 1). In the process it turned out that 16 3.1 step 4 wrote .env "with the version" without saying with which; with the version of install.sh (product) the installation would pull a server image that does not exist under this version. 16 3.1, environments-and-flags.md 1.3, PLAN M7-29
D-W-76 The product release writes server version and registry into the bundle (18 2.1, R-RL-09). The server version is the line BOTHY_VERSION= in infra/compose/.env.example of the tagged commit; make release-prep COMPONENT=bothy SERVER_VERSION=<version> writes it only if the tag server/v<version> exists. make release-build takes it over and writes BOTHY_REGISTRY=ghcr.io/<owner> (lowercase, from GITHUB_REPOSITORY_OWNER) into the .env.example of the bundle. It aborts without a bundle on 0.0.0-placeholder or a version without SemVer form, on a server pre-release in the stable product, without GITHUB_REPOSITORY_OWNER, on OWNER in the result and when an image from compose.yaml with ${BOTHY_VERSION} is missing from the registry. M7-29 takes on the build; M7-10 takes on make release-prep (ADR 0060), which no ticket built so far; 18 7 rule 4 names the tag pg18-<version> of bothy-postgres, which so far only compose.yaml named. 16 11.1 rule 3 now says only for the URL of install.sh that the point in time before or after V1-03 makes no difference. public-switch.md step 21 makes the packages bothy-server and bothy-postgres public before the test; portability.yml logs in to the registry only in the private phase, so that the test in the public phase proves the pull without login (16 11.1 step 1). Current state: a server pre-release in the stable product is already rejected by release-prep and the gate release-pins (D-W-98); install.sh gets the download address from the build (D-W-95). A reviewer after D-W-75 found: D-W-75 made the value in the bundle load-bearing, but nothing wrote it; make release-build packed the template with ghcr.io/OWNER and 0.0.0-placeholder unchanged, so every installation and M7-14 would pull an image that does not exist. The version in the commit instead of as a build argument, so that the tag alone determines the bundle and the release commit shows the version for review; a second file such as SERVER_VERSION next to the template would be a second source. The registry from the build, because it depends on the location of the repository (fork, V1-03). The check reads the images from compose.yaml, so that a new image is not forgotten. A newly published package in the container registry is private, and a linked package inherits "the access permissions (but not the visibility)" of the repository (https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility, retrieved 2026-10-11) [verified]; without its own step the public installation without login would fail, while the portability test with the job's token would stay green. 18 2.1, 16 3.1, 16 11.1, 18 7, 23 3, ADR 0060, release.md, environments-and-flags.md 1.3, configuration.md 9, public-switch.md, portability.yml, .env.example, PLAN M7-10, M7-14, M7-29, D-W-75
D-W-77 Ruling T14 of the lead (after critique round 4, 2026-10-11): The preview images terminal-* and viz-* apply the design system accepted by the owner (same tokens, same rules) and fall under its acceptance; there is no open owner question about them. They were created after the acceptance of 2026-10-10: both pairs on the same day with D-W-13, viz-* regenerated on 2026-10-11, last with D-W-55. OQ-23 is closed and is listed with its ID under "Decided"; the ID is not reassigned. The row on the seven regenerated images (D-W-46) names as its source the owner decision "Design accepted on docs/design/previews" with the day on which planning received it in this version (2026-10-11), instead of "Owner, 2026-10-10"; D-W-46 and D-W-55 name the images accepted without reservation. Finding C4-10: The owner decision applies to the folder docs/design/previews, in which terminal-* and viz-* lie; OQ-23 nevertheless kept it as an open question and said without evidence that the owner had not seen them. The seven images were created only on 2026-10-11; an owner statement dated 2026-10-10 about them was an inference of planning. Open questions, Decided, design/previews, D-W-13, D-W-46, D-W-55
D-W-78 The ranking "The five biggest risks" in RISKS.md follows its three criteria with a fixed reading. Criterion 2 means: A later fix does not undo the consequence (lockout, publication, leak, content, knowledge or a shell with someone without a permission for it, legal violation); outages, breaks at interfaces, effort and restrictions do not meet it. Criterion 3 measures the remainder by the "Residual risk" level of the threat model and, at the same level, by its reach. It follows: RK-77 (high, high) is at rank 2, RK-02 falls behind rank 5; every risk with the same rating as ranks 3 to 5 is named with the deciding criterion. A new tool tools/docs/risk_ranking.py in make docs checks criterion 1: same rating as in the register, ranks without a gap, no rising rating, no higher-rated risk outside the table. Criteria 2 and 3 remain a judgment call of the maintenance at the end of every milestone. Current state: Criterion 3 has a rule for risks without an entry in the threat model, and the tool also checks the list behind the last rank (D-W-79). Finding C4-14: Rank 5 (RK-07) was justified only against RK-02. The critique proposed placing RK-33 and RK-49 before RK-07 by criterion 2; this is rejected, because knowledge with another audience group is also irreversible and RK-33 and RK-49 have less remainder by criterion 3 (T-BK-02 and T-LG-01 low). The check showed a second error in the same ranking: RK-77 has been at high and high since the history scan, while the list kept saying RK-01 was the only such risk. Ratings change at every maintenance; only a test keeps criterion 1 and register in line. RISKS, tools/docs/risk_ranking.py, gates.md, RK-07, RK-77
D-W-79 Criterion 3 of the ranking in RISKS.md also applies to risks without an entry in the threat model: The rationale names a level on the same scale with a reason, and it counts like a level of the threat model (RK-77 and RK-50 low). Besides the remainder in the server, T-AI-08 lists the remainder in the adapter with medium: The evidence of the reported mode with Hermes Agent depends on a non-public interface (RK-13); with that, rank 3 follows from the fixed reading. tools/docs/risk_ranking.py additionally checks the list after the last rank (line **Behind rank <n>** up to the next heading): It carries the number of the last rank, and the IDs in the bold start of its list items are exactly the risks outside the table with the rating of the last rank, each once. Which criterion decides between risks of the same rating remains a judgment call of the maintenance. Findings RT-C4R-02 and RT-C4R-03 (reviewer after D-W-78): T-AI-08 rated only the remainder in the server, the rationale of rank 3 therefore deviated from the fixed reading, and a rule was missing for risks without an entry in the threat model (RK-77, RK-50). The list behind rank 5 named its risks by hand; a risk raised to this rating left make docs green. This is the same error class as RK-77 before D-W-78 (rating changed in the register, ranking not updated); therefore the same tool with tests now holds it. RISKS, Threat model T-AI-08, tools/docs/risk_ranking.py, gates.md, RK-13, RK-50, RK-77
D-W-80 HD7, HD10 and HD11 use fixed terms per platform (24 2.2 item 6). Trusted owners: Linux and macOS root, Windows SYSTEM, Administrators and NT SERVICE\TrustedInstaller; every other account does not count, not even that of a person with administrator privileges. "Writable" means, for files, changing or deleting content, attributes, permissions or ownership, for folders additionally creating, deleting or renaming entries: under Linux and macOS w from the mode, under Linux also from the ACL; under macOS in addition the ACL permissions write and append (files), add_file, add_subdirectory, delete_child (folders), delete, writeattr, writeextattr, writesecurity, chown; under Windows FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY, FILE_DELETE_CHILD, FILE_WRITE_DATA, FILE_APPEND_DATA, FILE_WRITE_ATTRIBUTES, FILE_WRITE_EA, DELETE, WRITE_DAC, WRITE_OWNER for the session user or a group in its token; entries only for inheritance do not count for the folder itself. Current state: permissions for files and attributes (D-W-86). Under Windows, bothy-host install, after confirmation by the instance operator, removes the default entry Authenticated Users:(AD) at the root of the system drive and of every drive with a location; install --drive-root <drive> performs this step for a drive added later. Without confirmation HD11 reports drive_root_writable and doctor refuses the start. The command leaves the inheritable entry Authenticated Users:(OI)(CI)(IO)(M) in place. Current state: The command writes the DACL of the root without propagation to the folders below (D-W-87). A chain file of a person in a shared folder on the path is a second way to the standstill (T-TM-14, RK-89). Since then, the terms "root or Administrators" in older rows of this page mean a trusted owner. On a Windows host with default permissions HD11 never passed, because C:\ allows all logged-in users to create folders and the system folders belong to TrustedInstaller; if "writable" was read without creating, a person could lock out every other person with C:\.git, the pattern of the gap in Git for Windows (sources in 24 2.2). The chosen option is removing only (AD): It applies to the root alone and changes nothing below. Windows would propagate the removal of the inheritable entry to every existing folder below and take write permissions from programs; the folders that inherit it are found by HD11 when they lie on the path to a location. The owners of the system folders under Windows are only assumed; M2-42 measures them before the build. 24 2.2, 16 4.4, 19 SEC-TERM-13, ADR 0068, Threat model T-TM-13, T-TM-14, RISKS RK-88, RK-89, R-TM-25, TM-T29 (f) and (i), PLAN M2-42
D-W-81 With organization, the profile claude starts exactly the resolved path of host_service.claude_bin, with the fixed search list of HD6 at the front of PATH; if the value is empty, the host service does not offer the profile (HD7, warning). Under Windows the value is an .exe. With personal it starts claude through the login shell of the holder; they choose program and folder, because only they get sessions there (I6). host_service.claude_bin is listed as the program of the profile among the locations for all. Current state: If the search list does not find the interpreter of a script, the value counts as empty for HD6 and the profile; the search list starts with the configured folder, and HD10 checks each of its folders (D-W-85). T13 justifies the locations for all with the fact that a program runs in every session user, but did not fix the program of the profile claude; a claude from the PATH could lie in a folder that belongs to another account, and neither HD10 nor HD11 checked it. With a fixed program, HD10 checks exactly what runs. A batch file under Windows carries no #!; its interpreter would remain unchecked. 24 7.4, 24 11.1, 16 4.4, R-TM-26, TM-T28 (d), TM-T29 (f) and (l), PLAN M2-45
D-W-82 With personal, the CLAUDE_CONFIG_DIR of the profile claude from the login shell of the holder must, after resolution, lie in the home of their session user; without a value, the default folder in the home applies. The PTY helper queries the value before every start in a separate login shell and sets it in the start command back to exactly this value. If it lies outside, the profile does not start (open_failed with shared_location); the profile shell remains. 24 2.2 item 6 counted this folder among the locations of a session user, but HD11 did not know it with personal. Outside the home and readable by others, the holder's subscription login reached long-running processes of other session users and, after a switch to organization, other people. In the home, the check of the home by HD11 protects it. Limit: The value comes from the holder's startup files; the check catches a misconfiguration, not the holder themselves. 24 2.2, 24 7.4, R-TM-26, TM-T29 (k), PLAN M2-45
D-W-83 HD7, HD10 and HD11 fully resolve every path before the check (Linux and macOS like realpath, Windows GetFinalPathNameByHandle). HD10 and HD11 check every folder of the resolved chain, every component of the path as it is configured, and every link that the resolution follows, together with its folder; a link counts like an entry of its folder and like its target, also as a chain file. HD7 compares working directories by their identity in the file system (device and inode, under Windows serial number of the volume and file ID). Profile and HD6 use the resolved path. Current state: who resolves, what applies to a path that cannot be resolved, and the 128-bit file ID under Windows (D-W-84). Whether the checks resolve paths was stated nowhere. A check without resolution would let through a link owned by root that points into a folder of another person, and HD7 did not recognize the same folder under two spellings as a duplicate. The comparison by identity covers links and upper and lower case in one step. 24 2.2, R-TM-25, TM-T29 (j), PLAN M2-42
D-W-84 HD7, HD10 and HD11 resolve every path with the user who is allowed to open it: the locations of a session user (home, CLAUDE_CONFIG_DIR, working directories) with its PTY helper, the locations for all with the service user. For HD7, each helper reports to the host service over the local channel, for each working directory of its list, only that directory's identity (device and inode, under Windows serial number of the volume and 128-bit file ID from FILE_ID_INFO); the host service compares the values. A path that cannot be resolved (it is missing, a link points nowhere, a folder on the path is closed) counts as not present: A working directory drops out of the list of its session user, a profile command is dropped (HD7, warning), host_service.claude_bin counts as empty for HD6 and the profile claude, a CLAUDE_CONFIG_DIR with personal counts like one outside the home (shared_location), and a home that cannot be resolved makes HD11 fail for its session user. Under T13, the locations of a session user are accessible only to that user; with mode 0700, neither the service user nor the helper of another user can resolve them, and it was open whether such a folder drops out of the list, HD11 fails or the folder stays unchecked. A folder that is never offered connects no accounts; blocking the whole session user would be more than necessary. A link that points nowhere must not count as present, because someone else can create its target later. According to Microsoft, the 64-bit file ID is not unique on ReFS; the 128-bit ID from FILE_ID_INFO is (sources in 24 2.2). 24 2.2, 24 7.4, 16 4.4, R-TM-25, TM-T29 (j) and (k), PLAN M2-42, M2-45
D-W-85 The search list of HD6 and of the profile claude starts with the folder in which host_service.claude_bin is located according to the configuration (resolved), not with the folder that a link points to; HD10 checks every folder of the search list. If the search list does not find the interpreter of a script, host_service.claude_bin counts as empty for HD6 and the profile (HD10 still checks file and folder): HD6 warns with auth_status_unchecked, and with organization there is no profile claude (HD7, warning). Until now, in this case only HD6 called nothing. The profile started anyway, and because its PATH continues after the search list, #!/usr/bin/env node found an interpreter that HD10 never checked, for example in the folder of a package manager owned by another account; this interpreter would run in every session user. A PATH made only of the search list would close this as well, but would take the person's other tools away from the coding agent [derived]. The configured folder is unambiguous and already lies on the path that HD10 checks; since paths have been resolved, it was open whether it or the folder of the target is meant. HD10 checks every folder of the search list, so that nobody places another interpreter in front of the checked one. 24 7.4, 24 2.2 HD7 and HD10, 24 11.1, 24 17 host_service.claude_bin, R-TM-26, TM-T27, TM-T29 (l), PLAN M2-42, M2-45
D-W-86 "Writable" under macOS also covers the ACL permissions write and append for files and writeattr and writeextattr for all entries, under Windows also FILE_WRITE_ATTRIBUTES and FILE_WRITE_EA; for files, the general rule names changing attributes. The list per platform is what the build implements. Without append, an ACL would pass with which one person appends instructions to a CLAUDE.md above the working directory of another; under Windows the counterpart FILE_APPEND_DATA was already in the list. Attributes count as well, because no source says which of them are without consequence for loading and starting: One permission too many only blocks, one too few opens a location. The same applies under Windows, so that both platforms have one rule; that the default permissions of C:\Users, C:\Program Files and C:\Windows\System32 give these permissions to no session user is assumed and is measured in M2-42. 24 2.2, R-TM-25, D-W-80, PLAN M2-42
D-W-87 bothy-host install (also with --drive-root) removes Authenticated Users:(AD) by rewriting the DACL of exactly the drive root with SetFileSecurityW, with all other entries in the same order and with the same control bits; never with SetNamedSecurityInfo or SetSecurityInfo. The instance operator removes the entry only with this command. TM-T29 (i) compares the security descriptor of every direct subfolder before and after, among them a test folder that a propagation of the inheritance would change. According to Microsoft, SetNamedSecurityInfo propagates the inheritable entries to all existing children every time a DACL is set and applies the inheritance model to everything below; at C:\ that would be the whole system drive, with reordered entries, and the promise from 16 4.4 that no permission below changes would not hold. For SetFileSecurityW the documentation says explicitly that nothing goes to the children; Microsoft calls it obsolete, but the behavior is documented only for this call (sources in 24 2.2). A test folder with an inherited entry before one of its own makes a propagation visible in the comparison, which on a fresh system would otherwise leave hardly any traces [derived]. 24 2.2, 16 4.4, R-TM-25, D-W-80, TM-T29 (i), PLAN M2-42
D-W-88 HD9 checks only home folders of users who are not session users (including that of the service user and that of a former session user), still with "Start refused"; the output of bothy-host doctor on the host names the user, only the code goes to the server. The home of a session user is checked only by HD11, as that user's location: If it gives another session user read or execute permission, only this session user gets no sessions, and the message names them. bothy-host session-user remove closes the home of the user that it takes out (mode 0700, under Windows an ACL only for that user and the trusted owners); install --session-user and session-user add create the home this way. Current state: a long-running process of a former session user can reopen its home after remove; the message to the server therefore names this user, and the instance operator deletes the user afterwards (D-W-109). HD9 and HD11 checked the same state with different consequences. If a person opened their own home, HD9 triggered in the helper of every other session user and, at the next start, refused the start of the whole host without naming the person; such a start also follows session-user remove. A person could bring the host to a standstill this way, and the consequence of HD11 for the home never took effect. An open home is an error in one account, and the consequence now affects only that account. The home folder of a user who is not a session user cannot be changed by any person with a terminal; remove closes the home of a former session user, because HD9 checks it afterwards. 24 2.2 items 3 and 4, HD9, R-TM-17, R-TM-24, 16 4.4, ADR 0068, Threat model T-TM-04, T-TM-14, RISKS RK-89, TM-T21 (d), TM-T24, TM-T28 (d), PLAN M2-42
D-W-89 Consequences of HD11 during operation: A violation at a location for all ends all running sessions with host_policy_violation and close_detail = shared_location and allows no new ones until HD11 passes; the message names no session user. The check of the entries runs per session user in its PTY helper; at start, only this session user waits for its run (code entry_scan_running), on command the previous result applies until the end. host_service.entry_scan_timeout limits a run; after that, HD11 fails for this session user (entry_scan_timeout). A finding of this check, including an abort, is lifted only by a later full run (start or bothy-host doctor), never by a periodic one. bothy-host doctor performs the full run in a running host service; if it finds a violation with "Start refused", except for HD6, HD10 and HD11, the host service exits as with a refused start. Current state: with entry_scan_timeout the person cannot reach their home without a session; the instance operator raises the value or makes the home smaller themselves (16 4.4 item 6). HD11 runs before every PTY and periodically, but for a location for all only "Start refused" was stated; HD10 governed operation, HD11 did not. The check of the entries runs only at start and on command, but the block applied "until HD11 passes", and whether a periodic run lifts it was open; a periodic run does not see the entry and therefore must not declare it settled. Its duration depends on the size of the home; without a limit, the whole host would wait for the largest home. An aborted run has not seen every entry and therefore does not count as passed; the consequence affects only its session user. 24 2.2 HD11 and "check of the entries", 24 17 host_service.entry_scan_timeout, R-TM-28, 16 4.4 item 6, RISKS RK-90, TM-T29 (e) and (f), PLAN M2-42, M2-45
D-W-90 bothy-host session-user add restarts the host service like remove; afterwards the host reports the name with the next report, and the owner assigns it without a further step. Every other change to the configuration of the host service (working directories, profiles, host_service.claude_bin) takes effect after bothy-host restart. Every restart of the host service ends all sessions of the host: Every PTY helper ends its PTYs as soon as its channel to the host service breaks off; on the control connection, identify names the sessions that the host service still knows (live_sessions), and the server ends every other session of the host in active or detached with the new close_reason host_restarted. The host service reads its configuration only at start. After add, the server rejected the new name with session_user_not_on_host until someone restarted by hand, and for working directories there was no way. What a restart does with running sessions of other people was open, and TerminalCloseReason had no matching value. Letting the PTYs keep running was rejected: The new process checks everything again at start and reads the configuration again; a session from the time before would keep running unchecked under the old configuration, for example in a removed working directory, and its scrollback buffer lay in the old process. The list in identify needs no state in the server and distinguishes a restart from a network drop. 24 2.2 items 3 and 5, 24 6.2, 24 10.3, R-TM-27, 16 4.4, 03 terminal_sessions, openapi.yaml HostIdentify, TerminalCloseReason, asyncapi.yaml hostControl, ADR 0068, TM-T16, TM-T21 (f), PLAN M2-42, M2-43, M2-45
D-W-91 bothy-host doctor prints on the host, per check, the result and code, for HD6 and HD11 the names of the affected session users, for HD9 the user and for drive_root_writable the drive, never content or values; the exit code is not 0 as soon as a check fails. If doctor refuses the start at start, the host service starts no PTY and accepts no session, but reports first if it is paired: login, one report, then it closes the connection and exits with its own exit code, after which the service manager does not restart it by itself; after the fix, the instance operator starts it with bothy-host restart. Ruling T2 stays: On a contradiction with the declaration there is no session. The promise that the console names every affected session user applied only to a hit during operation. If HD6 triggered at start, for example after a restart of the host or after session-user remove, there was no report; the console showed the host only as disconnected, with the result from the time before, and what doctor prints on the host was not defined. A restart by the service manager fixes none of these violations and only logged the host in again and again in a loop. 24 2.2 "Output on the host" and "Message on a refused start", 24 7.4, R-TM-29, 16 4.4 items 2, 3 and 6, openapi.yaml HostReport, asyncapi.yaml hostControl, Threat model T-TM-14, RISKS RK-89, TM-T21 (g), PLAN M2-42, M2-43
D-W-92 Owner, 2026-10-11: The repository is public from the first push, and every workflow job runs only on GitHub-hosted runners with a fixed label; there are no self-hosted runners. check_runners.py rejects every other runner (rule runner-not-github-hosted); the switch in runs-on and everything that served only it (rules, named values, guides) is removed. Public-only jobs keep their condition (R-QA-04). Contributions flow in one after another as pull requests into a public repository, checked by CI, SonarQube Cloud and CodeRabbit; the standard runners are free there, and a self-hosted runner would run code from other people's pull requests. ADR 0039, public-switch.md, R-QA-03, R-QA-16
D-W-93 Owner, 2026-10-11: The first push is a single commit without parents with the checked state (plan M0 and M1-01); the planning history stays private and is not rewritten. M0-08 (git filter-repo) is only needed if a history is to be published after all. Before that, the deny list and gitleaks run in a fresh clone of only this commit. A commit without history contains no old file versions, messages or identities that a rewrite would have to find and replace. public-switch.md A, PLAN M0-08, D-W-36, RK-77
D-W-94 Owner, 2026-10-11: All repository content is English: every Markdown document (specification, ADRs, ruling log, plan, risks, open questions, roadmap, design, quality, operations, observability, compliance, security, build briefs, and the READMEs of modules, services, SDKs, plugins and apps), diagram labels, code comments, docstrings, tool messages, templates, test names and test messages. German stays only in proper names, in the verdict tokens Urteil: JA and Urteil: NEIN that the review gate reads (D-W-32), in product strings of the de locale that a test holds verbatim (marked with language-check: off and language-check: on), and in the term list glossary.md (German term to English term). make docs fails on German prose (language_check.py, exceptions in language-allow.txt). Supersedes D-L5, D-API-10, D-AG-22, D-OB-07 and the language part of D-W-38; R-API-15, R-OB-09 and ADR 0001 follow. English takes precedence over every older rule that names German, as AGENTS.md (the rules for AI coding agents) states. The languages of the product interface (German and English, 01 section 9) do not change. Owner, 2026-10-11: "Almost everything in the repository is German; the instruction was English." The repository is public, and its contributors and instance operators read English. glossary.md, CONTRIBUTING.md, ADR 0001, R-API-15, R-OB-09
D-W-95 make release-build sets into install.sh, next to version and SHA-256 of the bundle, the download address https://github.com/<GITHUB_REPOSITORY>/releases/download/v<version>/; from there install.sh downloads bundle and signature (16 3.1 step 2). It aborts without GITHUB_REPOSITORY and if the finished install.sh still contains OWNER or a placeholder of the source (18 2.1 rules 3 and 4). M7-29 tests both against the fixture repository. 16 3.1 said which version install.sh downloads, but not from where; only version and checksum were built in. If the source names OWNER/bothy, the public run in public-switch.md step 21 with a tag from before V1-03 would have failed when downloading the bundle, although D-W-74 already made the URL of install.sh itself independent of that. The address comes from the build like the registry, because it depends on the location of the repository, not on the commit. 18 2.1, 16 3.1, 16 11.1, R-RL-09, R-OP-20, release.md, PLAN M7-29, D-W-74
D-W-96 The server images of an installation come only through BOTHY_REGISTRY and BOTHY_VERSION in the bundle and are pulled by tag, not by digest; this also applies to the portability test (16 11.1 step 1, M7-14). Before the start it checks the signature of every pulled digest and names the digests in the result; current state: D-W-99 fixes the path and scope of the check. Between staging and prod the same version moves, and prod compares the digests with those from staging (16 6.1 rule 2); the rest, a movable tag, is recorded as RK-91. The test itself shows the logs on failure before the cleanup, for exactly the installation of this run (16 11.1 interface rule 5); portability.yml has no log step of its own any more. 16 11.1, 16 6.1 and M7-14 named images by digest, but no step fixed a digest: the bundle pins only the version, and compose.yaml names the images by tags. Digests in the bundle would have been a second source per image next to BOTHY_VERSION, with changes to compose.yaml, install.sh and the bundle; signature check and comparison of the digests cover the moved tag, and RK-91 keeps the rest visible. The old log step called docker compose -f infra/compose/compose.yaml logs in the checkout; with PORTABILITY_TAG, however, the stack runs in the folder of install.sh under its project and is gone after the cleanup. Only the test knows folder and project, like smoke.sh in the smoke test. 16 6.1, 16 11.1, 18 2.1, 18 10, ADR 0036, environments-and-flags.md, portability.yml, PLAN M7-14, RK-91
D-W-97 Precondition portability test per tag (release.md 1 item 2): a stable server needs the green test of the product beta whose bundle pins its last beta. Server beta and product beta have no precondition from the test; their test runs after the release (for the server beta the test of the next product beta that pins it). A stable product and every other component need the test of the last product beta, as soon as there is one. The old version required, for every server release, the test of a product beta whose bundle pins this version or its last beta beforehand. For a server beta that is impossible: a bundle can pin it only once its tag exists (release-prep aborts otherwise), and before beta.1 there is no beta. Likewise, the first product beta would have required an earlier one. The test checks the beta afterwards (environments-and-flags.md 1.3), the stable version waits for it. release.md, environments-and-flags.md 1.3, D-W-75
D-W-98 That a stable product pins no server pre-release shows up before the commit, at the latest in the required check ci-ok of the pull request with the release commit, that is before the tag. The maintainer sets the tag after the merge on the squash commit on main; its push alone starts no check, because the commit is already on origin then and pre-commit runs no pre-push hook for a push without new commits (evidence in 18 2.1 rule 1). make release-prep COMPONENT=bothy rejects without CHANNEL=beta a SERVER_VERSION with a pre-release identifier and names the value, likewise a missing SERVER_VERSION while the line carries the placeholder (test in M7-29, where the option is built). New gate release-pins (tools/ci/components.py pins, part of make gates, so of make check, pre-push and CI): exactly one line BOTHY_VERSION= in infra/compose/.env.example, SemVer without v or the placeholder, and no server pre-release if VERSION carries no pre-release identifier. make release-build keeps the check as a second safeguard. So far only make release-build checked this in the release chain, that is after commit, signed tag and push; a typo cost a published product tag that cannot be taken back. release-prep knows CHANNEL and writes the line, so it checks there; the gate also covers a change by hand and runs already today, because it reads only the committed state. The gate lets the placeholder pass, because the template carries it until the first product release; release-prep and release-build reject it. 18 2.1, R-RL-09, ADR 0060, release.md, environments-and-flags.md 1.3, configuration.md 9, gates.md, push-discipline.md, 23 2, tools/ci/components.py, tools/ci/tests/test_components.py, Makefile, PLAN M7-29, D-W-76
D-W-99 The portability test pulls and checks the server images itself before install.sh (16 11.1 step 1): it reads BOTHY_REGISTRY and BOTHY_VERSION from the .env.example of the bundle of the tag (source release-download from PORTABILITY_RELEASE_DIR, source url downloaded from the download address of the release, built from GITHUB_REPOSITORY and the tag), pulls every image that the compose.yaml of the bundle names with ${BOTHY_VERSION}, and checks the signature of each of these digests with cosign as a container with a digest pin. Only then does it call install.sh; Compose takes the local images. After the installation it compares the images of the containers of this installation with the checked ones and fails on a difference. It does not check third-party images (virus scanner, object storage, observability) for a signature of the release chain. make portability thus covers steps 1 to 6 without runner and status check (16 11.1 interface rule 1). D-W-96 required the check before the start, but install.sh pulls and starts in one run without a point of intervention (16 3.1 step 5), with the source url the test did not have the bundle beforehand, and rule 1 of the interface named only steps 2 to 5. A check only after the start would have let an unchecked image run; an instance operator checks before use (release.md 5). Compose without pull_policy and build pulls only missing images (evidence in 16 11.1), so the installation starts the checked ones; the comparison afterwards catches another bundle under the same address or a second pull. cosign as a container, because the runner brings only Docker. The release chain signs only the images with ${BOTHY_VERSION}; the others are third-party images that carry no signature of the release chain. 16 11.1, 16 3.1, 18 2.1, ADR 0036, portability.yml, PLAN M7-14, RK-91, D-W-74, D-W-96
D-W-100 Decision of the owner of 2026-10-11: the briefs of milestone M2 (then M3) are written before M1 is closed, in the format of the M1 briefs, in English (P-16) and in the same brief index with waves across all milestones. Such a brief starts as soon as every ticket of its line "Depends on" is merged (P-01); it does not wait for the end of the milestone before it. It names interfaces from M1 as the M1 briefs and the code already merged name them. Each of these briefs carries under "Boundaries" the limit: if the merged code of an M1 ticket differs only in names or paths, the build session follows the code and names every difference in the report; if it differs in behavior from the specification, it ends with Verdict: BLOCKED. The build sessions of M1 run in parallel and would soon run out of work otherwise. The brief index so far planned to write the briefs of a milestone only after the previous one was closed, because they build on the delivered state; the limit catches this difference without building a deviation silently (P-11). briefs/README.md, PLAN 2, PLAN 19 P-01, P-16
D-W-101 Every brief of the terminal block (M2-38 to M2-48) has under "Fixed requirements that apply" the group "Terminal security requirements (D-W-101)" with seven items: permissions per host (capability terminal, SR10, SR11 with I6, SR12), fresh step-up (device key in the apps, in the console only a passkey), network, audit without content, idle time and maximum duration, service and session users without administrator rights, threats from section 6.7 of the threat model with a test. Per item the brief names what the ticket enforces and checks, or which ticket owns the item. The planning reads the item "Tailscale only" from the owner's requirement as "no public inbound path to the terminal": the host service opens only outbound connections (R-TM-01), an installation with the profile tailscale reaches the server only through the tailnet with a policy for port 443 only and without Funnel (D-OP-03); the product does not require Tailscale. Whether the terminal should be reachable only through a private network is OQ-24. The owner's requirement asks for these items in every terminal brief. A terminal only through Tailscale would contradict requirement 21 ("no Tailscale requirement") and ADR 0068; that is a product decision of the owner, not of the planning. 24, 07 SR10, 08 6.3, tailscale.md 3, Threat model 6.7, PLAN 18 requirement 21
D-W-102 The column "Depends on" of M2-39, M2-40, M2-41, M2-43, M2-44, M2-45, M2-46, M2-49 and M2-50 names only the transitive reduction, as for M1: M2-39 depends on M1-48 (M1-29 and M1-32 follow from it), M2-40 on M2-39 and M1-49, M2-41 on M2-40, M2-43 on M2-42, M2-44 on M2-43, M2-45 on M2-44, M2-46 on M2-45, M2-49 on M2-25 and M2-30, M2-50 on M2-49. The order does not change; the brief names the dropped dependencies under "Read first". The index lists only minimal dependencies, and tools/docs/brief_deps.py requires the column in the plan to equal the index; the dropped tickets were already in the closure of another dependency. PLAN 8, briefs/README.md, tools/docs/brief_deps.py
D-W-103 The column "Depends on" names for five M2 tickets what their brief really builds on (review of the briefs of 2026-10-11): M2-22 depends on M2-12 instead of M2-09, because the cost booking updates the sum of the agent session and needs the foreign key cost_entries.agent_session_id to agent_sessions (03, 10 12.1); M2-25 on M2-15, M2-17 and M2-26, because commands and interactions for apps use the autocomplete (M2-17) as well as signature, envelope and host rules of the outgoing webhooks (M2-26); M2-26 also on M1-56, because webhook_subscriptions.agent_id points to agents (03 4.11) and AZ-M49 needs message.created from a channel; M2-27 on M1-19, M2-05 and M2-24, because the inbound path needs rl.webhook_in (M1-19), channels, threads and messages and, for the proof "never triggers" (AZ-M53), the run subscriber from M2-05; M2-28 gets M2-12 through M2-22 (10 12.4 item 4). Index, waves and graph are regenerated with tools/docs/brief_deps.py --write; the order of the other tickets stays. The launcher starts a brief as soon as every ticket of its line "Depends on" is merged (P-01, D-W-100); without these edges each of the five briefs would have started too early and could only have ended with Verdict: BLOCKED. PLAN 8, briefs/README.md, tools/docs/brief_deps.py
D-W-104 The core contract is amended minimally and additively for briefs of the milestones M2 and M3 that would otherwise have stopped at a gap with Verdict: BLOCKED (planning of 2026-10-11): R-AZ-25 (05 section 5): the invocation token of command.invoked carries run_id only if the owner of the control command has an active run in the thread at execution time, otherwise the claim is missing (M2-16, M2-06); R-DM-42 (03 messages): messages.system_code also allows agent_session.resume_failed; if an adapter reports bind_failed, the server writes exactly one such system line with the agent of the run as author, and a system line does not reopen a done thread (M2-12, M1-56, 00 "Done", 01 6.1). R-AZ-26 (04 section 15.7 item 2, 05 section 5): for the autocomplete of an agent command, token and payload name the thread of the request or, in a text channel without a thread, the main timeline; in a forum channel without thread_id the server checks as for a new thread, does not ask the agent, issues no token and returns an empty list at once (M2-17); R-AZ-27 (05 section 4.11): the server writes the automatic state changes of outgoing webhook subscriptions as the system action webhook_out.transition into the audit log (M2-26); R-DM-43 (03 message_attachments, messages): message_attachments carries the copies thread_id and channel_id of its message (channel_id changed along on a move, without its own foreign key), messages the column has_attachments; M3-07 builds both, M3-14 filters with them before the candidate limit. R-DM-44 (03 runs, 04 section 15.3): runs.invocation_held_until remembers a held agent invocation, so that release and timeout write it exactly once (M3-07). R-AZ-28 (05 sections 2.6 and 4.12): new read action cost.reconcile.read on the instance for owners in a session or an app with scope costs:reconcile (without step-up, audit no, rl.read), operation listCostEstimates with the estimates per run in the mode organization without channel, thread and content (M2-28). R-AZ-29 (05 section 5 "Check without a request", 07): if the server checks the permissions of a human without their request (before every delivery to an agent, at run.token.renew, at a run from deferred follow-ups, before handing an app command or an interaction to an app, and for the permissions of the triggering human in SR9), the principal is this human without session, device and token, blocks come only from their status and the owner status from the current owner list; the permission state computed this way is never that of a personal token (M2-08, M2-14, M2-25). The affected briefs refer to the rule instead of a start check. 09 section 10.4 and D-AG-12 send every control command with a token and without a run, 09 section 9.5 and 22 section 9 require exactly one system message with a code on fail open (R-DM-34); without the amendment the brief could only have ended with Verdict: BLOCKED according to P-11, although M2 must be buildable before the end of M1 (D-W-100). 03, 05, 09, briefs/README.md
D-W-105 Terminal block after the briefs M2-43 to M2-47 (planning of 2026-10-11): M2-43 also depends on M1-67, because the control connection uses the frame codec, test client and routes of the gateway (M1-64), the short-lived path EphemeralPublisher (M1-66) and api/specschema (M1-67); M1-64 to M1-66 follow through M1-67. The row of M2-44 checks TM-T07 and TM-T12 each without the console part (M6-18). Only M2-44, where the deadline terminal.open_timeout comes from, registers and counts the metric bothy.terminal.relay.unpaired with the attribute bothy.terminal.side; M2-47 checks it with two nodes, M2-46 registers the other three metrics bothy.terminal.*. Every end of a terminal session reaches relay and control connection only through the observer of M2-44 at the log tail of every api process, because NOTIFY event_log runs in every mode and the tail reads the committed rows of event_log from the database; this applies to separate api and worker processes with cluster.mode = single as well as to the node of the connections with cluster.mode = multi. M2-46 keeps the dependency M2-45, M2-47 sends no signal of its own for session ends. Index, waves and graph are regenerated with tools/docs/brief_deps.py --write. Without M1-67, M2-43 would have built outside its dependencies and could only have ended with Verdict: BLOCKED; a metric with three owners would have been registered twice; a second path for session ends would have made two tickets depend on each other, while the tail already covers both modes (24 section 8.3 "immediately"). PLAN 8, 24, briefs/README.md
D-W-106 The column "Depends on" names every ticket that the start check of a brief requires (review of the briefs of 2026-10-11): M2-02 depends on M1-69 instead of M1-54 (canary scenario; above it M1-71, M1-65, M1-63 and M1-54); M2-13 on M2-11 and M2-12 instead of M2-09 (follow-ups and agent sessions belong in the context); M2-15 also on M1-57 and M1-58 (fetching, editing and deleting messages); M2-29 also on M2-14 (header Invocation-Token, SR9); M2-31 on M2-27 instead of M2-11 and M2-12 (above it M2-24 and everything else); M2-33 and M2-34 also on M2-12 (AgentSessionReport.started, bind_failed); M2-36 also on M1-72 (frame of make perf, above it M1-71); M3-02 also on M1-09 and M1-22 (secret provider, egress client); M3-03 on M1-69 and M1-70 instead of M1-55 (canary and leak tests). Because these edges imply others, M1-56 drops out at M2-04, M1-66 at M2-10 and M1-19 at M2-27; M2-03, M2-05, M2-11, M2-49, M2-50 and M3-11 get their missing tickets through the new edges. With the brief of M3-07, M3-14 depends on M1-57, M3-07 and M3-13 (M1-58, M1-69 and M1-70 through M3-07), M3-15 only on M3-14, and M3-07 also on M2-11, so that the follow-up run from deferred follow-ups already knows the hold for non-final attachments. Index, waves and graph are regenerated with tools/docs/brief_deps.py --write; M2-02 and with it the run chain start after M1-69. The launcher starts a brief as soon as every ticket of its line "Depends on" is merged (P-01, D-W-100); a start check on a ticket outside this closure would surely have ended the brief with Verdict: BLOCKED. The same class of error occurred in 16 briefs; the rule "start check needs" in tools/docs/brief_deps.py checks it since then for every brief (part of make docs). PLAN 8, briefs/README.md, tools/docs/brief_deps.py
D-W-107 Further edges after the start checks (review of the briefs of 2026-10-11, rule "start check needs" extended by the form BLOCKED <ticket> not merged; sentences with "only if merged" or "where merged" count as optional): M2-11 depends on M2-10 instead of M2-09 (acknowledgement of the follow-up as a run patch); M2-37 also on M2-22 (finishRun with costs in step 8); M2-46 also on M1-69 (canary scenario with terminal input and output); M2-48 on M1-30, M1-72, M1-73, M2-24, M2-46 and M2-47 (smoke test overlay, chaos frame of make perf, expiry job of the grants, app token for TM-T17); M1-71 also on M1-37, because harness.Start creates the first owner through the service of bothy setup bootstrap-owner and uses createUser with local_account (M1-71 was not built yet; no M1 wave moves); this drops M1-37 at M1-73 as an implied edge. Index, waves and graph are regenerated with tools/docs/brief_deps.py --write. As in D-W-106: without the edge the launcher would have started the brief before the required ticket, and it could only have ended with Verdict: BLOCKED; three M2 briefs (M2-31, M2-36, M2-48) reported M1-37 at M1-71 independently. PLAN 7, PLAN 8, briefs/README.md, tools/docs/brief_deps.py
D-W-108 Planning of the M3 push block (2026-10-11): (1) M3-19 creates the table push_registrations (03), the read port of the active registrations and the job notify.push_send with a provider port; M3-20 (APNs) and M3-21 (FCM) implement that port; M3-22 adds registerPush, unregisterPush, the preview token and the preview endpoint on that table and the deactivation on logout, device revocation and user disable. (2) Core amendment R-AZ-30 (05 section 4.9 row push.deliver, section 6 row notification.created): hints of the kinds security and admin have no thread and use the recipient rule of 13 section 2 at the time of each decision and send instead of push.deliver. (3) Core amendments R-DM-45 (03 6.5, 04 2.2 rule 5: deleting a file redacts its file.updated events to IDs and states) and R-DM-46 (03 1 item 7: files.purge_objects removes the row of a deleted file after its objects), found while writing M3-08. (5) Core amendments R-DM-47 (03 runs.invocation_held_until also for a pending server transcript) and R-DM-48 (03 files: derived ETag of a transcript), found while writing M3-09; M3-09 depends on M1-47, M2-31 (which implies M2-09, M2-13 and M2-20) and M3-05 as well and closes the deferred conformance lines AG-20 attachment M3-07, AG-20 transcript M3-09 and AG-25 voice M3-09. (6) Core amendment R-AZ-31 (05 sections 5 and 6, openapi.yaml, asyncapi.yaml): the durable personal event device.pending is the source of hints of kind security, written by identity when a device registers as pending; push.deliver and the recipient rule of R-AZ-30 are checks without a request (R-AZ-29); M3-19 builds the writer and the mapping and depends on M1-38 as well; M3-19 also builds the badge count and the badge sync of 13 section 8, which no plan row named; M7-06 emits the reminder of a privacy request deadline for hints of kind admin; found while writing M3-19. (8) notification.created of a hint without a thread has the recipient as resource (05 section 6, R-AZ-30); M3-23 delivers desktop hints in role all, M7-17 (now also after M3-23) adds the shared presence state and the worker-to-api path for separate roles and several api nodes; M3-22 (now also after M3-23) fills the preview fields of notification.created (13 6.3) with its preview builder. M1-66 at M3-23 and M2-09, M2-13, M2-20 at M3-09 drop out as implied edges. (9) From the review of the briefs: M2-46 depends on M1-30 as well, because its revocation test ends sessions through the job authz.expiry_sweep. M1-30 at M2-48 drops out as implied. (7) M3-22 depends on M3-06 and M3-07 as well, so the full preview of 13 6.4 (attachment_count, image_url) is always built; 08 10 step 4 sends no revoked push, because step 2 disables the device's registrations in the same transaction. (4) M3-16 and M3-24 depend on M1-72 as well and its plan row names perf/search with the make target bench-search instead of tools/bench-search, because the load test extends the perf frame of M1-72 and 02 section 6 puts load tests under services/server/perf/. PLAN rows M3-16, M3-19 and M3-22 name these changes. (1) Before, only M3-22 created push_registrations, but M3-22 depends on M3-20 and M3-21, which depend on M3-19, and M3-19 must create one notify.push_send per active registration (13 section 12 point 5) and test bundling and restarts with registrations: the order was wrong (found while writing M3-17). (2) Without R-AZ-30 no check is defined for two of the hint kinds of 13 section 2, and the push tickets could only stop with Verdict: BLOCKED. (3) Without them a deleted file would keep its name and digest in the event log and in its row, against 20 4.2, 4.3 and DP-T01. (4) As in D-W-106: without the edge the launcher would start M3-16 before its frame exists. (5) Without R-DM-47 the hold of a voice invocation contradicts its column; without R-DM-48 If-Match has no value to compare; without the edges M3-09 could build its switch, the transcript in the run context, the adapter report, the transcript read and the conformance cases only where merged, and no ticket would close the deferred lines. (6) Without the event the kind security of 13 section 2 had no source in the event log, and without the addition to section 5 no principal is defined for the hint check of a person who sent no request. (7) The preview fields need attachments and thumbnails; a revoked push had no kind, no recipient rule and no registration left to use. (8) The envelope of such a hint had no resource; the presence of 13 12 point 4 lives in the api process while the consumer runs in the worker; the preview builder belongs to M3-22, which ran before M3-23 in the old order. (9) As in D-W-107 for M2-48. PLAN 9, 05, 13
D-W-109 Rule for names in hosts.reported_doctor: If a person with terminal access, also a former one, can cause a violation, the message to the server names the account in which the instance operator must look, each account with its own entry (session_user); a new check follows the same rule. These are HD6 and HD11 (as before), new: HD10 with a wrong peer (the session user of this channel) and HD9 for every former session user (name bound to a person in the state directory, no longer in host_service.session_users) whose home can be listed; the console also shows the person of its assignment. Names of other users of the host are named only by the output on the host; for them an entry without a name goes to the server. HD9 stays at "Start refused". After bothy-host session-user remove the instance operator removes the long-running processes of the user, ends its processes and deletes it together with its home (or first moves the home to a location only for administrators); locking alone is not enough. After the access removal of a person who is not an owner, the console advises this step. remove closes the home but ends no processes. A long-running process that the person set up as the session user reopens the home, because it still belongs to their user; HD9 then refused the start of the whole host, and the console got only the code. The same applied to HD10 when a process of a person takes the place of their helper. This was the same error as with HD6 before D-W-64: a person brings the host to a standstill, and the console does not name them. Instead of extending every check one by one, one rule applies to every check that a person can make fail; the server knows these names from the assignment and needs no other names. Locking ends no running process, and passwd -l under Linux locks only the password ("Note that this does not disable the account", https://manpages.debian.org/bookworm/passwd/passwd.1.en.html, retrieved 2026-10-11 [verified]). Only deleting takes the home out of HD9 and ends the path; otherwise a later user with the same user ID would get the home without a user. Limit: after the loss of the state directory, the name of a former session user is missing from the message. 24 2.2 items 3 and 4, HD9, HD10, "Reason for HD9" and rule for names, 24 6.5, 24 10.1, R-TM-30, 16 4.4 items 1 and 6, 03 hosts, openapi.yaml HostDoctorCheck, 01 A8, 20, ADR 0068, Threat model T-TM-04, T-TM-11, T-TM-12, T-TM-14, RISKS RK-26, RK-89, TM-T24, TM-T26, PLAN M2-42, M4-37
D-W-110 check_runners.py recognizes release jobs independently of the spelling: it compares environment names without case, and blanks at the edges do not count. New rule environment-not-literal: an environment name with ${{ (as text or in environment.name) is a finding in every workflow. Not covered is where the secrets lie in GitHub: signing secrets exist only as secrets of the environments release* (ADR 0039, rule 4). The planning branch also compared runner labels without case and required bothy-ci or bothy-release in every self-hosted switch; on main there are no self-hosted runners (D-W-92), and a label in another spelling is already a finding of runner-not-github-hosted. GitHub treats environment names without case ("Environment names are not case sensitive", https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments, retrieved 2026-10-11) [verified]. With the literal comparison, a job with environment: Release got the signing secrets and passed outputs of resolve to a shell, without a finding. GitHub allows an expression as the name ("The value of name can be an expression", https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax, section jobs.<job_id>.environment, retrieved 2026-10-11) [verified]; whether it yields an environment release* would only be known at run time, and no workflow file needs it. These rules had already missed two forms (D-W-61, D-W-72); so the check now compares like GitHub itself instead of adding single spellings. tools/ci/check_runners.py, tools/ci/tests/test_check_runners.py, ADR 0039, gates.md, Threat model T-SC-05, R-QA-12, D-W-61
D-W-111 New rule actions-write in tools/ci/check_runners.py: no workflow has actions: write, neither in permissions on top level nor on a job, also not on a job with uses; write-all is now a finding on a job too (broad-permissions). There is no exception, also not for release.yml; if a workflow needs the permission later, that is a decision of its own. Not covered is a token from a secret with permission on Actions. The rule reusable-release-caller (D-W-73) saw only calls by uses:. A pull request workflow with actions: write could start portability.yml with gh workflow run: events created with the GITHUB_TOKEN start no run otherwise, but "workflow_dispatch and repository_dispatch events always create workflow runs" (https://docs.github.com/en/actions/concepts/security/github_token, retrieved 2026-10-11) [verified], and the endpoint POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches requires the permission "Actions" with write for the installation token of a GitHub App, which the GITHUB_TOKEN is (https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps, retrieved 2026-10-11) [verified]. The release workflow would then run outside the one run per tag of its release. Blocking the permission hits every path to a workflow_dispatch (gh, REST API, an action) in one place, instead of searching for commands in steps, and no workflow needs it. release.yml neither: with the permission it could start portability.yml a second time next to the call. A repository_dispatch is already allowed by contents: write; it starts only workflows that name this trigger (https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax, retrieved 2026-10-11) [verified], which no release workflow may name (rule release-trigger). On the planning branch the reason was also the minutes budget of the private phase, which main does not have (D-W-92). tools/ci/check_runners.py, tools/ci/tests/test_check_runners.py, ADR 0039, gates.md, 23 4, D-W-73
D-W-112 Port of the planning branch to main (2026-10-12): the planning rounds after the public baseline come to main in English with their IDs, as several pull requests. Three IDs of the planning branch are taken on main by other rulings (D-W-92, D-W-93, D-W-94 since the public baseline and the translation); their rulings carry new IDs here: D-W-92 of the planning branch is D-W-109, D-W-93 is D-W-110, D-W-94 is D-W-111, and every mention follows. Parts that rest on the self-hosted runners and the minutes budget of the private phase (docs/quality/self-hosted-runner.md, docs/quality/ci-budget.md, rule release-runner-label) are not ported, because main has only GitHub-hosted runners (D-W-92); D-W-110 and D-W-111 say what remains. Current state: in 05 section 5, the sentence on the actions that the principal of a check without a request may use also names push.deliver, which the planning branch lists among these checks (R-AZ-31) but left out of that sentence. The public baseline was built from an earlier state of the planning branch than its later rounds and reused D-W-92 and D-W-93; the translation added D-W-94. Renumbering the three later rulings keeps every ID unique (register_ids.py) and leaves the rulings that main already cites unchanged. D-W-92, D-W-93, D-W-94, D-W-109, D-W-110, D-W-111
D-W-113 tools/docs/brief_deps.py fails when a brief calls a ticket of its own dependency closure missing or optional. The check reads each paragraph and list item on its own, so a list never reaches into the next block. Parenthesized text is matched as a segment of its own and dropped from the sentence around it, so an aside like "M1-04 (which builds on M1-02)" names only M1-04 and a comma inside it ends nothing. Recognized forms: a ticket directly before "is not in" or "lies outside" a closure, a list of tickets ("M1-01, M1-02 and M1-04") before "are not in" or "lie outside" it; a ticket after "if", "when" or "once" with "is merged", "has (already) been merged" or "is on the base", a list with "are" or "have"; a ticket directly before "only if merged" or "only where merged"; a ticket followed by "(if merged)" with no other ticket in between; and the tickets of a list after "not in the closure ...:", "outside ... closure:" or "if merged:" up to a comma that starts a new clause (a comma before "and" or "or" and one last ticket does not). Not covered: a sentence that names the ticket only by a path, a feature or a pronoun ("the registry of M1-68 when it is on the base", "if services/server/test/canary is on the base"), other verbs and word orders ("unless", "is not merged yet", "(if present)"), a subject whose list words are joined by other text, and a list after such a colon whose tickets stand in list items of their own; review catches those. A correct sentence can still fail when it puts a dependency in one of these forms with another meaning; such a sentence is reworded. The port of the M2 briefs found the same class twice: sentences in M2-02, M2-03, M2-05, M2-08, M2-10, M2-11, M2-12, M2-16 and M2-22 (and in M1-26 and M1-59) still called tickets missing or optional that are dependencies, and a build session would have skipped work or reported a plan gap that does not exist. The launcher starts a brief only after its closure is merged, so such a sentence is always wrong; one check holds the common forms of the class instead of fixing instances (D-W-106 added the opposite direction for start checks). tools/docs/brief_deps.py, tools/docs/tests/test_brief_deps.py, tools/README.md, briefs/README.md, D-W-106, D-W-107
D-W-114 Decision of the owner of 2026-10-13: D-W-100 extends to milestone M4. The briefs of M4 are written before M1 is closed, in the format of the M2 and M3 briefs, in English and in the same brief index with waves across all milestones; each starts as soon as every ticket of its line "Depends on" is merged. They name interfaces from M1 to M3 as the briefs and the merged code name them and carry the same limit under "Boundaries" (follow the merged code where it differs only in names, stop with Verdict: BLOCKED where it differs in behaviour from the specification). The column "Depends on" of nine M4 tickets changes with the briefs: M4-03 names none (M0 closed) instead of M0-01, because M0 tickets are not in the index and M0 closes before any brief starts; M4-17 depends on M4-14, M4-15 and M4-16, because its scenarios from 04 section 16 send through the outbox and apply run documents, and its fake server lives in packages/sync_engine (library testing.dart) so that the headless sync tests and the app share it; M4-18 depends on M4-07 and M4-17 instead of M4-14, because its integration tests run against that fake server; M4-20 then depends only on M4-19 and M4-21 only on M4-20 and M2-31 (M4-15 and M4-16 follow from M4-17); M4-31 also depends on M1-69, because it builds the server endpoint client_metrics.submit on the signal catalogue of M1-68, the setting privacy.client_metrics of M1-61 and the canary harness of M1-69; M4-34 also depends on M1-73 (Compose smoke stack for the desktop E2E); M4-35 drops M2-38, which follows from M2-44; M4-36 depends on M4-35, M4-24 and M2-48 instead of M4-14 (implied by M4-24), because its integration tests run against the host service container and test overlay of M2-48. Builds must not run out of work while M1 closes (owner, 2026-10-13: no idle capacity); the index lists only the transitive reduction, and tools/docs/brief_deps.py requires plan and index to agree. A brief whose tests need a ticket outside its closure could only stop at its start. briefs/README.md, PLAN 10, D-W-100, D-W-102, D-W-106
D-W-115 The apps read eight configuration keys that steer their own behaviour but have place Server in the configuration reference, because no API operation delivers them: sync.reconnect_backoff_base, sync.reconnect_backoff_cap, sync.typing_min_interval, sync.typing_ttl, sync.max_focused_threads, client.cache_max_age, client.outbox_retry_base and client.outbox_retry_cap. AppConfig of packages/domain (M4-08) carries them with the default of the reference next to the keys of place App, and a test fails when one of them leaves the reference or changes its place. The server stays authoritative where it checks a value itself (for example a focus with more than sync.max_focused_threads threads is rejected). Limits that getInstance delivers in InstanceLimits (file_max_upload_bytes, voice_max_duration_ms, the message limits) are read from the server and never from AppConfig. M4-02, M4-09, M4-12 to M4-15 need these values; generating AppConfig only from place App would leave them without a source, and a literal in code would break the rule that limits are named keys. An API field for them can replace this list later without changing the callers. briefs/M4-08-domain.md, operations/configuration.md, D-W-114
D-W-116 The briefs of M4-11 to M4-38 change the column "Depends on" of nine M4 tickets: M4-15 also depends on M4-02, because its executors call the generated API and classify ApiProblem; M4-18 also depends on M4-11 (and through it M4-10), because the app shell registers the device keys of macOS and Windows; M4-24 also depends on M4-21, because its shortcuts stop runs, toggle the live box and are tested against approval cards (R-PR-04); M4-28 depends on M4-27 instead of M4-26, because the guided flow of 10 11 starts in the grant wizard; M4-30 depends on M4-25 instead of M4-18, because the notification note and "Start at login" live in the settings; M4-31 depends on M4-25 instead of M4-18, because it makes the crash report switch of the settings visible; M4-32 depends on M4-22, M4-24, M4-28, M4-29 and M4-33 instead of M4-21, so that its checklist covers the chat, settings and administration screens A1 to A6 and, through M4-33, the notification area of M4-30 (M4-36 adds the terminal rows and M4-37 the rows of screen A8, each into its section of the skeleton page docs/quality/accessibility-checklist.md that the brief package creates, so no build ticket has to create the page or its navigation entry); M4-33 depends on M4-20 and M4-30 instead of M4-19 and M3-18 (which M4-30 implies through M3-23), because the thread menu sits in the thread header and its test needs the notification stream and the fake's notification decision of M4-30 (and through it the settings entry and the step-up dialog of M4-25); M4-34 drops M4-21 and M4-30, which M4-24 and M4-33 now imply. AppConfig also carries auth.device_pending_ttl and, added by M4-36, terminal.max_session_duration with the rule of D-W-115, because the Device and TerminalSession schemas name no expiry and screens E4 and D11 show the remaining time (display only; the server decides). The cost figures of screen A5 come from getCostStats (M6-05) and join the agent page of admin_ui with the statistics views of M6; M4-28 builds the backend switch, the guided flow and the agent tokens. A brief whose tests or screens need a ticket outside its closure could start before that ticket is merged and could only stop; The cost figures belong with the statistics of M6 (M6-05 and the views built on it), so M4-28 does not wait for them. briefs/README.md, PLAN 10, D-W-114
D-W-120 Decision of the owner of 2026-10-13: D-W-100 extends to milestones M5 and M6 ("no idle capacity": briefs of later milestones are written now). The briefs of M5 and M6 are written before M1 is closed, in the format of the M2 to M4 briefs, in English and in the same brief index with waves across all milestones; each starts as soon as every ticket of its line "Depends on" is merged. They name interfaces of earlier milestones as those briefs and the merged code name them and carry the same limit under "Boundaries" (follow merged code that differs only in names, stop with Verdict: BLOCKED where it behaves differently from the specification). They land in packages; each package records its changes of the column "Depends on" in its own ruling. This first package (M6-01, M6-05, M6-10 to M6-15) changes: M6-01 depends on M1-39, M1-40, M1-41, M1-43, M1-45, M1-49 and M1-64 instead of M1-40 and M1-41, because the console form covers the password, passkey, OIDC and proxy header logins, revocation ends console sessions, the stored view builds on viewAsUser and the Origin check of the gateway upgrade (ID-T19) is built by M1-64; M6-05 depends on M2-22, M2-26 and M3-22, because the error rates read webhook deliveries and disabled push registrations; M6-11 also depends on M1-04, because it adds checks to make ops-check; M6-13 also depends on M2-15, because approval cards are one of its measuring points; M6-15 depends only on M6-13, because M1-65 follows from it. The core contract gets, additively, R-DM-49 (auth_sessions.view_as_user_id and view_as_started_at for the console) and R-AZ-32 (action access.view_as.end, owned by identity; while a view is stored, access.view_as.end, auth.logout, auth.step_up and self.read on one's own session are decided without view_as), which 07 9.2 asks to add before M6-01 builds the console session. Builds must not run out of work while M1 closes (owner, 2026-10-13); a brief whose tests need a ticket outside its closure could only stop at its start, and a brief that needs a core contract rule the contract lacks could only end with Verdict: BLOCKED (P-11, D-W-104). briefs/README.md, PLAN 11, PLAN 12, 03, 05, D-W-100, D-W-104
D-W-121 The briefs M5-01 to M5-07 (second package of D-W-120) change the column "Depends on" of the plan: M5-02 also depends on M4-23, because the tabs Mentions and Running and the search of screen M1 reuse the cross-cutting views of M4-23; M5-03 depends on M5-01 and M5-02 instead of M4-21 and M5-01, because the Android app reuses the phone layouts of M5-02 instead of building a second set of screens (M4-21 follows from M5-02); M5-04 depends on M3-22, M4-31 and M5-02 instead of M3-22 and M4-09, because the Notification Service Extension needs the iOS runner and build configuration of M5-02 (M4-09 follows from it) and forwards its display counters through the client metrics of M4-31; M5-05 depends on M5-03 and M5-04 instead of M3-22 and M5-03, because the Android service reuses the native strings generator, the DPoP contract vector and the app-side push rules of M5-04 (M3-22 follows from it); M5-06 depends on M1-73, M4-22, M5-03 and M5-04 instead of M5-02, M5-03 and M3-03, because the Share Extension sends through ClientKit of M5-04, the app takes shared files over into the upload entries of the outbox of M4-22, searches targets with searchThreadTitles of M3-14 (which follows from M5-02 through M4-23), and proves ID-T16 against the Compose smoke stack of M1-73 (M3-03 follows from M5-04); M5-07 depends on M3-09, M4-22 and M5-03 instead of M5-02 and M3-09, because recording uses the upload entries and the voice player of M4-22 and runs on both phone platforms. Tests that need a server stack (the Compose stack, a host service container) live outside apps/client/integration_test/, because the platform jobs of M4-01 run that folder without a server; M4-34 and M4-36 do the same on the desktop. No check enforces this (brief_deps.py does not read test paths); the brief review does. A brief whose tests need a ticket outside its closure could only stop at its start (D-W-106); reusing the phone layouts and ClientKit keeps each feature built once (owner rule 2, R-CL-01). PLAN 11, briefs/README.md, D-W-120
D-W-122 The briefs M5-08 to M5-13 (third package of D-W-120) change the column "Depends on" of the plan: M5-08 depends on M5-03 instead of M5-02, because the app lock runs on both phone platforms and uses the recent apps protection of M5-03; M5-09 depends on M4-27 and M5-03 instead of M4-26 and M5-02, because revoking a grant reuses the grant controllers of M4-27 and the actions run on both phone platforms; M5-10 depends on M5-06, M5-07, M5-08 and M5-09 instead of M5-02 and M5-03, because its checklist covers the share sheet, the recording, the lock and the emergency actions; M5-11 depends on M1-39, M5-05, M5-06, M5-07, M5-08 and M5-09 (M5-04 follows from M5-06), because its seed creates local accounts with TOTP and E2 logs in with them; M5-12 depends on M4-36 and M5-10 instead of M4-36, M5-02 and M5-03, because the terminal detaches on the lock event of M5-08 (which follows from M5-10) and reattaches only after the app lock (PLAN 11 red team), and its new routes M8 and M9 need rows in the checklist of M5-10, whose route test fails otherwise; M5-13 depends on M4-38, M5-11 and M5-12, because it measures the terminal targets with the output generator and the timing hook of M4-38 and completes the checklist of M5-10 (which follows from M5-12) for M8 and M9. The mobile E2E suites, the measurement and the phone terminal stack test live outside apps/client/integration_test/, as the desktop ones of M4-34 and M4-36 do, because the platform jobs run that folder without a server. M5-11 creates the make target e2e-mobile, outside check. Profile builds run only on physical devices, so the mobile targets client.perf.* (including the two terminal targets of 15 19, which M5-13 measures) are measured by M5-11 and M5-13 where a physical reference device is attached and otherwise checked at the closing of M5 (PLAN 4 item 3); an unattended build without such a device reports them as not proven. These changes build on D-W-121. A brief whose tests need a ticket outside its closure could only stop at its start (D-W-106); the lock must exist before the terminal can rely on it. PLAN 11, briefs/README.md, D-W-120
D-W-123 The briefs M6-02 to M6-04, M6-06 to M6-09 and M6-16 to M6-18 (fourth package of D-W-120) change the column "Depends on" of the plan: M6-02 also depends on M4-09, because the console builds on the generated api_client and the LoginService of auth; M6-03 also depends on M1-73, because its headless test runs the console against the Compose smoke stack and creates the make target console-e2e; M6-04 depends on M4-28, M4-29 and M6-03 (M4-27 follows from M4-28 and M6-02 from M6-03), because its patrol web tests run in console-e2e; M6-06 depends on M4-26, M6-02, M6-05 and M6-11, because A7 sits in the desktop administration of M4-26 and is drawn with the marks of M6-11; M6-07 depends on M1-30 and M6-04, because the expiry overview builds on the expiry of M1-30; M6-09 depends on M1-47, M3-10 and M6-04 as planned; M6-16 depends on M5-03, M6-04, M6-06, M6-12, M6-14 and M6-15 instead of M5-02, because M10 runs on iPhone and Android and the console suite runs among the patrol web suites that M6-04 adds to console-e2e (M6-03 follows from M6-04); M6-17 depends on M6-16 (M6-03 follows); M6-18 depends on M2-48, M4-35, M4-37 and M6-04 (M6-03 follows), because its suites need the host service container of M2-48. The core contract gets, additively, R-AZ-33 (public action console.csp_report for the report-to endpoint of 19 5) and R-AZ-34 (read action access.hints.read for the lists of 07 9.4 and 9.5). A brief whose tests need a ticket or a make target outside its closure could only stop at its start (D-W-106); an operation without an action from the catalogue cannot be built (05 section 1, P-11). PLAN 12, 05, briefs/README.md, D-W-120
D-W-124 Decision of the owner of 2026-10-14: D-W-100 extends to milestone M8 ("no idle capacity"). The briefs of M8 are written before M1 is closed, in the format of the M2 to M6 briefs, in English and in the same brief index with waves across all milestones; each starts as soon as every ticket of its line "Depends on" is merged, names interfaces of earlier milestones as their briefs and the merged code name them, and carries the same limit under "Boundaries". Every M8 brief tests only against fixtures and fakes and never contacts a live Discord account or server; test_brief_format.py requires the boundary item "Never contact Discord". No brief needs a paid plan or paid service: the import uses a free Discord bot application. This first package (M8-01 to M8-08, M8-10, M8-11) rules: (1) The tool code lives under services/server/cmd/bothy/importtool/ (the plan's cli/import; bothy import is a subcommand of the one binary, D-L1, component cmd; import is a Go keyword). (2) Chapter 22 is aligned with 08 6.4: a cli session never provides step-up, so every import.manage step (create with the mapping, start, pause, cancel, complete, ownership list) and channel.operator.set run in the web console (M8-08); the tool uses import.read, import.apply and read actions; --apply imports into the running source of its guild; bothy import login|logout (device pairing, 02 3.3, 22 10.1) is new; the dry run may call refreshToken and rewrite only the session file (22 10.2 rule 5); a run lease in the cursor replaces "source already running" (22 7.3, 7.4); switch performs the last mirror round and prints the console steps, revert only reads and prints (22 13.3, 13.4); forum_tag gets its origin key (22 7.1); ImportSource returns the stored cursor, so a new run reads its progress and lease (M8-03); the bytes of imported files go through two new operations under import.apply (uploadImportFilePart, completeImportFile), because uploadPart and completeUpload are file.upload, which rule 1.8 refuses in a read-only mirror channel, and ImportFileCreate carries sha256 for file_upload_sessions.declared_sha256 (M8-11); completion counts only imported channels, and the web console warns before it (22 13.5). (3) Two tickets are added: M8-10 (login and generated API client of the tool, split from M8-04) and M8-11 (thread, message, pin and file items, split from M8-03). (4) The column "Depends on" changes: M8-01 depends on M1-22 instead of M1-05, because every connection goes through the egress client; M8-03 depends on M2-09 instead of M3-07, because it builds the structure items and the read-only switch that cancels runs through requestCancel, while the content items that need files move to M8-11; M8-04 depends on M8-02, M8-10 and M8-11 instead of M8-02 and M8-03; M8-06 depends on M2-22, M2-34 and M8-04 instead of M8-03 and M2-12, because it adds the --sessions path of the tool and the cost baseline that the Claude Code runner must start from (M2-12 follows); M8-10 depends on M1-38 and M1-71 (pairing, real-server harness); M8-11 depends on M1-59, M2-18, M3-07, M3-13 and M8-03 (pins, the placeholder title rule, attachments, the search language of every message insert). M8-09 keeps its row and follows in a later package once M7-14 and M7-21 have briefs. Builds must not run out of work while M1 closes (owner, 2026-10-14); a brief whose tests need a ticket outside its closure could only stop at its start (D-W-106), and a brief that relied on a step-up from the command line could only end with Verdict: BLOCKED, because 08 6.4 forbids it (R-ID-17). briefs/README.md, PLAN 14, 22, 08 6.4, 02 3.3, D-W-100, D-W-120
D-W-125 Decision of the owner of 2026-10-14: D-W-100 extends to milestone M7 ("no idle capacity": briefs of later milestones are written now). The briefs of M7 are written before M1 is closed, in the format of the M2 to M6 briefs, in English and in the same brief index with waves across all milestones; each starts as soon as every ticket of its line "Depends on" is merged, and each carries the limit of D-W-100 under "Boundaries". Two further owner rules apply to every M7 brief. First, no costs: no brief requires a paid plan or a paid service; where the plan names one (Apple Developer Program, a Windows signing certificate or Artifact Signing, a Play Console account, cloud accounts, a Grafana Cloud account), the brief builds and tests a free path (test publisher, local VM, local registry, unsigned plugin allowed by its ID) and the paid step stays an owner decision (OQ-08, OQ-09, ruling T11); GitHub-hosted standard runners count as free because the repository is public. Second, no brief tags, releases, publishes or deploys by itself: release chains are proven with trial releases against local targets, and the first real tag of a component stays with the owner. The packages record their changes of the column "Depends on" in their own rulings; this first package (M7-01 to M7-09, M7-22) changes: M7-01 also depends on M1-08, because it adds a secret generator to the configuration reference that make config-doc-check holds; M7-02 depends on M3-02, M3-03 and M7-01, because the object backup reads files through the fs and s3 drivers and the manifest counts files; M7-04 depends on M2-07, M2-26 and M7-02 instead of M7-02 and M1-65, because restore finalize moves the cursors of agent consumers and webhook subscriptions and webhooks carry sync_epoch (M1-65 follows from M2-07); M7-05 depends on M1-39, M1-40, M1-59, M2-15, M2-22, M2-26, M2-46, M3-08, M3-15 and M3-22 instead of M3-08, because its participants cover the tables of the classes security, content, transient and cost that those tickets create (local credentials, passkeys, reactions and pins, interactions, costs, webhook deliveries, terminal sessions, text extracts, push registrations); M7-08 depends only on M7-09 (M2-01 follows); M7-09 depends on M6-13, M7-02 and M7-05 instead of M2-20, M3-20 and M1-68 (which follow), because its checks read the own measurement in stats_minutes and it builds retention.ops for the status tables and backup_runs on the participant port of M7-05. The core contract gets, additively, R-DM-50 (instance.pg_system_identifier and instance.pg_timeline_id, the start lock that ADR 0059 left open and M7-04 builds) and R-AZ-35 (the personal durable event gdpr.request_due with its schemas in openapi.yaml and asyncapi.yaml, which D-W-108 assigns to M7-06), and the consumer name privacy.journal in the check of event_consumer_cursors.name, which M7-07 adds by an additive migration (R-DM-27), and R-DM-52, the table deletion_journal_state (module privacy) that numbers the objects of the deletion journal densely, because the object storage port has no listing and no conditional write. M7-22 allows the AWS SDK in a second package, internal/audit/adapters/s3sink (vendor awssdk for the module audit in modules.yaml, narrowed by depguard as for M3-02), with its own narrow interface (conditional PutObject, GetObject, ListObjectVersions, GetObjectLockConfiguration), because the checkpoint sink needs listing and the lock check, which the core S3 rules of the file driver (ADR 0010, ADR 0048) exclude for files; the file driver keeps its rules. Builds must not run out of work while M1 closes (owner, 2026-10-14); the owner excludes all costs and keeps tags, releases and deployments for himself; a brief whose tests need a ticket outside its closure or a rule the core contract lacks could only stop at its start (D-W-106, P-11). briefs/README.md, PLAN 13, 03, 05, D-W-100, D-W-108
D-W-126 The brief of M8-09 (second package of D-W-124) changes the column "Depends on" of the plan: M8-09 depends on M8-07 and M8-08 instead of M8-07, M8-08, M7-14, M7-21 and M5-11. The brief proves the migration flow of 22 13 on fixtures and fakes (a rehearsal over a whole fake Discord server against the real server), writes the generic, white-label guide docs/operations/discord-migration.md and ends with the owner's decision sheet; it never runs the live migration, which uses a real Discord bot and a real installation and needs the owner's go. M7-14 (portability of the release), M7-21 (status page) and M5-11 (mobile E2E) are prerequisites of that live run, which the guide names; PLAN 14 exit criterion 2 is met only by the live run. A brief whose dependencies it does not use would wait for nothing (D-W-106); the live run is an external step that only the owner starts. PLAN 14, briefs/README.md, D-W-124
D-W-127 The release briefs M7-10 to M7-16, M7-23 and M7-29 (second package of D-W-125) prove their chains without a tag, a publication or a paid account. Trial release: make release-trial COMPONENT=<component> (M7-10 creates it for server; M7-29 adds bothy, M7-23 host) runs the release targets of the component on the build host against throwaway local targets: a registry container pinned by digest on 127.0.0.1, a cosign key pair generated for the run and signing with the signing config of the private phase (ADR 0062), Syft SBOMs and a signed SHA256SUMS, then the verification an instance operator runs (cosign verify --key, cosign verify-blob, sha256sum --check); it refuses to run when GITHUB_ACTIONS is set, and so do its trial-only variables (RELEASE_TRIAL_REGISTRY of M7-29 for the registry in the bundle, the trial key of make portability-trial of M7-14). This replaces the "trial release on a test repository" of RK-57, because creating remote repositories is outside every brief. Test publisher: on macOS a self-signed code signing identity in a temporary keychain (codesign --verify --strict must pass; spctl --assess accepts only an Apple-issued Developer ID with notarization, so the trial records its rejection and the release job runs it with the owner's identity), on Windows a self-signed certificate whose subject equals the MSIX publisher, imported only into the run's stores (signtool verify /pa must pass), a throwaway EdDSA key for Sparkle and a throwaway Android upload key; notarization, TestFlight and Play uploads need the owner's accounts (OQ-08) and run only in the release environments with his secrets. The keyless path of the public phase and the attestations can only run in GitHub Actions with a real tag; they stay not proven until the owner's first release. Changes of the column "Depends on": M7-11 also depends on M1-03, because the gate joins make api-checks; M7-13 depends on M5-11 and M7-12 instead of M5-11 and M7-10, because the update path comes from distribution.yaml and the test publisher of M7-12 (M7-10 follows); M7-14 depends on M2-29, M7-03, M7-09 and M7-29 (M7-10 follows from M7-29, M3-04 and M3-14 from M7-09), because its smoke run starts a test adapter from the test double of the Python SDK and calls the operations search (M3-14) and getStatus (M7-09) of 16 11.1; M7-15 depends on M7-29 instead of M7-10, because the host setup runs install.sh --bundle and the VM test installs the product trial, and that VM test (make tofu-vm-test) runs only on the build host, because the trial refuses to run under GITHUB_ACTIONS; M7-23 depends on M2-48 and M7-12 (M7-10 follows from M7-12). No brief may tag, release, publish or deploy, and none may require a paid account (D-W-125); a chain that only runs at the first real tag would be proven too late (RK-57). A brief whose tests need a ticket outside its closure could only stop at its start (D-W-106). PLAN 13, ADR 0062, OPEN-QUESTIONS, D-W-125
D-W-128 The briefs M7-17 to M7-21 and M7-24 to M7-28 (third package of D-W-125) change the column "Depends on" of the plan: M7-17 depends on M1-72, M2-10, M2-47 and M3-23, because its cross-node test is a target of make perf (M1-72) and reuses the two-node harness of the terminal relay (M2-47); M7-20 depends on M1-73 and M2-27 (M1-04 follows from M1-73), because its end-to-end test starts the Compose stack with the profile observability; the owner creates the alert channel, its app and the incoming webhook in a session with step-up, because webhook_in.create and app.install need one and an API token cannot step up (07 SR5, 08 12), and the M7-20 tool only stores the URL; M7-21 depends on M5-03, M6-04 and M7-08 instead of M7-09, M6-02 and M5-02, because the status views run on iPhone and Android, the console suite joins the patrol web suites of M6-04 in console-e2e, and the maintenance mode of M7-08 is part of what the views show (M7-09 follows); M7-26 depends on M7-14 and M7-25 (M7-10 follows from M7-14); M7-27 depends on M2-48, M6-14, M6-15 and M7-14 (M2-29 follows from M7-14), because its integration test reads getStatsTimeline, getStatsSlo and getStatsBackends of M6-14; bothy setup demo is a client of the public API that bootstraps the owner, enrolls a demo device and steps up with its key, because owner actions need a person's session with step-up (07 SR5) and no system action covers the demo; M7-28 depends on M5-13, M6-16, M6-18, M7-25 and M7-27 (M4-38 follows from M5-13 and M7-29 from M7-27); its images come from the build host only, because GitHub-hosted macOS runners have no Docker engine and Windows runners no Linux containers (M4-34), so images of the Windows app follow on a host of the owner and stay an owner decision. The plan's column "Tickets that can start after M1" for M7 is recomputed from the dependencies of all three packages. The core contract gets, additively, R-DM-51: the table gateway_presence (module gateway, UNLOGGED, section 4.16 of 03) with the shared presence state that M3-23 left to M7-17 and that D-W-108 and RK-37 name without a mechanism; the module register, 02, the ERD and the module README list it. A brief whose tests need a ticket outside its closure could only stop at its start (D-W-106); without a table the worker cannot see the live connections of other processes, and a build ticket may not add one to the core contract (P-11). PLAN 13, 03, briefs/README.md, D-W-108, D-W-125