Skip to content

Open Items Register

Owner: El · Last reviewed: 2026-08-19

Purpose, and how this differs from the roadmap

This register holds things that must be resolved, verified, or decided: risks, unverified assumptions, security debt, legal questions, and contradictions between documents. Each entry names what it blocks, so nothing gates a client quote silently.

The roadmap holds planned work, sequenced over time. It answers what are we building and when.

Register Roadmap
A decision not yet made ✅ ❌
A fact not yet verified ✅ ❌
Security or legal debt ✅ ❌
A contradiction between docs ✅ ❌
A feature to build ❌ ✅
A client deliverable ❌ ✅

An item leaves this register in one of three ways: it is resolved and closed, it becomes an ADR (a decision was made), or it becomes a roadmap item (it turned out to be build work). It is never deleted silently. Closed items move to the Closed Items register with a date, a reason and an outcome.

A Deferred item has not closed. It stays here with its mitigation and the trigger that ends the deferral. A deferral without a trigger is a decision nobody made.

Scope: ABOS only. The Internal Portal and ContractAR keep their own registers in their own repositories.

Status values: Open · In progress · Blocked · Deferred IDs are permanent. Never reuse a number, even after closure.


A. Blocking the Greenhills quote

Nothing goes on paper until OI-001 to OI-003 are answered. OI-001 in particular can turn a fixed-price build into a loss.

ID Item Owner Status
OI-001 AroFlo data completeness spike. Pull 90 days of real data. Are there reliable start and finish timestamps per job? Is job type a controlled field or free text? Free text means a classification mapping layer maintained forever. Duration-by-job-type cannot be built without this, and no modelling conjures it. El Open
OI-002 Metabase spike. Half a day, local Docker, dummy data shaped like the canonical model, 2 GB memory limit. Build a number card, a trend line, and a bar by job type. The memory figure sizes the shared host. El Open
OI-003 AGPL review. Embedding open-source Metabase in a closed commercial product routinely triggers legal review. Must be resolved before dashboard revenue is contracted. Not an engineering opinion: get it in writing. External / legal Open
OI-004 Clarify what Greenhills means by "approvals." Reporting on their existing approval process, or an assumption that we are building one? Very different quotes. El Open
OI-005 Get a number for "live." Client to state the acceptable data lag. 15 minutes is almost certainly enough and costs a fraction of the 2,000 calls per day AroFlo budget. El Open
OI-006 Verify the AroFlo reporting gap rather than assert it. "AroFlo has no comprehensive reporting" is exactly the claim a client repeats to their AroFlo rep. The defensible pitch is the cross-system join AroFlo cannot do. El Open

B. Time-sensitive, the window closes

ID Item Owner Status
OI-007 Greenhills before-state capture. Current invoice volume, manual touch time, rework rate, AroFlo to Xero reconciliation duration. The window closes the moment anything goes live. After that, every savings claim is an estimate against an estimate. El Open
OI-008 Metric definitions signed off by the client before the first run. Redefining "exception" in month four silently invalidates months one to three, and it will not be noticed until a trend looks wrong. El Open
OI-009 Minutes-saved multiplier agreed at Blueprint and recorded. "38 hours returned" is a model, not a fact. When challenged, we point at their sign-off rather than at our assumption. El Open

C. Blocking provisioning

ID Item Owner Status
OI-012 Shared host size, with Metabase overhead assumed rather than discovered (ADR-0021 §2, ADR-0023 §5). Depends on OI-002. El Blocked (OI-002)
OI-014 Tested restore. Posture is settled (ADR-0026), and the restore test is the open item. First restore proven before the first paying client goes live. An untested restore is a hopeful cron job. El Open
OI-015 Populate the Bitwarden shared collection. Organisation and collection exist, and the core vault password is stored. Remaining: Lachlan to accept the invite and be set to Owner (not User or Admin, per ADR-0027 §6), then add provider root logins (Vultr, Cloudflare, Micron21, Microsoft 365, GitHub, Railway) each with its MFA recovery codes, plus the Entra client secret and expiry date. El + Lachlan In progress
OI-036 AU-resident off-box backup target. Confirm Vultr Object Storage is available in the chosen AU region, with encryption at rest. El Open
OI-040 Canonical credential-name list in the repo, names only and never values, so the shared collection can be audited without opening it (ADR-0027 §10). El Open
OI-042 Scoped Cloudflare API token for the dns role. DNS edit on the specific zone only, never the global API key. Stored in Ansible Vault under the core vault ID, not Bitwarden. El Open
OI-043 Reconcile the Ansible tree against ADR-0024. The ADR specifies ansible/, the repository has infra/, and there is a stray inventory/ at the repo root. The ADR should be amended to match infra/, since it is already referenced in ansible.cfg. El + Lachlan Open
OI-054 Generate the age backup keypair. age-keygen. Public key into infra/inventory/group_vars/all.yml as backup_age_recipient. Private key into Bitwarden and nowhere else: not a host, not the repo, not Ansible Vault. Stated plainly, lose the private key and every backup is unreadable. It is the most important secret we hold, more than any vault password, because a vault password protects things that can be regenerated. El + Lachlan Open
OI-055 Restore verification is human-read, not asserted. verify-restore.yml prints row counts for a person to compare. It cannot assert correctness without knowing the tenant schema, which does not exist yet. Until it does, the restore test can be ticked off without actually being read. Add real assertions (expected tables present, approval-log row count non-zero and within tolerance) once the schema is built. El Open
OI-056 Disable Tailscale key expiry on every ABOS host. Node keys expire by default. An expired key silently removes the host from the tailnet, and with public SSH closed (ADR-0031) the only remaining path is the provider's web console. This is the most likely way to lose administrative access, and it fails months after the change that caused it. Set per machine in the Tailscale admin console after it joins, and verify before phase 3. El Open
OI-057 Tailscale auth key carrying tag:server, into the core vault. Pre-authorised and reusable. Tagged nodes are owned by the tailnet rather than a person's account, so a host does not vanish when someone's account changes. Machine credential, so Ansible Vault rather than Bitwarden (ADR-0027 §7). El Open
OI-051 Split the core_stack role by scope. ADR-0030 divides it into per-host components (Caddy, brain replica) and global ones (registry, Console). The current single stub role encodes the old assumption. El + Lachlan Open

D. Security debt, fix before it propagates

Ordered by how expensive each becomes if copied into the client-facing app.

ID Item Owner Status
OI-016 ALLOWED_EMAILS is not enforced on the live SSO callback path (Internal Portal). On the portal this is a bug. Replicated into a client-facing app it is one client seeing another client's approvals. Fix before the client review app is designed, so the pattern is not copied. El Open
OI-017 Do not reuse the portal JWT_SECRET for the client review app. Clients are not in the Microsoft 365 tenant, so one shared secret across a client-facing surface means a leak crosses tenants. Separate identity, issued per tenant. El Open
OI-018 Approval records: append-only, never updated in place, with a content hash of the exact proposed action. If the payload changes after approval, the approval is void and the item re-queues. Without this, "the client approved it" is unprovable, and that is the sentence said to an insurer. El Open
OI-020 Metabase patching task in the provisioning playbook (ADR-0023 §8). Pinned version plus a scheduled bump. Cost scales with dashboard-client count. El Open
OI-049 Brain statelessness must be enforced, not assumed. ADR-0030 and ADR-0032 make replicas safe only while the brain holds nothing that outlives a request. In particular LangGraph checkpoints must persist to the tenant's Postgres, never local disk or memory. Verify when the brain is built: a violation is invisible on one host and corrupting on two. El Open
OI-059 Per-tenant database roles and grants for the brain (ADR-0032 §5). The brain must never hold a superuser connection to tenant databases. Grants must be maintained as the schema evolves, which is real ongoing work, accepted because it converts the likeliest cross-tenant failure (a bug in shared engine code) into an error. El Open
OI-060 No eval harness exists. Nothing measures the brain's decision quality, so "the brain works" currently means "it returned valid JSON". Assist autonomy (every draft approved) is the only defensible setting until a scored harness exists, and any future autonomy increase depends on one. El + Lachlan Open
OI-061 max_calls_since_human must be visible on the work item, not only in a log. The mechanism is designed (flow/approve.md §3.5): the counter is calls since the last human action rather than calls in total, because a total cannot separate a loop from an abandoned item from a legitimate six-week negotiation. What is not built is the counter itself, the two outcomes it produces (call_ceiling alerting the Console, abandoned not), and surfacing both on the item. An agent cycling quietly is a cost failure and a correctness failure at once. El Open
OI-062 Nothing validates the tenant config schema. The Console will happily write a value that nothing reads, and the failure is silent: behaviour simply does not change, and nobody knows why. The schema needs a single definition that both the Console and the brain validate against. Closes in Console phase 2 (Console side), before the first write: one settings schema, validated by the Console API on every write (ADR-0037 §5). The brain validating against the same schema follows with Senior. El Open
OI-063 No audit trail on config changes. Who changed what, when, and why. The approval log is evidence for actions, but a dispute about behaviour is usually a dispute about what the rules were at the time, and there is currently no record of that. Work items pin a config_version, so the versions exist. Their history and authorship do not. Closes in Console phase 2, before the first write: every change, action and job recorded append-only with who, when, before and after (ADR-0037 §5). El Open
OI-064 Masking policy is described but not decided. Identifiers are stripped before any model call, inside packages/llm-client so it cannot be bypassed. What is actually masked, and what is deliberately kept, is not written down. Over-masking degrades the product: mask a site name and the model can no longer judge urgency or match a location. Needs an ADR, and it must be honest that masking protects against prompts surfacing in logs and traces rather than making content confidential. El Open
OI-065 Inference residency is undecided. ADR-0008 makes AU residency hard for client data at rest, and says nothing about prompts in flight. Options include accepting offshore inference under no-training terms, or an AU-resident route such as Bedrock Sydney. Likely a per-client tier rather than a platform-wide default, since a field-service client and a health or finance client have different needs. Adding a third-party platform would also need to supersede ADR-0009. Lachlan investigating. El + Lachlan Open
OI-066 Confirm Anthropic and Google commercial terms, being no training on inputs, the actual retention window, and whether zero-retention is available. This is what carries a confidentiality claim to a client, not masking. Note that Covered Models carry a mandatory 30-day retention regardless, so llm-client must make model choice explicit. El Open
OI-068 Backfill is unpriced and undesigned. Onboarding a client needs their existing customers and jobs in the tenant database before fact extraction works. Prefer a file export over the API, since against a 2,000 calls per day cap a large history is days of calendar time rather than an afternoon. The blocking question is whether export IDs match API IDs: if they do not, backfilled rows cannot be matched to later updates and the file route is dead. Build the file route, document the API route as a fallback without building it, and record provenance per row. Scope: all customers, all open jobs, closed jobs for twelve months. Emails are not backfilled beyond a sample for eval. El Open
OI-069 A completed Process Map checklist uploaded as a document is not structured data. It cannot gate a stage, feed a recommendation, or transcribe into tenant config automatically. Acceptable for the first engagements, and it stops being acceptable once the answers are the basis of a quote. Revisit after Greenhills: the technical readiness answers in Part B are the subset most worth structuring, because they change the price. Portal work, so it also belongs in the portal repo's register. El Open
OI-070 Thread identification must use RFC 5322 headers, not the provider's conversation ID. Microsoft Graph's conversationId is derived from the conversation topic, so editing the subject creates a new conversation, and it frequently changes when an external party replies (Gmail, Yahoo, another organisation's Exchange) even when References and In-Reply-To are correct. For email triage every inbound message is an external party replying, so this is the normal case rather than an edge case. Store the Message-ID of every message seen and sent, match on References and In-Reply-To, and treat conversationId as a secondary signal only. Verify against a real external reply during the client-zero build. Getting it wrong splits every conversation into single-message items, makes the turn counter meaningless, and removes the main reason for choosing per-thread work items. El Open
OI-106 learn_action_after is not a setting: one correction to Action always learns learned_action. The other threshold, learn_noise_after, is read from the mailboxes table; this one is fixed in learn in Junior: corrections. Making it configurable is not a one-line change: correction_count counts moves out of Action, so learning Action after more than one move needs a second counter on known_senders (moves into Action), its own reset rules when the opposite correction arrives, and Junior: corrections writing it. Recorded as a setting with default 1 on the Junior page. Close by: adding the counter and the setting when a client asks for it, or deciding it stays fixed at one (the safe direction). Raised 11 October 2026. El Open
OI-108 The Console runner holds every tenant's vault password, unattended. ADR-0039 §5 gives the runner its own vault password files on octo-ctl so it can run jobs without a person present. That concentrates what ADR-0027 §3's per-tenant vault IDs spread out, and leaves open how a new tenant's vault password, generated by create tenant, reaches Bitwarden (ADR-0027 §11) without passing through the Console. Close by: deciding between a per-job fetch from a secrets manager, runner-held files with a tenant subset per job, or accepting the concentration with a documented compensating control; and a custody step for new vault passwords that never sends them to the registry. Raised 11 October 2026. Custody decided 15 October 2026 (El): a new vault password is shown once to the approver after the passkey step-up, who stores it in Bitwarden. ADR-0042 §5 (accepted 15 October 2026) does it without the Console ever holding it in plain text: encrypted to a one-time key made in the approver's browser at approval, revealed there after a second step-up. The concentration itself stays open here, accepted for v1. El Open
OI-112 Job output stored in the registry must never hold a secret or client data. ADR-0039 §4 keeps each job's full Ansible output. Ansible prints whatever a task returns unless no_log is set, and a single missed no_log puts a token in the history for good, because the history is append-only. Close by: a no_log rule for every task touching a secret, checked in review; a redaction pass in the runner for vault-shaped and token-shaped strings before upload; and a test job whose output is checked for planted secrets. Raised 11 October 2026. El Open
OI-113 How the Console trusts a tailnet identity on the break-glass path. ADR-0040 §6 serves the Console over the tailnet with tailscale serve, admins only, for when Cloudflare is down. The Console must accept the tailnet identity only on that listener and never from a header that could arrive through the tunnel or the internet, or the break-glass becomes a bypass. Close by: a separate listener bound to the tailnet path only, identity read from tailscale serve's headers on that listener alone, an allow list of El's and Lachlan's tailnet logins, and a test that the same headers through the tunnel are refused. Also depends on OI-086: until the tailnet has least-privilege grants and lite-dev's share is removed, the break-glass listener is not enabled on any host carrying client data (ADR-0040 §6). Raised 11 October 2026. El Open
OI-119 Nothing outside n8n notices when n8n is down. Junior: watchdog runs inside the same n8n as the mailbox it watches, so when n8n stops, the watchdog stops too, and no alert is raised. Seen on lite-dev on 13 October 2026: n8n was down from 11:12 to 11:39 Melbourne time with no alert and no record. Once n8n came back, the first poll ran before the watchdog, so even the gap went unseen. The same applies to a host that is off or unreachable. Planned fix: the Console's link quiet (OI-116): the tenant link also runs inside n8n, so a stopped n8n stops its snapshots, and Fleet shows the mailbox as quiet within 15 minutes. That needs an alert email when a link goes quiet, sent by the Console through alert delivery (ADR-0036), so it reaches us when nobody is looking at Fleet. Close by: both built and proved by stopping lite-dev's n8n for 20 minutes outside a test round. Raised 13 October 2026. Recommendation, 13 October 2026: a pair, both sending from alerts@ through the ABOS alerts app (ADR-0036). (1) The Console emails when a tenant's link goes quiet (app.cli check-links, every 5 minutes from the host's cron; one email on quiet, one when heard again, append-only events). It catches n8n stopped or crashed, the link unpublished or failing, and a host cut off while the Console is up; it misses the whole host down, because on lite-dev the Console is on the same host, and it misses the Console or cloudflared being down. (2) A watcher on octo-ctl (infra/scripts/host_watch.py, cron every 5 minutes) asks each host's n8n /healthz over the tailnet and emails after two failed runs, and again when it answers. It catches the whole host down, n8n down and the tailnet path broken; it misses a link that fails while n8n is up (pair 1 covers that), and it is silent if octo-ctl itself is down. Together they miss only octo-ctl and the tenant host failing together. octo-ctl is a Vultr VPS, always on and reached over the tailnet (El, 13 October 2026), and lite-dev is Vultr too, so in practice the one case the pair misses is a Vultr outage in a location the two share; a single host failing on its own is always caught by one half or the other. An external dead-man's switch (healthchecks.io or similar, pinged by the link or the Console) is not recommended now: it would email from its own domain, not alerts@, unless routed through something on the same host that is down; it is a new account and a new processor. It is the later fix for that case: Healthchecks.io (outside Vultr) watching octo-ctl, which the watcher pings each run, so a shared Vultr outage still raises an email. It needs an account and accepting its own sending domain for that one alert, so it is not built now; it may go away if the central notifier (ADR-0036 §5) runs outside Vultr. Built: both halves with tests, and the Ansible wiring for (1), inactive until the inventory has the alerts app's two IDs. Waiting on El: the two IDs in lite-dev's inventory (not secret; the secret is lite-dev's existing vault_abos_alerts_client_secret); a new client secret for octo-ctl on the alerts app, its config file and cron (runbook). 15 October 2026: the watcher saw lite-dev's n8n stop (10:24, logged down from 10:30), but every send failed with Graph 400: recipients in its config was one string "a,b", so it sent one "recipient" per character. El fixed the config. The watcher now refuses to start unless recipients is a list of single valid addresses, logs Graph's error code and message on any failed send (never the token), and has --test-email to prove the send without an outage. Close by: the watcher's incident and resolved emails both arriving in an n8n-down test, with the Console's half already tested 13 October. Closed 15 October 2026: lite-dev's n8n was stopped from 10:24 to 10:56 Melbourne time. Every alert path arrived: the octo-ctl watcher's incident and resolved emails, the Console's link quiet and heard again emails, and the watchdog's poll_gap_recovered advisory. Left as known limits: a shared Vultr outage taking out both octo-ctl and the host (Healthchecks.io is the later fix, above), and per-host secrets still in group_vars/all (OI-124). El Closed
OI-120 Two thresholds for a mailbox that has stopped polling. The Junior's not_polling alert fires after 60 minutes without a completed poll (product page). The Console shows a mailbox as overdue after 15 (CONSOLE_POLL_OVERDUE_MINUTES). Neither depends on poll_minutes, so a mailbox polled every 2 minutes can miss about 30 polls before anyone is emailed. Close by: one rule both sides use, derived from poll_minutes, and a recovered-gap alert so an outage that ends before the watchdog runs is still reported. Proposed 13 October 2026, not yet agreed. Raised 13 October 2026. Update 13 October 2026: agreed and built as drafts: overdue is the larger of 15 minutes and four polls in the watchdog, the poll's gap check and the Console (poll_minutes in the health snapshot); poll_gap_recovered reports a gap once, after the fact. Closes when the drafts are published and a stopped n8n produces exactly one recovered-gap email. El In progress
OI-105 Alert-sending secrets are rotated by hand, and nothing warns before one expires. Each host holds its own client secret for the Entra app ABOS alerts (ADR-0036), 12 months at most. An expired secret stops that host's alerts silently: the failure is recorded in alerts as not sent, and nobody reads that table unprompted. lite-dev's secret is in the vault as vault_abos_alerts_client_secret, in inventory/group_vars/all/vault.yml (El, 13 October 2026; it was in no vault before, though the runbook said it was), and the Console's quiet link check reads it from there (OI-119, alerts configured yes on lite-dev). It moves to host_vars/lite-dev/vault.yml with OI-124. No playbook writes it into the n8n credential, so a rotation is three manual steps per host (runbook). Close by: an expiry check that warns at least two weeks ahead from somewhere that does not depend on the expiring secret (a Graph query of the app's passwordCredentials from the control plane, or a calendar entry per host as the minimum), and the n8n role writing the credential from the vault. Ends with the central notifier (OI-104), which removes per-host secrets. Raised 11 October 2026. El Open
OI-103 The sort log does not show when learned_noise shaped a decision. A learned_noise sender's email goes to the AI at the list-mail threshold, and the row records the AI's own decided_by (ai, below_threshold, ...), so a report cannot tell how often a learned rule let an email leave Action. Close by: recording the rule that set the threshold (for instance a threshold_reason column: list_mail, learned_noise, unknown_sender), set in Apply the rules and written by Junior: record. Raised 10 October 2026, stage 6. El Open
OI-102 A thread-level action counts as one correction per message. Gmail applies "move to inbox" or a label change on a conversation to every message in it, so moving one thread of three sorted emails back to the inbox records three corrections. Learning is unaffected (one sender), but corrected_share and corrections_increase are inflated, and the alert thresholds are judged against inflated counts. Close by: counting at most one correction per thread per poll in Junior: corrections (the sort log holds thread_id), or counting threads in the watchdog's metrics; decide which with the first real correction data. Raised 10 October 2026, stage 6. El Open
OI-101 Correction alerts across every client (ADR-0034 global scope) are not built. A rise in corrections across all clients at once points at a provider-side model change, invisible client by client. Junior: watchdog sees one n8n instance, which is one tenant, so it cannot compute it; alert_rules.scope accepts only tenant for now. Close by: a central job (the control plane or Metabase over per-tenant monthly counts, never content) that reads each tenant's correction and sorted counts and evaluates the same rule shape with scope = global. Out of scope for stage 6 (El, 10 October 2026). El Open
OI-100 The correction alert thresholds are placeholders. corrections_creeping fires above 15% of sorted email corrected over 7 days (advisory), corrections_spiking at 5 more corrections in a day than the day before (incident). Both are guesses with no data behind them. The BOS Console shows no correction rate below 20 emails sorted in a day ("too few emails"), agreed 13 October 2026 as part of the same placeholder set (CONSOLE_CORRECTION_RATE_MIN_SORTED). The watchdog's corrections_creeping has no such minimum yet: the watchdog's code reads an optional min_sorted on the rule's row (under it the rule neither opens nor resolves), but the column is parked here (El, 13 October 2026): without it the rule behaves exactly as before. Adding it means a min_sorted column on alert_rules, 20 on corrections_creeping. Replayed over 11 to 13 October it changes nothing, because the alert opened at 24 sorted; it matters on a quieter mailbox. Close by: setting them from the first month of real corrections on a client's mailbox, and deciding whether the creeping rule should count moved_to_inbox (a customer email archived) more heavily than a swap between Marketing and Receipts. Also to decide: count emails, not moves (raised 15 October 2026). Today every move a person makes is one correction, in the watchdog's corrected_share and corrections_increase, the tenant link's counts and the Console's rate. The first look through the masked view (ADR-0041) showed lite-dev's 8 bucket swaps over 7 days were 3 emails: sort_log rows 24 and 25 were dragged back and forth 7 times in testing. Proposal, not built: (1) The correction rate and both correction rules count distinct emails corrected, judged by final state: an email counts once if its last observed place (the last correction's to_bucket, Inbox or label removed) differs from where the Junior put it, and not at all if it ended back where it was sorted. It counts in the window of its last move. (2) Moves stay visible as their own count ("8 moves, 3 emails" in the corrections section and above the masked rows), since dragging back and forth can itself say the buckets are unclear, but they feed no rate or alert. (3) By type and by from and to bucket, each email is counted once, under its net path (first from_bucket to final place). What it touches: the tenant link's counts (link_core.js, a new field in the payload, so the Console accepts it before the link sends it), the Console's rate and corrections section, the watchdog's two correction metrics (on a (next) copy first), and this item's thresholds, which would then be set on emails, not moves. Decided 15 October 2026 (El), the three defaults: an email moved to Inbox and back does not count (final state); a later move on the same email is the same correction, counted in the window of its last move; label_removed stays out of the rate, as today. Scheduled: still a proposal, to be built before tenant zero moves to production, so the first month of real corrections, which sets the thresholds above, is measured in emails, not moves. Until then nothing changes in the link, the Console or the watchdog. El Open
OI-099 Junior alerts email from the draft watchdog only: not yet published or wired as the error workflow. Send alert email and Send resolved email in Junior: watchdog post to Microsoft Graph sendMail as alerts@octopodia.com.au (ADR-0036) with the credential Graph: ABOS alerts (lite-dev). Tested 11 October 2026: a forced not polling was emailed on open and on resolve (Graph 202 both times, both received from alerts@). Close by: El publishes the watchdog, sets it as each mailbox workflow's error workflow (n8n refuses an unpublished one), and activates it once the stage 6 mailbox poll is stamping last_poll_ok_at. Raised 10 October 2026, stage 6. Closed 13 October 2026: the watchdog has been published and active since 11 October, it is the error workflow of Junior: mailbox (admin.octopodia), and the poll stamps last_poll_ok_at. El Closed
OI-098 The Junior's AI quota alert cannot tell a short rate limit from an exhausted quota. Gemini words both as RESOURCE_EXHAUSTED ... quota, so the free tier's per-minute limit, hit by a burst of test emails, opens ai_quota_or_key at once, though it clears by itself on the next poll. The product page says a rate limit should alert only if it persists. Close by: recording the AI error's kind in the pending row (Gemini's error details distinguish per-minute from per-day quota), or requiring two consecutive polls for quota and keeping at once for a rejected key or billing; check against a real quota and a real rate-limit response. Raised 10 October 2026, stage 6. El Open
OI-097 Junior: sorter tests holds copies of four sorter nodes, which can drift. The fixtures workflow runs copies of Read the email, Google verdict, Guardrail facts and Apply the rules from Junior: sorter, and since stage 6 a copy of the corrections core from Junior: corrections (also duplicated between that workflow's Touched emails and Decide nodes), because n8n cannot run one node of another workflow on fixed input. A change made in the sorter and not pasted into the tests workflow leaves the tests passing against old code. Its sticky note says so; nothing enforces it. Close by: a check that compares the Code node sources in the two exports (junior_sorter.json, junior_sorter_tests.json) and fails on a difference, run before each commit or in CI; or moving the rule steps into a sub-workflow both call. Raised 10 October 2026, stage 5. Since 11 October 2026 also the reset-learning core (in Junior: reset learning and the Reset cases fixture), and local copies of both cores with their tests in workflows/email-assistant-junior/tests/. El Open
OI-096 Rename guardrail_words to Senior's name once Senior's tenant config is defined. The Junior's legal and safety word lists live in an n8n data table called guardrail_words (one row per entry: list, phrase), per n8n instance, which is one tenant. Senior's docs say word lists are per tenant and live in config (rules) but give them no name or shape yet. guardrail_words is a working name, chosen 10 October 2026. Close by: when Senior's tenant config schema is written (OI-062), adopt its name and shape for these lists in the Junior too (table, sorter node, tests, review workflow, docs), so a client moving up keeps one set of lists. El Open
OI-095 ADR: the ABOS registry as source of truth for the Junior's settings. Decided 10 October 2026 and recorded on the Junior page: the registry holds poll_minutes, the confidence thresholds, label names, business_type and go_live; the n8n mailboxes table is a local copy refreshed from it and used as-is if the registry is unreachable; a client's n8n reads only its own tenant's records; client-owned builds with no registry link use the local table. Not yet an ADR. It must settle how and how often the copy is refreshed, which settings are per tenant and which per mailbox, how a change is pinned for in-flight work (ADR-0006), and how n8n reads the registry without breaking two writers only (ADR-0006). Write it with the BOS Console design. Design settled by ADR-0038 (accepted 11 October 2026): the tenant link pulls every 5 minutes into the local copy, a change applies from the next poll with settings_version recorded, per-tenant and per-mailbox settings are listed there, and the link writes no config. Closes in Console phase 2, when built. El Open
OI-090 n8n MCP access is on for lite-dev; never enable it on a client host without a decision. The n8n MCP server lets an AI agent read and change workflows, data tables and executions, and run workflows with their credentials. It was switched on for lite-dev to build stage 4b of the Junior (October 2026), where it holds test data only. On a client's n8n it would give an agent the run of their mailbox credentials and stored data. Close by: an explicit decision (likely an ADR) on whether, when and with what scope it may be enabled on a client host, and an Ansible default that leaves it off. Until then, off on every client host. El + Lachlan Open
OI-089 Agree the minutes the Junior saves per email, for n8n Insights. Built in stage 6 (10 October 2026): the mailbox workflow counts time saved once per email sorted and labelled, in its own top-level production runs (Insights skips sub-workflow and manual runs; unsaved runs still count, checked against the n8n 2.42.4 source). Minutes per email are per-mailbox settings in mailboxes: minutes_saved_action 0, minutes_saved_marketing 0.5, minutes_saved_receipts 0.5. These are placeholders. The Time saved node takes only a fixed number, so Minutes saved emits one item per tenth of a minute and the node counts 0.1 each (resolution 0.1 minute a poll). Close by: agreeing real figures (a starting estimate, then checked against a client's own sense of it in the settling-in month), and confirming in the n8n UI that the mailbox workflow's Estimated time saved is set to dynamic, which the MCP cannot set. Use the same figures in the monthly report and on the product sheet only once backed by real numbers. Raised by El, 9 October 2026. El Open
OI-088 The client's Internal Google app has not been rehearsed. lite-dev connects a plain Gmail account through an External app (published, unverified), because Internal apps exist only inside a Google Workspace organisation. The first client is on Workspace, and their admin creating the Internal app (Junior onboarding step 1) is the step done least often and most likely to go wrong on the day. Close by: a Workspace 14-day free trial on lab.octopodia.com.au, the Internal app created there exactly as the client's admin will, the client-facing instructions written from doing it, an admin revoking access to confirm the sign-in rejected alert fires, then cancel before it bills. Before the first Junior client. El Open
OI-087 n8n on lite-dev has no backups. Everything the instance holds is in /opt/n8n/data: the SQLite database with the workflows, the encrypted credentials and the execution history. The runbook is explicit that lite-dev carries test data only until this is closed. Close by: adding the folder to the backup role (needs OI-054, the age keypair, and OI-036, the AU bucket), then the rehearsal from the Junior plan: restore onto a second host with the same encryption key, workflows deactivated, confirm a credential decrypts, then destroy it. Hard prerequisite for the first Junior client. El Open
OI-086 The tailnet's least-privilege policy, and Lachlan's break-glass, before production. Decided 15 October 2026 (El): Lachlan does not join the tailnet. His break-glass is the Vultr web console, which logs in as root with the root password Vultr set (in Bitwarden). Checked: hardening blocks root over SSH only (PermitRootLogin no) and never touches root's password; the admin users have no password, so root is the only console login. The hardening role now refuses to run unless root has a usable password (passwd -S root shows P), so public SSH can never close without that path. Checked on lite-dev 15 October 2026 (El): root shows P. Before step 10 of the phase 4 plan (not before step 2): Lachlan's own Vultr login with MFA and console access, a tested root login on lite-dev's web console, and his own YubiKey and a device passkey enrolled on the Console. What it changes: after phase 3 Lachlan has no SSH, only the web console, and the Console's tailnet break-glass (ADR-0040 §6) serves El alone; the host-setup runbook's phase 2 check for Lachlan becomes a web console login instead of SSH over the tailnet. The policy text is drafted for review in infra/tailscale/policy.hujson (not applied, El 15 October 2026; still to fill in: El's tailnet login and octo-ctl's SSH port): admins to tag:server on 52843 and 443, tag:ops (octo-ctl) to tag:server on 52843 and 443, tag:console to tag:server on 443, nothing else, with a tests block. Earlier text, kept for the record: Lachlan must be on the company tailnet before any production host reaches phase 3. After phase 3 there is no public SSH (ADR-0031), so an admin not on the tailnet has no way in at all, and the host depends on El alone: the bus-factor failure ADR-0027 exists to prevent. Deferred for lite-dev, a throwaway, where his key was verified as installed rather than by his login. For production it is a hard prerequisite: his own Microsoft account on the tailnet, made Admin, a Standard seat (US$8/month), and his login over the Tailscale address verified before ssh_public_access goes false. Until then the break-glass path is the Vultr web console with the root password. Related: OI-015. Also: lite-dev is currently shared into El's personal tailnet so his own device can reach n8n (October 2026). Acceptable for a test host with test data only; remove the share before it carries client data, and never share a production host out. The DevPod VPS also reaches lite-dev through that share, with an n8n MCP token and an --add-host=lite-dev.tail34193f.ts.net:100.116.238.22 entry in .devcontainer/devcontainer.json runArgs. Both are undone when the share is removed, and the token is rotated then. The tailnet policy is also still the default allow all, which lets any server reach every device, including the founders' machines: replace it with least-privilege grants (admins to tag:server on 52843 and 443, tag:ops to tag:server on 52843, servers initiate nothing except the Console's host to each tenant host on 443, for the masked view of ADR-0041; the Console's calls leave its host from the host's own tailnet IP, so the grant names that host, not the container) before production, with the policy's own tests block proving SSH still works. The Console's tailnet break-glass (ADR-0040 §6) waits on this: its listener stays off on any host carrying client data until the grants are in place and the share is removed. El Open
OI-085 The Tailscale role uses two Ansible features due for removal. Surfaced as deprecation warnings on its first real run (October 2026). ansible.builtin.apt_repository is replaced by deb822_repository, removed in ansible-core 2.25. Top-level facts such as ansible_distribution_release stop being injected as variables in 2.24; use ansible_facts['distribution_release']. Both run fine today on ansible-core 2.21. Fix before upgrading Ansible on octo-ctl, and sweep the other roles for the same ansible_* fact pattern at the same time, since they were written the same way. El Open
OI-084 The skills taxonomy and scheduling constraint vocabulary do not exist. ADR-0035 settles the model: connectors declare capabilities, we ask find_slots rather than reading a calendar, and three things are ours rather than the source system's. None of the three are defined. The skills taxonomy needs the same treatment as the canonical job: our labels, with each connector mapping theirs onto ours. The constraint vocabulary needs its facts published, the way rule facts are, covering working hours, day exclusions, travel buffers, skill-to-resource mappings and lead time. The slot shape needs a confidence field so a draft can tell "Dave is free" from "something is free". Also undesigned: re-checking availability at approval time rather than regenerating the whole draft, which is the better answer to proposed times going stale. El + Lachlan Open
OI-083 Alerting has an ADR but no metric catalogue and no default alert set. ADR-0034 settles the model: an alert is a rule over aggregates, reusing ADR-0004's {metric, operator, value} shape, evaluated on a schedule, with scope, kind and cooldown. What does not exist: the published metric catalogue (the equivalent of the fact catalogue for rules), the scheduled evaluation path, and the default alert set that ships enabled. That last one matters most, since a configurable system nobody configures is worse than a fixed one. Minimum viable set: abandoned_rate per tenant, parse_failure_rate global, and backup-did-not-run. El + Lachlan Open
OI-082 Attachments are undesigned, and document_request is nearly worthless without them. "Your CoC is ready, log in and find it" is not the reply anyone wants, so the highest-volume automatable category depends on attaching a real file. Needs three things: a document lookup capability fetching by job from the client's system (separate from the mail connector), an attach operation on the mail connector (provider-specific, like create_reply_draft), and attachments carried as draft_reply parameters rather than as a separate action, since the human still approves one draft. The human confirming it is the right document is the point, because a wrong attachment sent to a customer is worse than no reply. This also constrains mailbox mode: the attachment must land in their mail client with the draft, or they are downloading and attaching by hand, which is most of the saving gone. El Open
OI-081 create_job needs a per-system field map, and it must be a live schema read. Not just a name translation: it has to say which fields are mandatory for that system and what values are allowed, because a job-type dropdown means reading the client's own taxonomy. Gaps do not block the action: propose the job with what the email gave, mark what is missing, and the human completes the form at approval, which turns "we could not do this" into "finish this". Requires final_payload_hash on the approval record, since the human changed what we proposed. Whether it is worth building at all depends on their mandatory field count, and that is an OI-001 question: a prefilled form with two gaps beats typing into their system, and one with fourteen is a worse version of their own interface. El Open
OI-078 The eval set needs to reach 40 to 55 emails, meaning 3 to 5 per category, before a category-level regression is detectable. Launch is ~12 from the walkthrough, which catches gross breakage only. The gap closes on its own from client corrections and resolved unclear_category escalations, so it is time rather than labelling work, but the pipeline that captures those corrections into the eval set has to exist or they are lost. See eval. El Open
OI-077 n8n's brain timeout must be derived from the brain's own settings, not set by hand. It has to exceed llm_max_attempts × llm_request_timeout plus backoff, or n8n abandons a turn the brain is still working on and retries it. Two hand-set numbers drift, so template the n8n value from the brain settings in Ansible and assert the relationship at brain startup as a backstop. The failure produces duplicate work rather than an error, which is why it is worth designing out rather than relying on the idempotency key to catch it. See configuration: brain API. El Open
OI-076 No decided home or versioning scheme for prompt templates. docs/architecture/prompts.md says the prompt is a versioned template in the brain's source rather than in the docs, but not where in the source, nor how prompt_version is generated. Three questions: which directory (packages/, the brain's own tree, or elsewhere); whether the version is a hand-set string, a content hash, or a git ref; and whether prompts release with the brain or independently, since a prompt fix should be able to ship without a code deploy. Decide before the brain is built, because retrofitting a versioning scheme means every prior decision's prompt_version is meaningless. El Open
OI-073 The guardrail facts do not exist, and the word lists are guesses. The set itself is now written (docs/products/email-triage/rules.md): six guardrails, all facts, four of which remove close_item because the shared failure is silence. What is missing is the deterministic facts they need (mentions_legal_action, mentions_safety, mentions_insurer, max_amount_mentioned, classification_failed), and the per-tenant word lists, which need real emails to tune against. high_value may not earn its place: it fires on an amount in the email text, so it catches invoice disputes and misses large quote requests, since the expensive jobs are exactly the ones with no number in the email. Revisit after testing. El + Lachlan Open
OI-072 Which email-triage reports are built for v1, and where they surface. docs/products/email-triage/reporting.md lists what to monitor and what each number should trigger, but not what exists on day one. The cannot_determine breakdown and the outcome mix are the minimum, since without them a degrading system looks like a busy one. Decide what is a Console view, what is a monthly client report, and what is a query someone runs by hand. El + Lachlan Open
OI-071 The email-triage entity model predates the work item. docs/products/email-triage/data-model.md (31 July) was written before the work item existed. Its Message entity does the job now split between work_item (one conversation, our unit of work) and message (one email), and Classification and Draft hang off the wrong parent. The ER diagram at docs/architecture/diagrams/abos-er.svg is stale for the same reason. Reconcile against ADR-0032 before the schema is built, because a schema built from the old model needs a migration on day one. El Open
OI-050 Reconciliation pass to report brain version per host. Version skew across hosts is behaviour that differs by which box a tenant sits on, with nothing appearing broken (ADR-0030). Needs to be visible rather than inferred. El Open
OI-058 Tailnet ACLs restricting tag:server SSH to operators. Without an ACL, any device on the tailnet can reach the hosts. El + Lachlan Open
OI-041 Microsoft 365 break-glass account, deferred deliberately. Two global admins exist on Octopodia accounts, but both are working identities under the same conditional access policies, so a single bad policy could lock out both. Mitigation, in force now: both admins' MFA recovery codes in Bitwarden, at least two MFA methods per admin, and no tightening of conditional access policies until the break-glass account exists. Trigger: first paying client. (ADR-0027 §11) El Deferred
ID Item Owner Status
OI-021 Vanity-domain support clause in the service agreement. Accepting client vanity domains (ADR-0022 §5) puts their DNS in our uptime path. The agreement must name the Octopodia address as the supported one, with the vanity name offered as convenience. A liability boundary, not a naming detail. El Open
OI-022 "No review, no go" reflected in contract terms and liability caps before client use. The tagline is a commercial commitment, and OI-018 is the evidence artefact that backs it. El + Lachlan Open
OI-023 Data hosting region disclosure and consent in all client agreements (ADR-0008). The clause commits us to disclosing a region, not a vendor, which is what keeps provider switching open. El Open
OI-024 Bespoke dashboard cards priced with a maintenance component. They break when AroFlo changes a field, and someone has to fix them. Every one given away free is an annuity we pay. El Open
OI-037 Approval-log retention floor disclosed in the service agreement. ADR-0026 sets a floor a client cannot request below. Holding client-adjacent data longer than a client might prefer must be disclosed, not discovered. El Open
OI-038 Monthly audit report, scope and price. Built on the approval log. Offering it converts retention from internal policy into a contractual commitment. Becomes a roadmap item once scoped. El + Lachlan Open
OI-039 Scope boundary of "no review, no go" in client-facing language. The commitment covers AI-generated actions. Deterministic automation, such as scheduled syncs and provisioning, runs unapproved. A client could reasonably read the tagline as covering all automation, which would make the product unusable and the commitment unmeetable. Must be explicit in the agreement, not implied. El + Lachlan Open
OI-052 Price dedicated hosting at its true cost. A dedicated box is not "another VPS". ADR-0030 makes it conditional on moving the control plane to its own host and on private networking, both one-off engineering costs that land the first time a client asks. Treat the request as a project, not a plan upgrade. El + Lachlan Open

F. Cross-repository corrections

ID Item Owner Status
OI-044 octopodia-portal/docs/business/INFRASTRUCTURE.md says "EU by default." Directly contradicts ADR-0008. This document feeds client agreement language, so the contradiction has commercial consequences. It also carries a stale Trigger.dev reference and a multi-tenant connector claim inconsistent with ADR-0023. El Open
OI-045 octopodia-portal/docs/products/ai-employee/VPS-SETUP.md specifies Hetzner and EU regions, manual provisioning, and snapshot "golden image" cloning, all superseded by ADR-0008, ADR-0021 and ADR-0024. Correct it, or clearly mark it as historical. El Open

G. Decisions and documents still to produce

ID Item Owner Status
OI-107 How destructive Console jobs could be added later. ADR-0039 §3 keeps delete tenant, rotate secrets and rebuild host out of v1; they stay as break-glass runbooks. Doing them by hand more than once would make them jobs under the Console's own rule, so the gate needs designing before that happens. Close by: deciding the gate: a typed confirmation of the tenant slug, a second approver who is not the requester, a delay before it runs, and a pre-flight that checks a recent restorable backup exists (ADR-0026). Raised 11 October 2026. El + Lachlan Open
OI-109 The tenant link's route to the Console, and its tokens. ADR-0040 §5 sends the tenant link and the runner through the tunnel on a separate hostname, behind an Access service token plus their own token. Open: whether tenants on the same host should use an internal path instead, how a tenant's link token is issued at create tenant and placed in its n8n, and how it is rotated without a gap. Close by: a route decision in phase 1, and the issue and rotation steps written into the create tenant job. Raised 11 October 2026. Update 11 October 2026: the route is decided for v1: every tenant link, on the same host or not, uses link.octopodia.com.au through the tunnel with an Access service token and its own link token, so there is one path to secure and test. Built in phase 1 (runbook). Still open: issuing the token at create tenant, and rotating both without a gap. El In progress
OI-110 The Console's tunnel and Access applications sit on El's personal Cloudflare account. ADR-0040 §7. A personal account is owned by one person, the same problem ADR-0027 §6 solved for Bitwarden. Close by: moving the zone, the tunnel and the Access applications to a company Cloudflare account with both partners as owners, as part of the planned Cloudflare migration, and updating the vault's tunnel token. Raised 11 October 2026. El Open
OI-111 Adding a mailbox needs a person to sign in to Google. Gmail's OAuth consent cannot be automated, so the add mailbox job (ADR-0039 §2) and the phase 4 acceptance test have one human step. Close by: designing the hand-off: the job creates the credential shell, the Console shows a link to the tenant's n8n credential page, the tenant link reports when the credential works, and the job resumes. Raised 11 October 2026. El Open
OI-114 Publishing n8n workflows from the repository is unverified. Redeploy tenant (architecture) would import each workflow's JSON from main and publish it, replacing the hand-publishing done today. n8n 2.x keeps a draft and a published version, credentials are referenced by ID per instance, and sub-workflow IDs differ between instances. Close by: a proof on lite-dev that a workflow imported through n8n's API keeps its credentials and sub-workflow links and can be published, before phase 4 relies on it. Raised 11 October 2026. El Open
OI-115 The metric catalogue and how long aggregates are kept. ADR-0038 §7 lists the v1 Junior metrics in prose. The catalogue needs a single definition (names, types, periods) that the tenant link and the endpoint share, and the registry needs a retention and backup rule for aggregates (ADR-0026). Close by: writing the catalogue with phase 1, and a retention period, such as 13 months of hourly totals then monthly. Raised 11 October 2026. Update 11 October 2026: the catalogue is written (junior.v1, on the Console page) and shared by the endpoint and the link's tests. Still open: the retention and backup rule. El In progress
OI-118 The Console passkey recovery runbook is a stub. ADR-0040 §4 makes job approval need a passkey, and an operator who loses both passkeys while the other is away is re-enrolled by hand on the host (runbook). The recovery command, its one-time enrolment link and its break-glass history entry do not exist until phase 4. Close by: building the command in phase 4, running the runbook end to end on a test operator, and correcting the page. Raised 11 October 2026. 15 October 2026: the commands exist (passkey-invite for a first passkey, passkey-recover when both are lost), with tests, and the runbook is complete. Close by: running it once on lite-dev (step 2 of the phase 4 tests uses passkey-invite; passkey-recover once more there proves the rest). Closed 16 October 2026: run end to end on lite-dev by El: passkey-invite, two passkeys enrolled and a third refused, then one revoked, passkey-recover, two re-enrolled, and History marks every step break-glass. The same day passkey-recover was confirmed to work with a passkey still left (it revokes it too), and the runbook and passkey-invite's refusal now say so; before, both read as if recover were only for both lost. El Closed
OI-117 Confirm the Microsoft 365 plan, and whether Entra's "every time" sign-in works through Cloudflare Access. ADR-0040 §4 chooses a passkey step-up in the Console for job approval and keeps Entra Conditional Access sign-in frequency "every time" as the fallback. The fallback needs Entra ID P1 (in Microsoft 365 Business Premium, E3 and E5; not in Business Basic or Standard), and Octopodia's plan is not recorded. Microsoft documents "every time" as a full reauthentication including MFA; Cloudflare documents importing Entra authentication contexts into Access but not whether a session control is honoured on each Access sign-in. Close by: recording the plan; if it includes P1, proving on a test Access application that approval through an authentication context with "every time" prompts for MFA each time (allowing for Microsoft's five-minute skew). Does not block phase 4 while the passkey step-up is the gate; blocks phase 4 if the passkey step-up cannot be built. Raised 11 October 2026. El Open
OI-116 A quiet tenant link looks like a healthy one. If a tenant's link stops pushing, the Console sees no new numbers, which is also what a quiet mailbox looks like. ADR-0038 requires the Console to show it. Open: where that check runs, since it is about the platform rather than a tenant's aggregates (ADR-0034 §3), and who it notifies. Close by: a link silent check in the Console with a threshold, such as 15 minutes, shown on Fleet and notified through alert delivery (ADR-0036). Raised 11 October 2026. Update 11 October 2026: first form built: Fleet shows link quiet under Needs you when a mailbox's last snapshot is over 15 minutes old. Still open: notifying through alert delivery when nobody is looking at Fleet. El In progress
OI-104 Central alert notifier in the control plane. Tenant hosts should post alert events (rule, kind, tenant or mailbox reference, value, threshold, times; never content) to one notifier, which sends, so no tenant host holds a mail credential (ADR-0036 §5). The same component evaluates ADR-0034's global scope across tenants (OI-101). Open questions: the endpoint and its authentication from a tenant host, the event schema, retry and de-duplication when the control plane is unreachable (the host should keep the event and resend), and where events are stored. Until it exists each host sends directly. Raised 11 October 2026. El + Lachlan Open
OI-029 Approval granularity: per action or per batch. Per-action is defensible and will exhaust a client receiving forty invoices a day. El + Lachlan Open
OI-030 Does Greenhills get Metabase logins, or only curated views? Shapes how defensively the serving layer is modelled. Current lean is curated, since interactive embedding is Pro tier and bills per signed-in viewer. El Open
OI-031 Live numbers or a monthly snapshot for Octopodia's own value reporting? El + Lachlan Open
OI-032 Ratify the reporting and dashboard ADR (docs/adr/DRAFT-reporting-and-dashboards.md). Blocked on OI-001, OI-002, OI-003. El + Lachlan Blocked
OI-067 Is the action catalogue per product or universal? draft_reply is email triage, create_job arguably is not. Universal means a tenant's brain could propose actions their n8n does not implement. Per-product means something must resolve which catalogue applies. Current lean: universal catalogue with per-tenant enablement in config, so it stays "config, not code" (ADR-0003) and the brain cannot propose an action the tenant has not switched on. Becomes concrete at the second product. El + Lachlan Open
OI-047 Tenant migration runbook, shared host to dedicated host. Not yet written. Sequence: lower the {tenant}.app TTL to 60 seconds a day ahead, provision the tenant stack on the new box, restore from a per-tenant dump (ADR-0026), verify, stop writes on the old, run a final incremental sync, move the CNAME, hold the old stack cold for a rollback window, then restore the TTL. Caddy on the new host must know the hostname before the CNAME moves, or the certificate request fails. Write it before the first migration, not during. El Open
OI-048 Monitoring: what runs behind console.octopodia.com.au/health. A core_stack decision, deliberately deferred. The constraint is already fixed: on-box monitoring covers container state and metrics, but uptime checking must be external, because an on-box monitor cannot report that the box is down. Sentry already covers error alerting. El + Lachlan Open
OI-121 Console phase 2: how an operator responds to an alert. Recorded 13 October 2026 (El), no code and no ADR yet. Three responses, none deletes the alert: Seen, on it moves it to a Watching list on the tenant and returns it to Needs you if it worsens; Snooze until returns it on that date if it is still open; Not a real problem closes it with a reason (testing, expected, threshold wrong). The reasons feed the threshold items: OI-120 as asked, and OI-100, which holds the correction thresholds. Every response records who and when, visible to both operators. Snooze limits follow the alert's kind (ADR-0034): an advisory can snooze for weeks, an incident only for hours. One look link per alert type: corrections_creeping to the mailbox's corrections grouped by sender, a quiet link to the tenant's n8n executions, poll overdue to the runbook step. Anything that changes a tenant stays in phase 3, behind the passkey step-up. Open questions before an ADR: what counts as worsening for Watching, the exact snooze ceilings, where a response is stored (the registry, never the tenant's n8n), and whether a response is ever sent back to the tenant link. El Open
OI-122 The Fleet's why line shows an alert's value from when it opened, not now. Junior: watchdog writes value on the alerts row when an alert opens and when it resolves, never while it stays open, so the tenant link sends, and the Console shows, the opening figure: on 13 October 2026 corrections creeping said 16.7% while the real share was 27% (10 corrections over 37 sorted; an earlier report said 24%, 9 over 37, a miscount). Fix tested and in the watchdog's draft: each still-open alert's current value and threshold is written every run, with no email. Tested on Junior: watchdog (next) with its email branches disabled: the manual run at 05:32 UTC wrote 0.27 to the alert and sent nothing. Close by: El publishes Junior: watchdog, and the why line shows the current share within 10 minutes. Raised 13 October 2026. Closed 13 October 2026: published; the 05:40 run wrote 0.27 and El confirmed 27% on the Console. El Closed
OI-123 ADR-0041's open questions, before it can be accepted. ADR-0041 (proposed) shows the rows behind a correction count in the Console, fetched live from the tenant's n8n and never stored. (1) Cloudflare sees them: operators reach the Console through Cloudflare Access and a Tunnel, so the rows (sender addresses, dates, buckets) pass Cloudflare's edge decrypted. Either accept Cloudflare as a processor of this metadata, with its residency checked against ADR-0008, or serve the detail view only on the tailnet break-glass path (ADR-0040 §6). (2) Client notice: a line in the client agreement and privacy notice before any client tenant uses it. (3) The Console host on the tailnet, with the least-privilege grants of OI-086 (Console host to each tenant's n8n only). (4) Whether a row may carry a link to the email in Gmail (needs the message ID, which the Console otherwise never sees). Raised 13 October 2026. Closed 13 October 2026, not needed: ADR-0041 was rewritten as a masked view. Each line shows only the sort_log row number, the sorted and corrected times, the rule (decided_by) and the from and to bucket; no sender, domain, subject, message ID or reason, and the masking is done in the tenant's n8n before anything leaves it. None of those fields is personal information: the row number is an internal table ID that only leads to an email through the tenant's n8n over the tailnet, the rule and buckets are Octopodia's fixed vocabularies, and the times are the same kind of fact the Console already shows as hourly counts (ADR-0041 §6). So (1) Cloudflare carries nothing personal and the view works through it for both operators; (2) no client agreement or privacy notice change is needed, only a sentence in the service description; (4) a Gmail link is out, since it needs the message ID, and the email stays one click away in n8n. (3), the Console's host reaching each tenant's n8n with a least-privilege grant, is not a privacy question: it stays with OI-086 and is a build step of ADR-0041. El Closed
OI-130 Both repositories are owned by El's personal GitHub account. octopodia-abos and octopodia-abos-inventory are under zurelc (GitHub Pro), with Lachlan as a collaborator. A business's code and inventory should not depend on one person's account: its billing, its recovery, or who can add people. Close by: both repositories moved to a GitHub organisation owned by El and Lachlan, before the first paying client; remotes on octo-ctl and every clone updated, the runner's deploy key re-added, and links in the docs fixed (GitHub redirects a transferred repository, but not forever). Raised 16 October 2026. El Open
OI-124 lite-dev's machine secrets are in group_vars/all/vault.yml, which every host inherits. A second host added as things stand would receive lite-dev's n8n encryption key, its Cloudflare tunnel token (joining lite-dev's tunnel) and its alerts@ client secret, with no error. The runbooks said host_vars/<host>/vault.yml, but no such file exists; they now say where the secrets really are. Decided: group_vars/all/vault.yml holds only what every host shares; each host's own secrets go in inventory/host_vars/<host>/vault.yml, under that host's vault ID (known trap 15). This is where ADR-0036 §3 already puts the alerts secret. Close by: lite-dev's machine secrets moved into host_vars/lite-dev/vault.yml and removed from all, with the trap's check passing, before a second host enters hosts.yml. 15 October 2026: with several tenants per host, tenant secrets (n8n encryption key, link and detail tokens, the AI key) belong in tenants/<slug>/vault.yml under the tenant's own vault ID (ADR-0024, ADR-0027 §3), host secrets in host_vars/<host>/vault.yml; where those files live once the runner writes them is OI-126. Step 3 of the phase 4 plan. Raised 13 October 2026. 16 October 2026, built (phase 4 step 3, ADR-0042): the inventory is its own private repository, octopodia-abos-inventory, with host_vars/<host>/ and tenants/<slug>/ (the tenant's own vault ID); infra/scripts/vault_split.py moves today's secrets into it without printing one, and infra/scripts/inventory_check.py refuses any secret in group_vars/all but the tailnet key. Close by: El running the inventory repository runbook on octo-ctl, with the check passing and both playbooks changing nothing on lite-dev. El In progress
OI-125 Every alert email gives times in Melbourne time, never UTC ISO. One form everywhere: 15 Oct 10:25 Melbourne time. Done: the Console's link emails (13 October) and the octo-ctl watcher (15 October). Waiting: Junior: watchdog still writes At:, Open since: and a recovered gap's ends as UTC ISO. The change is built and tested (tests/wd_core.js, test_watchdog.js), tried on the copy Junior: watchdog (next) (9gj7k8B2dy4gMVp5), and on 15 October moved into the live watchdog's draft (version 8753668f): the diff against the published version is the Evaluate node's code only. Only the email text changes; stored times stay ISO. Published by El 15 October 2026 (version 8753668f, checked live). Close by: the next real watchdog email showing Melbourne time. Raised 15 October 2026. El In progress
OI-126 Where a job writes the inventory and a new tenant's vault file. Create tenant (ADR-0039 §2) makes an inventory entry, a vault ID and an encrypted vault file. ADR-0024 §4 keeps the inventory static and in git (diff, review, rollback, and recovery that does not need the control plane), and ADR-0039 §7 runs the runner from a clean main, so the runner can write neither. Today infra/inventory/hosts.yml is not in git at all. Options: (a) the runner writes the tenant's files on a branch and a person merges them before the playbook runs: every ADR kept, but a step outside the Console and a repository write key for the runner; (b) a separate private inventory repository the runner commits to, checked out at a recorded commit beside main: diff, history and rollback kept, everything started from the Console, supersedes ADR-0024 §4 and ADR-0039 §7 in part; (c) inventory and vault files kept only in the runner's own folder on octo-ctl: simplest, but no history or review and a single copy. Recommendation: (b). Also settle who writes the tenant record (ADR-0006 says Ansible; ADR-0024 §6 and the adding a tenant runbook have the Console write it and the playbook check it). Close by: ADR-0042, before step 3 of the phase 4 plan. Raised 15 October 2026. Decided 15 October 2026 (El): (b), a separate private inventory repository the runner commits to. Drafted as ADR-0042, which also has the Console create the tenant record at approval. Closed 15 October 2026: ADR-0042 accepted. Built in step 3 of the phase 4 plan. El Closed
OI-127 Where the production Console lives, and what happens to lite-dev's. The registry, the Console and cloudflared run on lite-dev, a test host, and console.octopodia.com.au points at it. Phase 4's acceptance test creates tenant zero on a new production host from the Console, so the production Console cannot depend on a throwaway host. Proposal: following ADR-0014 (core and tenants on one host to start), the production host carries the core stack, built by hand with Ansible, and creates tenant zero from its own Console; console.octopodia.com.au and the link hostname move to it; a separate runner identity serves it. To decide: whether lite-dev keeps a test Console on its own hostname (a new Access application) or is retired, and whether its registry (alert and view history) is migrated or the production registry starts fresh. Close by: a decision before step 10 of the phase 4 plan. Raised 15 October 2026. Decided 15 October 2026 (El): the production Console lives on the production host and takes console.octopodia.com.au; lite-dev's becomes the dev Console. Names approved 15 October 2026: console-dev.octopodia.com.au and link-dev.octopodia.com.au, each with its own Access application (the dev link with its own service token), so nothing on the dev Console shares a credential with production. Still to decide: migrate lite-dev's registry history or start production fresh. El In progress
OI-128 The n8n role runs one n8n per host; phase 4 needs one per tenant. ADR-0024 §1 makes a tenant an inventory host whose ansible_host is the box, so several tenants can share one. The n8n role has fixed names (/opt/n8n, container n8n, port 5678, tailnet HTTPS on 443). A second tenant on lite-dev, and tenants beside tenant zero on the production host, each need their own compose project, data folder, local port and tailnet HTTPS port (n8n's WEBHOOK_URL and the Google redirect URL then carry that port), plus the registry pre-flight gate (ADR-0024 §6). lite-dev's existing tenant must keep its names and data. Close by: step 5 of the phase 4 plan: a throwaway sandbox tenant beside lite-dev's own, with lite-dev's n8n and drafts untouched. Raised 15 October 2026. El Open
OI-129 Each new n8n instance needs its Google redirect URI added, and the app published, before its Gmail sign-in. The Gmail credential's redirect URL is per instance (https://<tailnet name>[:port]/rest/oauth2-credential/callback), so add mailbox on a new tenant fails at the sign-in until that URI is on the Google OAuth client. The n8n runbook also warns that a sign-in made while the app is in Testing expires after 7 days. A client's own Workspace uses an Internal app instead. Close by: add mailbox showing the exact redirect URI to add and refusing to ask for the sign-in until the operator confirms it is added; the production OAuth client set up and published for tenant zero. Raised 15 October 2026. El Open

H. Watch items, deliberately not now

ID Item Revisit when Status
OI-033 DuckDB-WASM and Parquet for the native headline layer. No backend, no JVM, no AGPL question, no per-tenant CVE patching. Costs: the whole file lands in the browser so row-level access control is impossible, it is a snapshot rather than a stream, and the charting layer becomes our build. After Greenhills ships. The serving views export to Parquet either way. Watch
OI-034 Control-plane migration to its own host. Triggers per ADR-0025 §3. On trigger Watch
OI-035 Interactive Metabase embedding, Pro tier. Unlocks client self-serve querying and white labelling, and bills per signed-in viewer. If self-serve becomes a sales blocker Watch

Closed items live in the Closed Items register.