{"type":"transition_applied","payload":{"type":"transition_applied","status":"running","residency_state":"resident"}}
{"type":"assistant_message","payload":{"text":"I read this as advisory architecture design - I’ll survey NutriCoach end to end, compare at least two minimal invite models, then deliver one decision-complete read-only design with states, gates, migration, and tests."}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":true}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"tool_execution","payload":{"tool":"read","is_error":false}}
{"type":"tool_execution","payload":{"tool":"bash","is_error":false}}
{"type":"assistant_message","payload":{"text":"I read this as advisory architecture design - I surveyed the invite, Telegram, onboarding, registry, activation, routing, and multi-customer boundaries and recommend a provisional first-claim flow that leaves the canonical customer registry unchanged until approval.\n\n## Current architecture and gap\n\nCurrent targeted flow:\n\n1. `dualcoach_admin customer invite --draft ...`\n2. `dualcoach_customer_invite.py` requires a complete `CustomerDraft`: customer key/ID, intended Telegram user, schedule, calories, protein, meals.\n3. `telegram_customer_bootstrap.py` stores a hashed `rc1_` token in a private, append-only, generation-CAS ledger; expiry is 24 hours.\n4. `/start rc1_...` must arrive in the intended customer’s private DM.\n5. `telegram_customer_bootstrap_registration.py` immediately writes a complete disabled `CustomerSpec` to `customers/registry.json`.\n6. Consent starts the 22-question onboarding.\n7. The canonical owner reviews and approves the questionnaire; finalization replaces the draft nutrition profile/plan and writes readiness artifacts.\n8. `dualcoach_activation_cutover.py` requires consent, readiness, membership/checklist evidence, capacity, registry identity, and committed activation receipts before enabling the customer.\n9. `NutritionCoachingCoordinator` routes only enabled customers on exact registered identities.\n\nThis already has strong token, consent, owner-review, activation, routing, journal, and crash-recovery gates. It cannot support first claim without either inventing false registry values or decoupling pre-approval onboarding from the registry.\n\n## Viable designs\n\n### Option A: provisional first-claim authority outside the registry — recommended\n\nKeep the bootstrap ledger authoritative until questionnaire completion and operator identity approval. Generate the customer key only when the link is claimed. Materialize one complete disabled registry customer during approval.\n\n**Trade-offs**\n\n- Coupling: localized to bootstrap, onboarding runtime, one admission orchestrator, finalization pins, and activation preflight.\n- Testability: state transitions and registry transaction recovery are independently testable.\n- Migration: no registry schema migration; old `rc1_` sessions normalize as targeted sessions.\n- Failure mode: approval spans bootstrap and registry files, requiring a small write-ahead recovery transaction.\n- Benefit: active routing and every existing registry consumer continue seeing only complete `CustomerSpec` records.\n\n### Option B: add `pending_customers` to the canonical registry\n\nClaim would append a minimal identity-only pending row. Approval would promote it to a complete customer.\n\n**Trade-offs**\n\n- Coupling: every strict registry reader, writer, migration, digest, receipt, cleanup path, and coordinator reload must preserve and validate the new namespace.\n- Testability: duplicate/capacity checks are simpler because they occur in one registry lock.\n- Migration: all registry versions and writers need coordinated changes.\n- Failure mode: one malformed pending row could make the production registry fail closed, interrupting unrelated active customers.\n- Benefit: avoids the cross-file approval transaction.\n\nOption B is viable but has the wrong blast radius. Pending invitations should not become production routing authority.\n\n## Recommended design\n\n### 1. Invitation contract\n\nAdd a distinct first-claim mode while preserving targeted invites:\n\n```text\nrc1_<22-char token>  targeted_v1, unchanged\nfc1_<22-char token>  first_claim_v1\n```\n\nCLI:\n\n```text\ndualcoach_admin customer invite --profile-root ... --first-claim --json\n```\n\n`--first-claim` and `--draft` are mutually exclusive. Existing `--draft` calls remain byte-for-byte compatible.\n\nFirst-claim output:\n\n```json\n{\n  \"customer_link\": \"https://t.me/<bot>?start=fc1_<token>\",\n  \"expires_at\": \"...\",\n  \"generation\": 1,\n  \"session_id\": \"cb_...\"\n}\n```\n\nIt contains no customer key/ID, identity, schedule, or nutrition target. The token remains 128-bit CSPRNG material; only its SHA-256 is persisted.\n\nIssuance validates private profile/config/registry files, bot username, owner identity, and that capacity is not already unavailable. This check is advisory; claim repeats it atomically.\n\n### 2. Bootstrap data additions\n\nUpgrade the bootstrap projection to `telegram-customer-bootstrap-v2`. Each normalized session gains:\n\n```text\ninvite_kind:\n  targeted_v1 | first_claim_v1\n\ncustomer_draft:\n  CustomerDraft | null\n\nassigned_customer_key:\n  string | null\n\ncapacity_reserved:\n  bool\n\nquestionnaire_schema:\n  targeted_nutrition_v2 | first_claim_nutrition_v1 | null\n\nquestionnaire_baseline_digest:\n  sha256 | null\n\nadmission_inputs_digest:\n  sha256 | null\n\napproval_transaction:\n  null | {\n    phase: prepared | committed,\n    approval_id,\n    customer_key,\n    claim_digest,\n    operator_user_id,\n    operator_chat_id,\n    operator_topic_id,\n    operator_message_id,\n    operator_update_id,\n    consent_handoff_digest,\n    questionnaire_baseline_digest,\n    admission_inputs_digest,\n    candidate_customer_digest,\n    registry_before_sha256,\n    registry_after_sha256,\n    approved_at_kst,\n    digest\n  }\n\nidentity_approval_digest:\n  sha256 | null\n\nready_digest:\n  sha256 | null\n```\n\n`RoleClaim` remains the authoritative Telegram binding. Usernames and Telegram display names are not identity authority.\n\nFor `first_claim_v1`, `customer_draft` and `assigned_customer_key` are null while `PREPARED`. Claim assigns a CSPRNG-generated key such as `nc_<22 chars>` after the link is opened.\n\n### 3. States\n\nExisting targeted flow remains:\n\n```text\nPREPARED\n-> REGISTERING\n-> AWAITING_CONSENT\n-> AWAITING_ACTIVATION\n-> ACTIVE\n```\n\nFirst-claim flow:\n\n```text\nPREPARED\n-> AWAITING_CONSENT\n-> ONBOARDING\n-> AWAITING_IDENTITY_APPROVAL\n-> FINALIZING\n-> AWAITING_ACTIVATION\n-> ACTIVE\n```\n\nTerminal states:\n\n```text\nCANCELLED | REJECTED | EXPIRED | FAILED | ACTIVE\n```\n\nRules:\n\n- `EXPIRED` applies only to unclaimed `PREPARED` links at `issued_at + 24h`.\n- A successful claim consumes the token permanently.\n- Consent decline becomes `CANCELLED`.\n- Operator rejection becomes `REJECTED`.\n- Revision returns `AWAITING_IDENTITY_APPROVAL -> ONBOARDING` and clears questionnaire/approval pins.\n- Safety hold remains `ONBOARDING`; identity approval is unavailable until the existing clinical/risk flow returns the questionnaire to owner review.\n\n### 4. Atomic first claim\n\nCreate one host-owned `FirstClaimAdmissionService` to coordinate the existing profile authority lock and bootstrap ledger. Do not spread capacity logic through Telegram handlers.\n\nClaim gate, under `profile_authority_lock` then bootstrap lock:\n\n1. Session is `first_claim_v1/PREPARED`.\n2. `now < expires_at`.\n3. Exact private DM: non-bot actor, no `sender_chat`, `user_id == chat_id`, topic `0`.\n4. Token hash and generation match.\n5. Actor is not:\n   - canonical owner or configured review operator;\n   - any existing customer, enabled or disabled;\n   - any assigned trainer;\n   - the intended or claimed identity of another nonterminal bootstrap session.\n6. Capacity occupancy is below the current registry limit.\n\nCapacity occupancy is the union of:\n\n```text\nenabled registry customer keys\n+\nassigned_customer_key for nonterminal first-claim sessions\n  where capacity_reserved = true\n```\n\nUsing a union prevents double-counting a customer if registry activation committed but bootstrap finalization is recovering.\n\nOn success, one ledger mutation stores the role claim, generates `assigned_customer_key`, sets `capacity_reserved=true`, advances generation, and enters `AWAITING_CONSENT`. Concurrent claimants receive the generic invalid/expired response. Invalid duplicate or capacity attempts do not consume the token.\n\n### 5. Consent and questionnaire\n\nFirst-claim consent is rendered from bootstrap authority, not `NutritionCoachingCoordinator`, because no registry customer exists yet. The current consent publication receipt, uncertain-send recovery, callback provenance, and exact private-DM checks remain.\n\nConsent grant transitions to `ONBOARDING`. Before every questionnaire mutation, runtime reloads and verifies:\n\n- bootstrap state and generation;\n- exact claimed route;\n- current consent handoff;\n- canonical owner route;\n- no duplicate claim identity.\n\nDo not disable authority validation globally. Add an explicit bootstrap-backed authority validator for `first_claim_v1`; targeted onboarding continues using `validate_current_registry_authority`.\n\nPersist questionnaire schema rather than an arbitrary field list:\n\n```text\ntargeted_nutrition_v2:\n  existing 22 QUESTION_FIELDS\n\nfirst_claim_nutrition_v1:\n  existing 22 fields, then:\n  display_name\n  starts_on\n  daily_time_kst\n  weekly_weekday\n  monthly_day\n```\n\nValidation:\n\n- `display_name`: normalized, 1–80 characters.\n- `starts_on`: ISO date; must still permit activation inside the existing 28-day KST plan window.\n- `daily_time_kst`: `HH:MM`.\n- `weekly_weekday`: 0–6.\n- `monthly_day`: 1–28.\n\nAt attestation, split the answers into:\n\n- unchanged `NutritionOnboardingBaseline`;\n- `FirstClaimAdmissionInputs`;\n- separate canonical digests.\n\nCalories, protein, macros, and meal structure are generated only after attestation from the baseline and meal count. No placeholders are ever written.\n\n### 6. Mandatory operator identity approval\n\nWhen the questionnaire reaches ordinary owner review, transition bootstrap to `AWAITING_IDENTITY_APPROVAL` and publish to the exact canonical owner route.\n\nThe card must display:\n\n- stable numeric claimant Telegram user ID;\n- requested display name;\n- requested schedule;\n- generated customer key;\n- baseline/plan summary;\n- explicit warning that the link may have been forwarded and this exact first claimant will become the customer.\n\nUse a new callback action, `claim_ok`, labelled **“Approve claimant and onboarding”**. Do not reuse the ambiguous targeted `owner_ok` branch.\n\nApproval requires the existing owner-route, member-presence, current-publication, message-ID, callback-generation, and consumed-update gates. It additionally rechecks duplicate identities, capacity, baseline/admission digests, consent, and claim generation.\n\n### 7. Complete disabled registry materialization\n\nApproval derives exactly one complete disabled `CustomerSpec`:\n\n```text\ncustomer_key         assigned_customer_key\ndisplay_name         questionnaire admission input\nenabled              false\ntelegram             exact RoleClaim route\ntrainer              null\nschedule             questionnaire admission inputs\nprofile              registry_profile(baseline)\nai_processing_consent exact consent handoff date + privacy-v1\nplan                 registry_plan(generate_initial_plan(...))\n```\n\nNo registry model change is needed.\n\nApproval transaction:\n\n1. Append `approval_transaction.phase=prepared` to the bootstrap journal with before/after registry hashes and exact candidate customer digest.\n2. Atomically append the complete disabled customer under `profile_authority_lock`.\n3. Reload and verify the complete registry and exact disabled row.\n4. Append `phase=committed`, store `identity_approval_digest`, and enter `FINALIZING`.\n5. Finalize existing readiness artifacts, pinning `identity_approval_digest`.\n6. Verify readiness and transition to `AWAITING_ACTIVATION`, storing `ready_digest`.\n\nStartup recovery compares registry bytes:\n\n- Equal to `registry_before_sha256`: apply the deterministic prepared mutation.\n- Equal to `registry_after_sha256` with the exact row: commit the approval.\n- Anything else: fail closed; no activation or automatic overwrite.\n\nThis is a local write-ahead transaction, not a general saga or new database.\n\n### 8. Activation gates\n\nFor `first_claim_v1`, `dualcoach_activation_cutover._preflight_bootstrap` must require all existing gates plus:\n\n- state exactly `AWAITING_ACTIVATION`;\n- committed approval transaction;\n- valid `identity_approval_digest`;\n- approval claimant equals the sole `RoleClaim`;\n- approval operator equals the current canonical owner triple;\n- approval baseline/admission digests equal current ready artifacts;\n- readiness receipt and pointer pin `identity_approval_digest`;\n- registry contains exactly one matching disabled complete row;\n- no duplicate identity now exists;\n- current capacity still permits conversion of this reservation to enabled.\n\nStaff membership evidence, activation audit, and nutrition activation receipt must also pin the identity approval digest. Direct `activate_customer`, stale CLI evidence, old generations, or manually created readiness artifacts cannot bypass it.\n\nTargeted sessions omit the new digest and continue through their existing intended-identity and owner-review gates. The compatibility branch must be explicit by `invite_kind`, never “missing receipt means legacy.”\n\n## Migration and rollback\n\n- Existing bootstrap v1 ledger/journal entries normalize to:\n  - `invite_kind=targeted_v1`\n  - `assigned_customer_key=customer_draft.customer_key`\n  - `capacity_reserved=false`\n  - no identity approval.\n- On the next mutation, append a v2 projection; never rewrite old journal events.\n- Existing `rc1_` links and targeted CLI invocations remain supported.\n- Existing onboarding documents missing `questionnaire_schema` map only to `targeted_nutrition_v2`.\n- Existing targeted readiness/activation receipts remain valid.\n- First-claim readiness is accepted only when explicitly marked with the new review flow and identity digest.\n- `customers/registry.json` requires no migration.\n- A default-off `room_bootstrap.first_claim_invites` flag controls new issuance and unclaimed `fc1_` links. Runtime code may continue or reject already-claimed sessions safely; disabling it does not reinterpret them as targeted.\n- Rejected/cancelled/failed first claims release their capacity reservation. Historical records remain private and auditable.\n\n## Required tests\n\n### Invite and claim\n\n- First-claim CLI output contains no customer key, identity, schedule, calories, protein, or meals.\n- Exact 24-hour boundary with a fake clock.\n- Private-DM-only; bot, group, channel, anonymous, and sender-chat updates do not consume the link.\n- Concurrent claimants synchronize on a barrier: exactly one winner, no timing sleeps.\n- Replay after successful claim is rejected.\n- Old `rc1_` targeted behavior remains green.\n\n### Duplicate and capacity\n\n- Reject owner, review operator, trainer, enabled customer, disabled customer, targeted intended identity, and another claimed identity.\n- Duplicate rejection leaves the token usable by an eligible claimant.\n- Capacity includes enabled customers plus reservations and does not double-count recovering activation.\n- N simultaneous claims for N−1 available slots produce exactly N−1 claims.\n- Legacy one-customer and configured 2–20 capacity policies both fail closed.\n\n### Questionnaire and approval\n\n- First-claim questionnaire cannot start before consent.\n- Exact claimed route only; no generic LLM fallthrough for any nonterminal state.\n- Schedule and customer key are created only after claim.\n- No nutrition targets exist before attestation.\n- Wrong owner, stale card, wrong message, changed membership, changed baseline, or changed schedule rejects approval without registry mutation.\n- Approval card and receipt pin the exact claimant.\n- Revision clears approval pins; rejection and cancellation release capacity.\n- Safety hold cannot expose `claim_ok`.\n\n### Transaction and activation\n\n- Crash before registry write, after registry write, and before approval commit recover deterministically.\n- Conflicting registry bytes fail closed without overwrite.\n- Concurrent approval callbacks produce one committed row.\n- Activation before identity approval, during `FINALIZING`, or with a tampered approval digest fails before calling profile activation.\n- Capacity and duplicate identity are rechecked at activation.\n- Ready/readiness/membership/activation receipt tampering is rejected.\n- End-to-end fake Telegram flow reaches `ACTIVE` only through:\n  `fc1 claim -> consent -> questionnaire -> owner claimant approval -> readiness -> activation`.\n- Source and installed-wheel tests exercise the same flow.\n- Logs/status documents remain answer-redacted and do not expose raw identities beyond the private operator card.\n\n## Decision\n\nUse Option A. It adds one bounded provisional authority and one recoverable local transaction while preserving the strongest existing boundary: the canonical registry contains only complete customers. Do not relax `CustomerSpec`, add nullable schedules/plans, or write placeholder targets.\n\nNo files were edited."}}
{"type":"transition_applied","payload":{"type":"transition_applied","status":"completed","residency_state":"resident"}}
{"type":"transition_applied","payload":{"type":"transition_applied","status":"completed","residency_state":"evicted"}}
{"type":"evicted","payload":{"cause":"evict"}}
