Skip to content

fix(api): strip email PII from auth log templates + explicit AllowAnonymous on logout - #325

Merged
thomasluizon merged 2 commits into
mainfrom
fix/api-pii-logs-logout-authz
Jul 12, 2026
Merged

thomasluizon merged 2 commits into
mainfrom
fix/api-pii-logs-logout-authz

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Removes email PII from the Sentry-captured (Warning-level) auth log templates and makes the Logout action's anonymous access explicit.

Changes

  • ResendContactsService — LogContactAdded / LogContactAddFailed drop {Email} and the raw response {Body} (which can echo submitted PII). The failure log keeps the HTTP status code; the now-orphaned response-body read is removed.
  • AuthController — LogFailedToSendCode / LogCodeVerificationFailed (both Warning, captured by Sentry) drop {Email}, keeping {Error} and the existing RequestId correlation.
  • GoogleAuthCommand — LogWelcomeEmailFailed logs {UserId} (the user's id) instead of {Email}.
  • AuthController.Logout — adds the explicit [AllowAnonymous] the csharp-authz rule wants (auth endpoints are exempt per CLAUDE.md).

Scope note

Deliberately scoped to the Sentry-captured Warning-level templates per the task. The two Information-level email logs in AuthController (LogVerificationCodeSent, LogUserLoggedInViaCode) are out of this change's scope and left untouched to avoid conflicting with other in-flight #243 work on this file.

Tests

  • New reflection test Logout_IsAnnotatedAllowAnonymous_SoItIsReachableWithoutAuth asserts [AllowAnonymous] is present and no [Authorize] overrides it.
  • Full Orbit.Infrastructure.Tests (1508) + Orbit.Application.Tests (2617) green locally; the existing ResendContactsServiceTests failure-path test still exercises the trimmed error log.

No API contract change (internal logging + authz attribute only), so no packages/shared/consumer edits.

Refs thomasluizon/orbit-ui-mobile#243

…nymous on logout

Warning-level auth logs are captured by Sentry, so the raw email address they
carried was PII landing in the error backend. Drop {Email} from the Sentry-captured
templates and keep the non-PII diagnostics:

- ResendContactsService: LogContactAdded/LogContactAddFailed drop {Email} and the
  raw response {Body} (which can echo submitted PII); keep the HTTP status code.
  The now-orphaned body read is removed.
- AuthController: LogFailedToSendCode / LogCodeVerificationFailed drop {Email},
  keeping {Error} and the existing RequestId correlation.
- GoogleAuthCommand: LogWelcomeEmailFailed logs {UserId} instead of {Email}.

Also add the explicit [AllowAnonymous] the csharp-authz rule wants on the Logout
action (auth endpoints are exempt per CLAUDE.md), with a reflection test asserting
logout is reachable without an authenticated session.

Refs thomasluizon/orbit-ui-mobile#243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Complete

Scope: PR #325 in thomasluizon/orbit-api — fix(api): strip email PII from auth log templates + explicit AllowAnonymous on logout
Recommendation: APPROVE

Severity Count
Critical (incl. old-client breaks) 0
High 0
Medium 0
Low / Info 0

Code Review: PR #325 (thomasluizon/orbit-api)

Scope: PR #325, fix/api-pii-logs-logout-authz to main
Recommendation: APPROVE

Summary

This PR strips email PII from four Sentry-captured Warning-level (and one Information-level) log templates in AuthController, GoogleAuthCommand, and ResendContactsService, replacing {Email} with {UserId} or nothing, and makes AuthController.Logout's anonymous access explicit via [AllowAnonymous]. A new reflection-based test asserts the attribute is present. No DTOs, routes, or contracts changed. This is the first review pass on this PR - no prior comments, reviews, or resolved threads exist.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

None (signal gate: Low/Info are not posted per rubric).

Subagents

Agent Verdict
security-reviewer PASS
contract-aligner N/A - no DTO, route, or shared-type surface touched by this diff (confirmed by PR body: "No API contract change")

Validation

Check Result
Build (dotnet) N/A - CI job, separate required check handles this
Tests (dotnet) N/A - CI job, separate required check handles this

Verification detail

Coverage contract: all 4 changed files given a verdict, ranked worst-first:

  1. src/Orbit.Api/Controllers/AuthController.cs (trust boundary, authz surface) - reviewed in full; confirmed [AllowAnonymous] on Logout is a lint/CI-hook-satisfying change, not a runtime behavior change - AuthController has no class-level [Authorize] and there's no global fallback policy in Program.cs/Extensions, so SendCode/VerifyCode/GoogleAuth/Refresh (unchanged) are already just as anonymously reachable today. LogoutSessionCommand uses the refresh token itself as the authorization credential (same pattern as /api/auth/refresh), so anonymous reachability is not a new risk.
  2. src/Orbit.Application/Auth/Commands/GoogleAuthCommand.cs - SendWelcomeEmailInBackground correctly threads the new userId param through to the log call while email remains used for its real purpose (SendWelcomeEmailAsync); no dead parameter.
  3. src/Orbit.Infrastructure/Services/ResendContactsService.cs - the now-orphaned response.Content.ReadAsStringAsync body read was correctly deleted alongside {Body}/{Email} removal from the log templates; email remains used in the outbound request payload.
  4. tests/Orbit.Infrastructure.Tests/Controllers/AuthControllerTests.cs - new reflection test correctly asserts [AllowAnonymous] present and [Authorize] absent on Logout.

Adversarial pass: no Critical/High findings were raised, so no skeptic challenge was required. Cross-model second opinion: not triggered (no Critical survivors). Completeness pass: one pass over the changed surface - nothing missed; the diff is self-contained (logging + one authz attribute + one test file).

Deferred - N/A dimensions & files not verdicted

  • DESIGN.md / AI-slop: N/A - no apps/* UI files in this diff (backend-only repo).
  • Parity: N/A - frontend-only dimension, orbit-ui-mobile not checked out; doesn't apply to a pure orbit-api diff.
  • i18n: N/A - frontend-only dimension; doesn't apply here.
  • Contract drift + backward-compat guard: N/A - diff contains no packages/shared or DTO hunks to compare (sibling repo not checked out, but the gate itself never fires since no DTO/route/contract field changed in this diff).
  • FEATURES.md parity: N/A - no user-facing feature surface changed; this is an internal logging + authz-annotation change only.
  • All 4 changed files received a verdict; nothing left un-reviewed.

What's good

  • PII minimization is precise and complete - every touched log call site was checked for leftover dead parameters (none found) and for whether the referenced field is still needed elsewhere (it is, where kept).
  • The [AllowAnonymous] addition is correctly reasoned as a no-op relative to actual runtime authorization, and is backed by a durable reflection test rather than a one-off manual check, guarding against future regression if someone later adds a stray [Authorize].
  • PR body explicitly scopes out the two Information-level email logs in AuthController to avoid conflicting with in-flight work (#243) - good hygiene, avoids scope creep into a file another change is actively touching.
  • RequestId correlation is preserved on every trimmed log line, so the debugging value lost by dropping {Email} is offset by request-level traceability.

Recommendation

No blocking issues. Approve and merge.

…e tests

The tests injected a NullLogger, whose IsEnabled(Information) is false, so the
success-branch LogContactAdded call never ran under test. Inject an enabled
substitute logger (matching production, where Information is enabled) so the
success path — and its now-PII-free log — is actually covered.

Refs thomasluizon/orbit-ui-mobile#243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Complete

Scope: PR #325 in thomasluizon/orbit-api — "fix(api): strip email PII from auth log templates + explicit AllowAnonymous on logout"
Recommendation: APPROVE

Severity Count
Critical (incl. old-client breaks) 0
High 0
Medium 0
Low / Info 1

Code Review: PR #325 (thomasluizon/orbit-api)

Scope: fix/api-pii-logs-logout-authz → main (5 files: AuthController.cs, GoogleAuthCommand.cs, ResendContactsService.cs, AuthControllerTests.cs, ResendContactsServiceTests.cs)

Summary

This PR strips {Email} from Warning-level (Sentry-captured) auth failure logs and one Error-level Resend log, replaces one welcome-email failure log's {Email} with {UserId}, and adds explicit [AllowAnonymous] to AuthController.Logout. Prior review: this repo's own bot already reviewed and APPROVED commit 24afb65 (the substantive diff) with zero findings. Since that approval, one incremental commit 5020e82 was pushed — test-only, fixing a SonarCloud coverage-gate failure (66.7% < 80% required) by swapping a NullLogger for an ILogger substitute with IsEnabled stubbed true, so the previously-untested success-path LogContactAdded branch is now exercised. I independently re-verified the full diff (not just the delta) rather than trust the prior pass blindly, and reached the same conclusion.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

  1. src/Orbit.Api/Controllers/AuthController.cs:353,359 — two Information-level logs still emit raw {Email} (LogVerificationCodeSent, LogUserLoggedInViaCode), on every successful send-code/verify-code call — higher volume than the Warning-level ones this PR fixed. Not a regression and not introduced by this diff; the PR body explicitly discloses and scopes this out ("out of this change's scope... to avoid conflicting with other in-flight #243 work on this file"). Flagged here for traceability only — does not block.

Independent re-verification detail

  • [AllowAnonymous] on Logout (AuthController.cs:259) — verified myself, not just trusted the PR description: AuthController has no class-level [Authorize] (AuthController.cs:16), and ServiceCollectionExtensions.cs:164-168's AddAuthorization registers only AdminPolicy — no FallbackPolicy/RequireAuthenticatedUser default. Under ASP.NET Core defaults an action with neither attribute is already anonymous-reachable, identical to the unchanged Refresh action (AuthController.cs:206, still no attribute). This addition is behavior-preserving (satisfies the csharp-authz lint intent, not a runtime change); [DistributedRateLimit("auth")] remains intact on Logout.
  • PII removal correctness — checked every touched call site for orphaned params/dead code: ResendContactsService.cs's now-unused response.Content.ReadAsStringAsync() was correctly deleted alongside {Body}; GoogleAuthCommand.cs's email param is still legitimately used for SendWelcomeEmailAsync (not logged) while the log call now takes userId. No leftover PII, no dead parameters.
  • New commit 5020e82 (test-only) — read the full ResendContactsServiceTests.cs. Swapping NullLoggerFactory for an NSubstitute ILogger with IsEnabled(Arg.Any<LogLevel>()) → true correctly makes the success-path LogContactAdded call (guarded by logger.IsEnabled(LogLevel.Information)) actually execute under test, which is exactly what the coverage gate was missing. No assertions in the other two tests depend on IsEnabled being false, so no collateral breakage.
  • Security-reviewer subagent dispatched independently on the three changed source files plus the AddAuthorization config — confirms the same conclusions above; its one MEDIUM candidate (the two untouched Info-level email logs) is the same pre-existing, PR-disclosed item captured as Low/Info above, not a new Medium.

Subagents

Agent Verdict
security-reviewer PASS (one pre-existing/disclosed Info-level observation, not a diff regression)
contract-aligner N/A — no DTO, route, or packages/shared surface touched; PR body confirms "No API contract change"

Validation

Check Result
Build (dotnet) N/A — CI job, separate required check
Tests (dotnet) N/A — CI job, separate required check

Prior review / thread check

One prior review exists from this bot (claude, state APPROVED, submitted against commit 24afb65) with a full detailed pass reaching the same no-findings conclusion this pass reaches. No requested-changes reviews or open threads to re-address. The only new activity since that approval is the coverage-fix commit 5020e82, reviewed fresh here.

Deferred — N/A dimensions & files not verdicted

  • DESIGN.md / AI-slop: N/A — no apps/* UI files (backend-only repo).
  • Parity / i18n: N/A — frontend-only dimensions, not verifiable from orbit-api (orbit-ui-mobile not checked out per task scope).
  • Contract drift + backward-compat guard: N/A — no DTO/route/shared-type field changed.
  • FEATURES.md parity: N/A — internal logging + authz-annotation change only, no user-facing feature surface.
  • All 5 changed files received a verdict (including the coverage-fix commit's sole file); nothing left un-reviewed.

What's good

  • PII minimization is precise: every touched log call site was checked for orphaned parameters and for whether the removed field is still needed elsewhere (it is, where kept) — no dead code introduced.
  • [AllowAnonymous] addition backed by a durable reflection test (Logout_IsAnnotatedAllowAnonymous_SoItIsReachableWithoutAuth) rather than a one-off manual check, guarding against a future regression.
  • PR body proactively discloses the Info-level email logs left out of scope and why (avoiding conflict with in-flight #243) — good hygiene, not scope creep.
  • The coverage-fix commit is minimal and targeted: it doesn't touch production code, only makes the existing test actually exercise the branch it already claimed to cover.

Recommendation

No blocking issues. Approve and merge. Optionally track the two Information-level {Email} logs (AuthController.cs:353,359) as a fast-follow once #243 lands, since the PR itself flags this as intentionally deferred.

@thomasluizon
thomasluizon merged commit 74c1184 into main Jul 12, 2026
19 checks passed
@thomasluizon
thomasluizon deleted the fix/api-pii-logs-logout-authz branch July 12, 2026 05:24
thomasluizon added a commit that referenced this pull request Jul 12, 2026
) (#331)

* fix(api): strip email PII from remaining email/marketing log sites (#243)

Removes recipient email addresses and raw provider response bodies from the
log templates that merged PR #325 left untouched, keeping only non-PII
diagnostics (subject, HTTP status, a user/correlation id, error):

- VerifyCodeCommand: LogWelcomeEmailFailed logs {UserId} instead of {Email}.
- ResendEmailService: LogEmailSent/LogEmailFailed/LogEmailSendException/
  LogSkippingTestEmail/LogMarketingRetry drop the recipient {To}; LogEmailFailed
  also drops the raw response {Body} (which can echo submitted PII) and keeps the
  HTTP status. The two now-orphaned response-body reads are removed.
- SendMarketingBroadcastCommand: LogPreviewSent logs {Subject} instead of the
  {TestEmail}; LogBroadcastQueued drops the redundant {QueuedAtUtc} timestamp
  (the logging framework already stamps each entry, and it tripped the tz rule).
- AuthController: the Information-level LogVerificationCodeSent drops {Email}
  (RequestId already correlates) and LogUserLoggedInViaCode logs {UserId}.

Adds capturing-logger tests asserting the email address and response body never
reach the rendered log line while status/subject/user id survive.

Refs thomasluizon/orbit-ui-mobile#243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(api): cover marketing/skip/exception log paths for PII scrub (#243)

Lifts new-code coverage over the SonarCloud gate by exercising the
IsEnabled-guarded log lines that existing NullLogger tests skip. Each new
case asserts the recipient email never reaches the rendered marketing log
line (success, retry/rate-limit, send exception, and both test-account skip
paths), completing the PII contract across every ResendEmailService log site.

Refs thomasluizon/orbit-ui-mobile#243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Sep 25, 2026
* Start #325 habit log duplicate recovery

* Recover duplicate habit logs without losing idempotency response
thomasluizon added a commit that referenced this pull request Sep 26, 2026
* Add habit widget empty reason (#527)

* Start #372

* Add habit widget empty reason

* ORB-223: Generate gating matrix from PayGateService (#520)

* chore: start ORB-223

* feat: generate gating matrix

* fix: fail closed on unreadable plan gates

* fix: close gating matrix parser gaps

* fix: preserve plans in combined guards

* fix: distinguish quota lifted plans

* fix: carry plan provenance through quota aliases

Derived quota variables were recorded unscoped and only the variable that
literally contains the plan ternary was relabelled afterwards, so plan
provenance did not survive a local alias. A semantics-preserving
`var selectedLimit = user.HasProAccess ? proLimit : freeLimit;
var messageLimit = selectedLimit;` generated cleanly and changed
CanSendAiMessage.quotaLiftedByPlan from "Pro" to null.

The directly plan-selected variables are now labelled first, and the
fixed-point propagation carries the source variable's exact plan instead of
null. An assignment deriving from two different plans cannot have its
provenance proven, so generation fails closed with the same
"cannot derive plan requirement" error the other unprovable shapes use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: fail closed on unknown gating sources

* fix: derive feature flags from migrations

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: fall back to Claude only when the Codex reviewer step fails (#529)

Mirrors thomasluizon/orbit-ui-mobile#1022.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let the executor own the MCP confirmation gate (#599) (#534)

* fix: let the executor own the MCP confirmation gate (#599)

Every MCP tool whose capability requires a confirmation or a step-up was
refused forever. The selective-auth middleware evaluated the policy a second
time, in front of the executor, and it never received the caller's
confirmation token. A client that stepped up and retried with a valid token
got the identical refusal, because the token never reached that evaluation.

Carrying the token into the middleware cannot fix it. A confirmation token is
single use and is bound to the pending operation's fingerprint, and the
middleware computes a different fingerprint from the raw MCP arguments than
the executor computes from the operation id and its snake_case argument
object. One token cannot satisfy two gates.

So the middleware now steps aside for a confirmation-gated capability, the
same way it already steps aside for execute_agent_operation_v2. Both reach
IAgentOperationExecutor, which evaluates access and confirmation together,
holds the token, and writes the audit row.

Two guard tests pin the invariants the deferral rests on: a confirmation
requirement always sits on a mutation, and every confirmation-gated MCP tool
reaches the executor through McpExecutorBridge.

Also corrected: the step-up message and the three tool parameter descriptions
named verify_step_up_agent_operation_v2 as the source of the confirmation
token. It does not return one. confirm_agent_operation_v2 does.

Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and
nothing ever read it; the real step-up state lives on
PendingAgentOperationState.StepUpSatisfiedAtUtc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: thread the confirmation token through the bulk habit tools (#599)

bulk_log_habits and bulk_skip_habits declared no confirmationToken
parameter and their shared helper hardcoded confirmationToken: null, so
their HabitsBulkWrite capability (FreshConfirmation) could never see a
token and both tools stayed permanently refused. Both now declare the
parameter and ExecuteBulkHabitOperationAsync forwards it.

Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests
pin what the old one missed. The scan now asserts it reaches exactly the
catalog's gated MCP tools, that each tool's forwarded operation id
resolves to a capability with the same ConfirmationRequirement, and that
each tool accepts and forwards a confirmation token. The source scan
walks subfolders and keys members by their declaration rather than by the
first invocation-shaped token in the chunk.

AgentOperationExecutor writes an AgentAuditLogs row before returning
UnknownOperation, restoring the trail the middleware used to leave.

McpConfirmationGateTests now drives the real AgentTools recovery methods
end to end and pins that all three refuse an API-key credential.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599)

The token guard whitelisted one spelling of null. It flagged a bridge call only
when the confirmation-token argument was absent or the literal `null`, so
`confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while
the tool stayed refused forever. A longer literal list does not close that,
because the next spelling is not on the list either.

Assert the structural property instead: a gated tool declares a
`string? confirmationToken` parameter, and the expression it forwards into the
executor's token slot resolves back to that identifier, directly or through one
helper hop. Every other expression fails, whatever it spells.

The declaration check now reads the tool's parameter list rather than its whole
body, so a local named `confirmationToken` no longer satisfies it.

Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It
compared only the confirmation requirement, so a gated tool could forward
another gated capability's operation id, keep confirmation firing, and have the
executor enforce the wrong scope.

The file's doc comment said four invariants and listed four; the file holds
five. Name the source-scan invariant the other three rest on.

Six mutations, each red on its own, real tree green at every step:
`confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool
forwarding a different local, and `delete_tag` forwarding `delete_goal`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: strip only a real named-argument prefix in the MCP guard parser (#599)

ArgumentAt treated any colon as a named-argument separator, so a conditional
expression in the token slot or the operation-id slot collapsed to its
else-branch. `ids.Count > 0 ? null : confirmationToken` read as
`confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read
as `"delete_tag"`, and both guards stayed green over the restored defect.

Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and
every other expression reaches the resolver whole. The anchor keeps a colon
inside a string literal and a `::` qualifier from matching.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: separate an omitted caller argument from a non-parameter expression (#599)

ResolveExpression returned the expression itself both when it was not a callee
parameter and when the caller supplied nothing at that index. The second case
handed back the helper's own parameter name, which then compared equal to
`confirmationToken` and passed. Giving the helper an optional
`string? confirmationToken = null` and calling it without that argument restored
the #599 defect with the guard green.

Return the `<omitted>` sentinel for the second case instead. It can never equal
the parameter name, and it resolves to no capability in the operation-id slot,
so both guards fail closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read a gated tool's direct and helper bridge calls together (#599)

CollectBridgeCalls returned early as soon as the tool body held one bridge
call, so a correct call in a dead branch hid every helper call in the same
tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken`
plus a same-file helper forwarding `null` restored the #599 defect with the
guard green.

Collect both sets instead. Every bridge call a gated tool can reach, directly
or through one helper hop, now has to forward the parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: refuse a gated tool that assigns to its own confirmationToken (#599)

The rule compared the forwarded expression's text only, so
`confirmationToken = null;` above the bridge call restored the #599 defect with
the forwarded identifier unchanged and the guard green.

Add an offender when the tool body, or a same-file helper it reaches, assigns
to the parameter. MemberSource now carries its statements with the parameter
list excluded, so the declaration's own `string? confirmationToken = null`
default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`,
`!=`, `>=`, `<=`, `??=` and a lambda arrow alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say what the MCP token resolver models, not more (#599)

Invariant 5 claimed "any other expression in the token slot refuses the tool
forever" and that the tool forwards the identifier "directly or through a
helper it calls". A conditional refuses the tool on some paths only, and the
resolver models exactly one same-file helper hop matched by argument position.

Replace both sentences with what the scan actually does, and state its limits:
two or more hops read as no bridge call and fail the routing guard, and
aliasing, reflection and an interface call are outside the model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: catch a compound assignment to confirmationToken (#599)

The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??`
of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the
bridge call compiled green with all five guards green, and a later change
that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would
hand the executor a token the MCP caller never sent: HasFreshConfirmation is
then satisfied from state the caller does not control, while the middleware
has already stepped aside.

Admit the two compound operators a `string?` can carry. `==` still fails on
the second `=`, `!=`, `>=` and `<=` still fail because those characters break
`\s*` and are not in the alternation, `=>` still fails on the trailing
character class, and a bare `??` with no `=` still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read every same-named helper an MCP tool could be calling (#599)

The member index was a dictionary keyed on the bare name and built with
group.First(), so two same-named members collapsed to whichever was declared
first. The scan then analysed a method the tool never calls. It broke both
ways: a decoy overload declared after the real helper and forwarding null was
green, and a gated tool calling a correct overload reddened when an unrelated
same-named member happened to be declared first.

Hold every member under its name and, at each call site, read every candidate
whose parameter count can admit that many arguments. One candidate resolves
the call; more than one is ambiguous, so all of them are read and any bad one
reddens the tool. That fails closed instead of guessing, and it costs no false
red in the case above, where only the real helper carries a bridge call. A
name that admits no candidate stays unresolved, which hides nothing: the
bridge call inside it is not collected either, so the routing guard reddens
the tool.

Arity, not the parameter type, is what decides a candidate. The scan is a text
scan, so it cannot type-check an argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say what invariant 5 now refuses and what it still cannot see (#599)

The general claim "refuses a tool that assigns to the parameter" is now exact:
`=`, `??=` and `+=`. The helper hop no longer claims a "matching argument
position", because the resolver pairs by position alone and strips a
named-argument prefix without reading the name, so arguments named out of the
declared order are read wrong. The comment said nothing about overloads, so
say that an ambiguous name is read as every candidate and reddens the tool.

Close with the honest limit: a text scan is defeatable by an author who sets
out to defeat it, and what the guard closes is every shape an ordinary
refactor produces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Fix calendar event timezone projection (#521)

* Start calendar timezone fix

* Fix calendar event timezone projection

* Fix projected calendar end times

* Fix calendar end time during DST fallback

* Fix calendar end time minute precision

* Fix projected calendar recurrence rule

* Guard calendar recurrence across timezone shifts

* Project calendar recurrence weekdays

* Keep calendar recurrence unchanged

* Omit unrepresentable recurring calendar events

* Gate projected calendar recurrence on the whole fetch window

Closes the six review findings on pull request 521.

1. The weekday gate now walks every expanded occurrence Google returned for a
   recurring master, not the one sampled instance. The fetcher collects each
   instance start with the source calendar's own offset into a JsonIgnore'd
   ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in
   January and shifts to Wednesday in July is refused. The empty list still
   falls back to the sampled instance for an all-day series and for a
   suggestion row read back from the database.
2. The legacy title plus date plus time key only excludes a suggestion when
   exactly one candidate carries it. Inside a fall-back repeated hour two
   events project to the same local time, so the key proves nothing and both
   stay. This mirrors the group.Count() == 1 guard the auto sync reconciler
   already applies to the same key.
3. An omitted end time now logs its reason at Debug with the event id and the
   user id. EndUtc already ships the real duration, so the client keeps it.
4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are
   reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule
   it imports and cannot re-express without inventing a weekday.
5. Both new call sites pass the logger and the user id to FindTimeZone, which
   also catches InvalidTimeZoneException so a corrupt zone stops escaping as a
   500. User.SetTimeZone trims at the boundary and rejects a blank id.
6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the
   sub hour offset gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Prove calendar recurrence stability from both zones

Round 6 gated a BYDAY series on the expanded instances Google returned, and
GoogleCalendarApi only asks for sixty days, so a series whose transition falls
outside that window was admitted and a stored suggestion row never carried the
instances at all.

The gate now reads the source calendar's own timezone from the recurring master
the fetcher already fetches, and walks a year of dates at the occurrence's source
wall clock through both zones' rules. A series it cannot prove stable is withheld
from both feeds. StoredCalendarEventJson carries the source zone beside the stored
suggestion, so the suggestion feed judges a row on the same evidence the events
feed had.

Also in scope: the auto-sync reconciler projects a fetched event into the account
timezone before matching a legacy habit, the end-time omission logs whatever EndUtc
holds, and an all-day event no longer reports Google's exclusive end date as an
end instant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Keep calendar series a spring forward gap cannot move

A probe date whose source wall clock a spring forward gap removes is a date
the series does not fire on, not evidence the series moves. The walk ended
there with "unproved" and the gate withheld the whole series, so an account
reading a calendar kept in its own zone lost an hour wide band in thirteen
DST zones even though the projection is the identity.

HasUnrepresentableRecurrenceAfterProjection now answers false as soon as
TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone
removes instead of ending. RFC 5545 section 3.3.5 does define the missing
case, but probing that normalized instant changes no decision for any of the
14,752 zone pairs that could distinguish it, and asserting an expansion
Google does not document would let one guessed date withhold a year.

Also pins what earlier rounds argued: auto-sync refusing to store or notify a
withheld series, JsonIgnore keeping SourceTimeZone off the response body, the
repeated hour needing both of its instants, the probe reaching a full year,
and an all day event logging no dropped end time. A stored SourceTimeZone key
holding a number now reads as no source zone rather than failing the request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fix recurring calendar timezone proof and legacy refresh

* Avoid refreshing legacy calendar rows with duplicate event IDs

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Bump FluentAssertions and 23 others (#538)

Bumps FluentAssertions from 8.10.0 to 8.11.0
Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277
Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25
Bumps Hangfire.Core from 1.8.24 to 1.8.25
Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12
Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12
Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0
Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1
Bumps OpenAI from 2.13.0 to 2.14.0
Bumps PostHog from 2.14.0 to 2.15.7
Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7
Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9
Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1
Bumps Stripe.net from 52.3.0 to 52.4.2

---
updated-dependencies:
- dependency-name: Google.Apis.AndroidPublisher.v3
  dependency-version: 1.76.0.4277
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.AspNetCore
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.OpenApi
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Design
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Relational
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.ApiDescription.Server
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Abstractions
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Http
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
  dependency-version: 8.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: OpenAI
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: PostHog
  dependency-version: 2.15.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: PostHog.AspNetCore
  dependency-version: 2.9.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Scalar.AspNetCore
  dependency-version: 2.17.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Sentry.AspNetCore
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Stripe.net
  dependency-version: 52.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Memory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Give API key management one authorization concept with two doors (#528)

* Start #529 API key step up

* Require API key management step up

* chore: regenerate the architecture map

The step up change moved the API key endpoints' shape, so `architecture.json`
and `architecture.html` no longer matched the tree and the `drift` check
failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`,
which reports 45 entities and 0 untested feature folders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Give API key management one authorization concept with two doors

Listing, creating and revoking API keys now read one grant. Two doors open
it, and both end in a six-digit code emailed to the account owner: the HTTP
challenge, and a verified agent step-up.

Creating key material spends the grant. Listing and revoking only read it,
so one emailed code authorizes a management session instead of exactly one
revoke. A revoke no longer locks the person out of the key list.

get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read
capability to a step-up inside the executor, so the read opens a pending
operation the caller can step up against, and the MCP read routes through
McpExecutorBridge like the mutation does.

AppConfigService now parses a stored row strictly and throws by name when a
row is malformed, so a typo cannot leave the gate off while the read-back
reports it on.

Both controller actions declare 403 and 428, and openapi.json carries them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Cover API key step up through merged MCP gate

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Expose habit emoji in MCP tools (#513) (#546)

* Open ticket 513 implementation review

* Expose habit emoji across MCP tools (#513)

* Prevent stale pull request workflow runs for #659 (#555)

* Start #659 workflow concurrency change

* Group core PR workflows by pull request for #659

* Group remaining PR workflows by pull request for #659

* Fix #657 logout revocation across a session family (#553)

* Start #657 session family fix

* Revoke the full auth session after logout (#657)

* Explain temporary legacy token allowance (#657)

* Handle duplicate refresh history race (#657)

* Fix #588 bulk habit interval weeks (#539)

* Start #588 bulk interval weeks fix

* Fix #588 bulk habit interval weeks mapping

* Guarantee crisis resources in Astra chat for #319 (#547)

* Add crisis guidance to Astra static prompt for #319

* Add curated crisis detection and regression cases for #319

* Wire crisis guard across chat delivery and metrics for #319

* Refine crisis delivery and cover fallback regression for #319

* Return static crisis support when AI chat fails for #319

* Keep crisis FAQ regression isolated for #319

* Fix crisis replies to bypass AI quota and provider

* Record streak freeze source for #571 (#540)

* Start #571 freeze source work

* Record streak freeze origin for #571

* Require explicit origin for new streak freezes

* Keep support tool available for support entry point (#541)

* Expose scheduled streak repair gap dates for #505 (#542)

* Fix standalone sub habit title validation for #628 (#543)

* Start #628 sub habit title validation fix

* Use sub habit title messages in standalone validation (#628)

* Carry the onboarding repeat interval into the created habit (#596) (#533)

A signed-out person who set a weekly repeat interval during onboarding lost
it at sign-up. ApplyHabitInput carried no interval field at all, so the apply
path dropped the value between the screen and the record.

Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it
to Habit.Create alongside the other schedule options. No existing field
changes name, shape or nullability, so a client that sends nothing keeps the
behaviour it has today: IntervalWeeks stays null and the habit repeats every
week.

Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules
range, the same 1 to 52 bound every other create path uses.

Regenerate openapi.json and architecture.json/html for the new field and for
the test class that now touches HabitScheduleService.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Expose recurrence time zone in calendar events for #591 (#544)

* Start #591 recurrence time zone work

* Expose recurring calendar event time zone for #591

* Restore recurrence time zone in legacy suggestions for #591

* Project safe calendar BYDAY recurrences (#569) (#545)

* Start calendar BYDAY projection (#569)

* Project uniform calendar BYDAY recurrences (#569)

* Cover alternate week recurrence projection (#569)

* Keep shifted calendar rules importable by installed clients

* Add closed week and year recap ranges for #369 (#548)

* Fix Google sign-in after concurrent user updates (#551)

* chore: start ticket 324 review

* fix: retry concurrent Google sign-in updates (#324)

* Fix duplicate agent confirmation consumption (#557)

* Start fix for ticket 671 confirmation race

* Claim agent confirmation once under concurrent saves

* chore: regenerate the architecture map for the confirmation claim migration (#671)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: show newest habit completion date in profile (#549)

* chore: start ticket 391

* fix: expose latest habit completion date in profile

* fix: exclude bad habit slips from last completion date

* fix: pass the freeze origin in the profile freeze test after #540 (#391)

The merge with main brought StreakFreeze.Create's required origin
parameter (#540). The test now asserts that a freeze without a
completion leaves lastCompletionDate null for both origins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: preserve last completion across habit cleanup (#391)

* Preserve descendant completions during sync cleanup

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Recover concurrent habit log duplicates (#325) (#552)

* Start #325 habit log duplicate recovery

* Recover duplicate habit logs without losing idempotency response

* chore(deps): bump the github-actions group across 1 directory with 5 updates (#530)

* chore(deps): bump the github-actions group across 1 directory with 5 updates

Bumps the github-actions group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `5` | `7` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` |
| [actions/setup-java](https://github.com/actions/setup-java) | `5` | `6` |
| [oasdiff/oasdiff-action/breaking](https://github.com/oasdiff/oasdiff-action) | `0.1.13` | `0.1.17` |



Updates `actions/upload-artifact` from 5 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v5...v7)

Updates `github/codeql-action/init` from 4.37.8 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...1c5b675)

Updates `github/codeql-action/analyze` from 4.37.8 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...1c5b675)

Updates `actions/setup-java` from 5 to 6
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](actions/setup-java@v5...v6)

Updates `oasdiff/oasdiff-action/breaking` from 0.1.13 to 0.1.17
- [Release notes](https://github.com/oasdiff/oasdiff-action/releases)
- [Commits](oasdiff/oasdiff-action@2649ebe...5e81b5c)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: oasdiff/oasdiff-action/breaking
  dependency-version: 0.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: restore optional response removal gate (#664)

* fix: keep the oasdiff levels file out of the repository root and allowlist .oasdiff.yaml (#664)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Thomas Luizon Rodrigues Gregorio <thomaslrgregorio@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ORB-101: verify Turnstile tokens on anonymous writes (#550)

* chore: open ORB-101 review branch

* feat: gate anonymous writes with optional Turnstile verification

* test: cover Turnstile gate and verifier failures

* test: assert valid Turnstile request returns success

* test: ground Turnstile responses in observed Siteverify results

Document web, Android, and landing activation checks for ORB-101.

* Fix Astra goal access for every plan (#677) (#560)

* fix: accept stored recap week anchors after preference changes (#683) (#559)

* fix: resolve stored recap week anchors without the current preference

A closed week anchor lives in the recap share link and in the stored
snapshot key. Validating it against today's WeekStartDay rejected a
Monday anchor after the person moved to Sunday, before the stored
snapshot could be read. Accept any supported week start (Sunday or
Monday) for an explicit closed anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: use closed week anchor for recap cache misses

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Prune idle Hangfire session connections for #564 (#554)

* Bump Scalar.AspNetCore from 2.17.9 to 2.17.10 (#558)

---
updated-dependencies:
- dependency-name: Scalar.AspNetCore
  dependency-version: 2.17.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Make unsubscribe token tampering test byte-accurate (#673) (#561)

* Make goal completion timestamp test deterministic (#676) (#562)

* Start #676 goal clock fix

* Make goal completion timestamp test deterministic (#676)

* chore: regenerate the architecture map for the goal clock test

The test now references HabitLog, so node tools/arch-map.mjs adds it to
the test's references. Two runs produce the same output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Pin Pullfrog reviewer to GPT 6 Sol medium (#636) (#537)

* Pin Pullfrog reviewer model and effort for #636

* Set Pullfrog effort to medium on Thomas's decision (#636)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Describe the pinned Pullfrog reviewer in the workflow comment (#636)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: run the pinned Pullfrog commit so GPT-6 Sol medium applies

The published pullfrog 0.1.82 has no gpt-6-sol entry, so the effort
input was dropped. The review step now runs the action's own code at
pullfrog/pullfrog main 405f60c2, whose model table has gpt-6-sol with a
medium rung. Return to @v0 once 0.1.83 is published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Take Pullfrog model and effort from the repository config (#565)

The app dispatch payload carries model and effort and outranks action
inputs (utils/payload.ts:320-326 at 405f60c2), so the inputs from #537
never applied (run 36187465385 resolved effort xhigh). Effort is now
0.25 (medium on GPT-6 Sol) in the Pullfrog repo config.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix relative reminders across local day boundaries (#566)

* chore: start reminder boundary fix for #695

* test: inject a fixed clock into relative reminder checks

* fix: send relative reminders on their local occurrence day

* chore: ratchet the scheduler suppression count and regenerate the map

#695 removed the two relative-reminder UTC-date suppressions, so the
closed allowlist now declares 2 ORBIT0004 sites in
ReminderSchedulerService.cs. The architecture map is regenerated for
the new clock dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: fire relative reminders at the first local due time across DST

Pullfrog review of 9ebf379: on a fall-back day a 01:30 due time is
ambiguous and ConvertTimeToUtc picked the later occurrence, an hour
after the old scheduler. A repeated hour now resolves to its first
occurrence, and a due time inside a spring-forward gap resolves to the
moment the clock jumps past it instead of being skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix bulk habit replay idempotency (#567)

* chore: open ticket 389 review early

* fix: make bulk habit writes replay safe

* fix: scope idempotency ledger by command content

* fix: scope idempotency ledger by request ordinal

* Use published Pullfrog v0 for Codex review (#568)

* Start #700 Pullfrog action update

* Use published Pullfrog v0 for Codex review

* ci: pin both Pullfrog steps to the v0 release commit

SonarCloud githubactions:S7637 flags a moving tag as an unpinned dependency.
Pin the commit the v0 tag named on 2026-09-26, release 0.1.83.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: align worker PR handoff with ticket orders (#570)

* Fix child habit title messages on update for #651 (#572)

* Fix explicit culture formatting for #704 (#569)

* chore: begin #704 culture formatting work

* fix: format #704 notification prompts logs and goal tools explicitly

* fix: enforce invariant formatting across #704 prompts and machine text

* test: cover invariant decimal formatting for #704

* chore: refresh architecture artifacts for #704

* test: cover remaining culture formatting paths for #704

* Fix last completion date after habit type changes (#574)

* fix: preserve habit log slip kind at write time

* test: include slip kind in concurrent log fixture

* fix: classify old habit logs during rolling deploys

* Clean stale text in API repository (#575)

* Use neutral names in test fixtures

* Make repository text timeless

* Remove disallowed narration comment

* Add timeless text gate to API repository (#576)

* Add timeless text gate to API repository

* fix: read comments in XML build files

* fix: match script end tags with attributes

* fix: end a script only at a real end-tag delimiter

* Prepare timeless cleanup for #715

* Preserve main culture formatting in merged code

* fix: sync the checker's quote and staged-blob fixes

* fix: let a reviewed directory entry exempt any rule but machine-path

* fix: sync the final timeless checker

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
thomasluizon added a commit that referenced this pull request Sep 26, 2026
* Add habit widget empty reason (#527)

* Start #372

* Add habit widget empty reason

* ORB-223: Generate gating matrix from PayGateService (#520)

* chore: start ORB-223

* feat: generate gating matrix

* fix: fail closed on unreadable plan gates

* fix: close gating matrix parser gaps

* fix: preserve plans in combined guards

* fix: distinguish quota lifted plans

* fix: carry plan provenance through quota aliases

Derived quota variables were recorded unscoped and only the variable that
literally contains the plan ternary was relabelled afterwards, so plan
provenance did not survive a local alias. A semantics-preserving
`var selectedLimit = user.HasProAccess ? proLimit : freeLimit;
var messageLimit = selectedLimit;` generated cleanly and changed
CanSendAiMessage.quotaLiftedByPlan from "Pro" to null.

The directly plan-selected variables are now labelled first, and the
fixed-point propagation carries the source variable's exact plan instead of
null. An assignment deriving from two different plans cannot have its
provenance proven, so generation fails closed with the same
"cannot derive plan requirement" error the other unprovable shapes use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: fail closed on unknown gating sources

* fix: derive feature flags from migrations

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: fall back to Claude only when the Codex reviewer step fails (#529)

Mirrors thomasluizon/orbit-ui-mobile#1022.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let the executor own the MCP confirmation gate (#599) (#534)

* fix: let the executor own the MCP confirmation gate (#599)

Every MCP tool whose capability requires a confirmation or a step-up was
refused forever. The selective-auth middleware evaluated the policy a second
time, in front of the executor, and it never received the caller's
confirmation token. A client that stepped up and retried with a valid token
got the identical refusal, because the token never reached that evaluation.

Carrying the token into the middleware cannot fix it. A confirmation token is
single use and is bound to the pending operation's fingerprint, and the
middleware computes a different fingerprint from the raw MCP arguments than
the executor computes from the operation id and its snake_case argument
object. One token cannot satisfy two gates.

So the middleware now steps aside for a confirmation-gated capability, the
same way it already steps aside for execute_agent_operation_v2. Both reach
IAgentOperationExecutor, which evaluates access and confirmation together,
holds the token, and writes the audit row.

Two guard tests pin the invariants the deferral rests on: a confirmation
requirement always sits on a mutation, and every confirmation-gated MCP tool
reaches the executor through McpExecutorBridge.

Also corrected: the step-up message and the three tool parameter descriptions
named verify_step_up_agent_operation_v2 as the source of the confirmation
token. It does not return one. confirm_agent_operation_v2 does.

Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and
nothing ever read it; the real step-up state lives on
PendingAgentOperationState.StepUpSatisfiedAtUtc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: thread the confirmation token through the bulk habit tools (#599)

bulk_log_habits and bulk_skip_habits declared no confirmationToken
parameter and their shared helper hardcoded confirmationToken: null, so
their HabitsBulkWrite capability (FreshConfirmation) could never see a
token and both tools stayed permanently refused. Both now declare the
parameter and ExecuteBulkHabitOperationAsync forwards it.

Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests
pin what the old one missed. The scan now asserts it reaches exactly the
catalog's gated MCP tools, that each tool's forwarded operation id
resolves to a capability with the same ConfirmationRequirement, and that
each tool accepts and forwards a confirmation token. The source scan
walks subfolders and keys members by their declaration rather than by the
first invocation-shaped token in the chunk.

AgentOperationExecutor writes an AgentAuditLogs row before returning
UnknownOperation, restoring the trail the middleware used to leave.

McpConfirmationGateTests now drives the real AgentTools recovery methods
end to end and pins that all three refuse an API-key credential.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599)

The token guard whitelisted one spelling of null. It flagged a bridge call only
when the confirmation-token argument was absent or the literal `null`, so
`confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while
the tool stayed refused forever. A longer literal list does not close that,
because the next spelling is not on the list either.

Assert the structural property instead: a gated tool declares a
`string? confirmationToken` parameter, and the expression it forwards into the
executor's token slot resolves back to that identifier, directly or through one
helper hop. Every other expression fails, whatever it spells.

The declaration check now reads the tool's parameter list rather than its whole
body, so a local named `confirmationToken` no longer satisfies it.

Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It
compared only the confirmation requirement, so a gated tool could forward
another gated capability's operation id, keep confirmation firing, and have the
executor enforce the wrong scope.

The file's doc comment said four invariants and listed four; the file holds
five. Name the source-scan invariant the other three rest on.

Six mutations, each red on its own, real tree green at every step:
`confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool
forwarding a different local, and `delete_tag` forwarding `delete_goal`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: strip only a real named-argument prefix in the MCP guard parser (#599)

ArgumentAt treated any colon as a named-argument separator, so a conditional
expression in the token slot or the operation-id slot collapsed to its
else-branch. `ids.Count > 0 ? null : confirmationToken` read as
`confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read
as `"delete_tag"`, and both guards stayed green over the restored defect.

Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and
every other expression reaches the resolver whole. The anchor keeps a colon
inside a string literal and a `::` qualifier from matching.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: separate an omitted caller argument from a non-parameter expression (#599)

ResolveExpression returned the expression itself both when it was not a callee
parameter and when the caller supplied nothing at that index. The second case
handed back the helper's own parameter name, which then compared equal to
`confirmationToken` and passed. Giving the helper an optional
`string? confirmationToken = null` and calling it without that argument restored
the #599 defect with the guard green.

Return the `<omitted>` sentinel for the second case instead. It can never equal
the parameter name, and it resolves to no capability in the operation-id slot,
so both guards fail closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read a gated tool's direct and helper bridge calls together (#599)

CollectBridgeCalls returned early as soon as the tool body held one bridge
call, so a correct call in a dead branch hid every helper call in the same
tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken`
plus a same-file helper forwarding `null` restored the #599 defect with the
guard green.

Collect both sets instead. Every bridge call a gated tool can reach, directly
or through one helper hop, now has to forward the parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: refuse a gated tool that assigns to its own confirmationToken (#599)

The rule compared the forwarded expression's text only, so
`confirmationToken = null;` above the bridge call restored the #599 defect with
the forwarded identifier unchanged and the guard green.

Add an offender when the tool body, or a same-file helper it reaches, assigns
to the parameter. MemberSource now carries its statements with the parameter
list excluded, so the declaration's own `string? confirmationToken = null`
default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`,
`!=`, `>=`, `<=`, `??=` and a lambda arrow alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say what the MCP token resolver models, not more (#599)

Invariant 5 claimed "any other expression in the token slot refuses the tool
forever" and that the tool forwards the identifier "directly or through a
helper it calls". A conditional refuses the tool on some paths only, and the
resolver models exactly one same-file helper hop matched by argument position.

Replace both sentences with what the scan actually does, and state its limits:
two or more hops read as no bridge call and fail the routing guard, and
aliasing, reflection and an interface call are outside the model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: catch a compound assignment to confirmationToken (#599)

The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??`
of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the
bridge call compiled green with all five guards green, and a later change
that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would
hand the executor a token the MCP caller never sent: HasFreshConfirmation is
then satisfied from state the caller does not control, while the middleware
has already stepped aside.

Admit the two compound operators a `string?` can carry. `==` still fails on
the second `=`, `!=`, `>=` and `<=` still fail because those characters break
`\s*` and are not in the alternation, `=>` still fails on the trailing
character class, and a bare `??` with no `=` still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read every same-named helper an MCP tool could be calling (#599)

The member index was a dictionary keyed on the bare name and built with
group.First(), so two same-named members collapsed to whichever was declared
first. The scan then analysed a method the tool never calls. It broke both
ways: a decoy overload declared after the real helper and forwarding null was
green, and a gated tool calling a correct overload reddened when an unrelated
same-named member happened to be declared first.

Hold every member under its name and, at each call site, read every candidate
whose parameter count can admit that many arguments. One candidate resolves
the call; more than one is ambiguous, so all of them are read and any bad one
reddens the tool. That fails closed instead of guessing, and it costs no false
red in the case above, where only the real helper carries a bridge call. A
name that admits no candidate stays unresolved, which hides nothing: the
bridge call inside it is not collected either, so the routing guard reddens
the tool.

Arity, not the parameter type, is what decides a candidate. The scan is a text
scan, so it cannot type-check an argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say what invariant 5 now refuses and what it still cannot see (#599)

The general claim "refuses a tool that assigns to the parameter" is now exact:
`=`, `??=` and `+=`. The helper hop no longer claims a "matching argument
position", because the resolver pairs by position alone and strips a
named-argument prefix without reading the name, so arguments named out of the
declared order are read wrong. The comment said nothing about overloads, so
say that an ambiguous name is read as every candidate and reddens the tool.

Close with the honest limit: a text scan is defeatable by an author who sets
out to defeat it, and what the guard closes is every shape an ordinary
refactor produces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Fix calendar event timezone projection (#521)

* Start calendar timezone fix

* Fix calendar event timezone projection

* Fix projected calendar end times

* Fix calendar end time during DST fallback

* Fix calendar end time minute precision

* Fix projected calendar recurrence rule

* Guard calendar recurrence across timezone shifts

* Project calendar recurrence weekdays

* Keep calendar recurrence unchanged

* Omit unrepresentable recurring calendar events

* Gate projected calendar recurrence on the whole fetch window

Closes the six review findings on pull request 521.

1. The weekday gate now walks every expanded occurrence Google returned for a
   recurring master, not the one sampled instance. The fetcher collects each
   instance start with the source calendar's own offset into a JsonIgnore'd
   ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in
   January and shifts to Wednesday in July is refused. The empty list still
   falls back to the sampled instance for an all-day series and for a
   suggestion row read back from the database.
2. The legacy title plus date plus time key only excludes a suggestion when
   exactly one candidate carries it. Inside a fall-back repeated hour two
   events project to the same local time, so the key proves nothing and both
   stay. This mirrors the group.Count() == 1 guard the auto sync reconciler
   already applies to the same key.
3. An omitted end time now logs its reason at Debug with the event id and the
   user id. EndUtc already ships the real duration, so the client keeps it.
4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are
   reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule
   it imports and cannot re-express without inventing a weekday.
5. Both new call sites pass the logger and the user id to FindTimeZone, which
   also catches InvalidTimeZoneException so a corrupt zone stops escaping as a
   500. User.SetTimeZone trims at the boundary and rejects a blank id.
6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the
   sub hour offset gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Prove calendar recurrence stability from both zones

Round 6 gated a BYDAY series on the expanded instances Google returned, and
GoogleCalendarApi only asks for sixty days, so a series whose transition falls
outside that window was admitted and a stored suggestion row never carried the
instances at all.

The gate now reads the source calendar's own timezone from the recurring master
the fetcher already fetches, and walks a year of dates at the occurrence's source
wall clock through both zones' rules. A series it cannot prove stable is withheld
from both feeds. StoredCalendarEventJson carries the source zone beside the stored
suggestion, so the suggestion feed judges a row on the same evidence the events
feed had.

Also in scope: the auto-sync reconciler projects a fetched event into the account
timezone before matching a legacy habit, the end-time omission logs whatever EndUtc
holds, and an all-day event no longer reports Google's exclusive end date as an
end instant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Keep calendar series a spring forward gap cannot move

A probe date whose source wall clock a spring forward gap removes is a date
the series does not fire on, not evidence the series moves. The walk ended
there with "unproved" and the gate withheld the whole series, so an account
reading a calendar kept in its own zone lost an hour wide band in thirteen
DST zones even though the projection is the identity.

HasUnrepresentableRecurrenceAfterProjection now answers false as soon as
TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone
removes instead of ending. RFC 5545 section 3.3.5 does define the missing
case, but probing that normalized instant changes no decision for any of the
14,752 zone pairs that could distinguish it, and asserting an expansion
Google does not document would let one guessed date withhold a year.

Also pins what earlier rounds argued: auto-sync refusing to store or notify a
withheld series, JsonIgnore keeping SourceTimeZone off the response body, the
repeated hour needing both of its instants, the probe reaching a full year,
and an all day event logging no dropped end time. A stored SourceTimeZone key
holding a number now reads as no source zone rather than failing the request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fix recurring calendar timezone proof and legacy refresh

* Avoid refreshing legacy calendar rows with duplicate event IDs

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Bump FluentAssertions and 23 others (#538)

Bumps FluentAssertions from 8.10.0 to 8.11.0
Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277
Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25
Bumps Hangfire.Core from 1.8.24 to 1.8.25
Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12
Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12
Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0
Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1
Bumps OpenAI from 2.13.0 to 2.14.0
Bumps PostHog from 2.14.0 to 2.15.7
Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7
Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9
Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1
Bumps Stripe.net from 52.3.0 to 52.4.2

---
updated-dependencies:
- dependency-name: Google.Apis.AndroidPublisher.v3
  dependency-version: 1.76.0.4277
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.AspNetCore
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.OpenApi
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Design
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Relational
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.ApiDescription.Server
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Abstractions
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Http
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
  dependency-version: 8.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: OpenAI
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: PostHog
  dependency-version: 2.15.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: PostHog.AspNetCore
  dependency-version: 2.9.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Scalar.AspNetCore
  dependency-version: 2.17.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Sentry.AspNetCore
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Stripe.net
  dependency-version: 52.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Memory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Give API key management one authorization concept with two doors (#528)

* Start #529 API key step up

* Require API key management step up

* chore: regenerate the architecture map

The step up change moved the API key endpoints' shape, so `architecture.json`
and `architecture.html` no longer matched the tree and the `drift` check
failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`,
which reports 45 entities and 0 untested feature folders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Give API key management one authorization concept with two doors

Listing, creating and revoking API keys now read one grant. Two doors open
it, and both end in a six-digit code emailed to the account owner: the HTTP
challenge, and a verified agent step-up.

Creating key material spends the grant. Listing and revoking only read it,
so one emailed code authorizes a management session instead of exactly one
revoke. A revoke no longer locks the person out of the key list.

get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read
capability to a step-up inside the executor, so the read opens a pending
operation the caller can step up against, and the MCP read routes through
McpExecutorBridge like the mutation does.

AppConfigService now parses a stored row strictly and throws by name when a
row is malformed, so a typo cannot leave the gate off while the read-back
reports it on.

Both controller actions declare 403 and 428, and openapi.json carries them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Cover API key step up through merged MCP gate

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Expose habit emoji in MCP tools (#513) (#546)

* Open ticket 513 implementation review

* Expose habit emoji across MCP tools (#513)

* Prevent stale pull request workflow runs for #659 (#555)

* Start #659 workflow concurrency change

* Group core PR workflows by pull request for #659

* Group remaining PR workflows by pull request for #659

* Fix #657 logout revocation across a session family (#553)

* Start #657 session family fix

* Revoke the full auth session after logout (#657)

* Explain temporary legacy token allowance (#657)

* Handle duplicate refresh history race (#657)

* Fix #588 bulk habit interval weeks (#539)

* Start #588 bulk interval weeks fix

* Fix #588 bulk habit interval weeks mapping

* Guarantee crisis resources in Astra chat for #319 (#547)

* Add crisis guidance to Astra static prompt for #319

* Add curated crisis detection and regression cases for #319

* Wire crisis guard across chat delivery and metrics for #319

* Refine crisis delivery and cover fallback regression for #319

* Return static crisis support when AI chat fails for #319

* Keep crisis FAQ regression isolated for #319

* Fix crisis replies to bypass AI quota and provider

* Record streak freeze source for #571 (#540)

* Start #571 freeze source work

* Record streak freeze origin for #571

* Require explicit origin for new streak freezes

* Keep support tool available for support entry point (#541)

* Expose scheduled streak repair gap dates for #505 (#542)

* Fix standalone sub habit title validation for #628 (#543)

* Start #628 sub habit title validation fix

* Use sub habit title messages in standalone validation (#628)

* Carry the onboarding repeat interval into the created habit (#596) (#533)

A signed-out person who set a weekly repeat interval during onboarding lost
it at sign-up. ApplyHabitInput carried no interval field at all, so the apply
path dropped the value between the screen and the record.

Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it
to Habit.Create alongside the other schedule options. No existing field
changes name, shape or nullability, so a client that sends nothing keeps the
behaviour it has today: IntervalWeeks stays null and the habit repeats every
week.

Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules
range, the same 1 to 52 bound every other create path uses.

Regenerate openapi.json and architecture.json/html for the new field and for
the test class that now touches HabitScheduleService.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Expose recurrence time zone in calendar events for #591 (#544)

* Start #591 recurrence time zone work

* Expose recurring calendar event time zone for #591

* Restore recurrence time zone in legacy suggestions for #591

* Project safe calendar BYDAY recurrences (#569) (#545)

* Start calendar BYDAY projection (#569)

* Project uniform calendar BYDAY recurrences (#569)

* Cover alternate week recurrence projection (#569)

* Keep shifted calendar rules importable by installed clients

* Add closed week and year recap ranges for #369 (#548)

* Fix Google sign-in after concurrent user updates (#551)

* chore: start ticket 324 review

* fix: retry concurrent Google sign-in updates (#324)

* Fix duplicate agent confirmation consumption (#557)

* Start fix for ticket 671 confirmation race

* Claim agent confirmation once under concurrent saves

* chore: regenerate the architecture map for the confirmation claim migration (#671)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: show newest habit completion date in profile (#549)

* chore: start ticket 391

* fix: expose latest habit completion date in profile

* fix: exclude bad habit slips from last completion date

* fix: pass the freeze origin in the profile freeze test after #540 (#391)

The merge with main brought StreakFreeze.Create's required origin
parameter (#540). The test now asserts that a freeze without a
completion leaves lastCompletionDate null for both origins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: preserve last completion across habit cleanup (#391)

* Preserve descendant completions during sync cleanup

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Recover concurrent habit log duplicates (#325) (#552)

* Start #325 habit log duplicate recovery

* Recover duplicate habit logs without losing idempotency response

* chore(deps): bump the github-actions group across 1 directory with 5 updates (#530)

* chore(deps): bump the github-actions group across 1 directory with 5 updates

Bumps the github-actions group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `5` | `7` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` |
| [actions/setup-java](https://github.com/actions/setup-java) | `5` | `6` |
| [oasdiff/oasdiff-action/breaking](https://github.com/oasdiff/oasdiff-action) | `0.1.13` | `0.1.17` |



Updates `actions/upload-artifact` from 5 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v5...v7)

Updates `github/codeql-action/init` from 4.37.8 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...1c5b675)

Updates `github/codeql-action/analyze` from 4.37.8 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...1c5b675)

Updates `actions/setup-java` from 5 to 6
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](actions/setup-java@v5...v6)

Updates `oasdiff/oasdiff-action/breaking` from 0.1.13 to 0.1.17
- [Release notes](https://github.com/oasdiff/oasdiff-action/releases)
- [Commits](oasdiff/oasdiff-action@2649ebe...5e81b5c)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: oasdiff/oasdiff-action/breaking
  dependency-version: 0.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: restore optional response removal gate (#664)

* fix: keep the oasdiff levels file out of the repository root and allowlist .oasdiff.yaml (#664)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Thomas Luizon Rodrigues Gregorio <thomaslrgregorio@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ORB-101: verify Turnstile tokens on anonymous writes (#550)

* chore: open ORB-101 review branch

* feat: gate anonymous writes with optional Turnstile verification

* test: cover Turnstile gate and verifier failures

* test: assert valid Turnstile request returns success

* test: ground Turnstile responses in observed Siteverify results

Document web, Android, and landing activation checks for ORB-101.

* Fix Astra goal access for every plan (#677) (#560)

* fix: accept stored recap week anchors after preference changes (#683) (#559)

* fix: resolve stored recap week anchors without the current preference

A closed week anchor lives in the recap share link and in the stored
snapshot key. Validating it against today's WeekStartDay rejected a
Monday anchor after the person moved to Sunday, before the stored
snapshot could be read. Accept any supported week start (Sunday or
Monday) for an explicit closed anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: use closed week anchor for recap cache misses

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Prune idle Hangfire session connections for #564 (#554)

* Bump Scalar.AspNetCore from 2.17.9 to 2.17.10 (#558)

---
updated-dependencies:
- dependency-name: Scalar.AspNetCore
  dependency-version: 2.17.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Make unsubscribe token tampering test byte-accurate (#673) (#561)

* Make goal completion timestamp test deterministic (#676) (#562)

* Start #676 goal clock fix

* Make goal completion timestamp test deterministic (#676)

* chore: regenerate the architecture map for the goal clock test

The test now references HabitLog, so node tools/arch-map.mjs adds it to
the test's references. Two runs produce the same output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Pin Pullfrog reviewer to GPT 6 Sol medium (#636) (#537)

* Pin Pullfrog reviewer model and effort for #636

* Set Pullfrog effort to medium on Thomas's decision (#636)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Describe the pinned Pullfrog reviewer in the workflow comment (#636)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: run the pinned Pullfrog commit so GPT-6 Sol medium applies

The published pullfrog 0.1.82 has no gpt-6-sol entry, so the effort
input was dropped. The review step now runs the action's own code at
pullfrog/pullfrog main 405f60c2, whose model table has gpt-6-sol with a
medium rung. Return to @v0 once 0.1.83 is published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Take Pullfrog model and effort from the repository config (#565)

The app dispatch payload carries model and effort and outranks action
inputs (utils/payload.ts:320-326 at 405f60c2), so the inputs from #537
never applied (run 36187465385 resolved effort xhigh). Effort is now
0.25 (medium on GPT-6 Sol) in the Pullfrog repo config.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix relative reminders across local day boundaries (#566)

* chore: start reminder boundary fix for #695

* test: inject a fixed clock into relative reminder checks

* fix: send relative reminders on their local occurrence day

* chore: ratchet the scheduler suppression count and regenerate the map

#695 removed the two relative-reminder UTC-date suppressions, so the
closed allowlist now declares 2 ORBIT0004 sites in
ReminderSchedulerService.cs. The architecture map is regenerated for
the new clock dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: fire relative reminders at the first local due time across DST

Pullfrog review of 9ebf379: on a fall-back day a 01:30 due time is
ambiguous and ConvertTimeToUtc picked the later occurrence, an hour
after the old scheduler. A repeated hour now resolves to its first
occurrence, and a due time inside a spring-forward gap resolves to the
moment the clock jumps past it instead of being skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix bulk habit replay idempotency (#567)

* chore: open ticket 389 review early

* fix: make bulk habit writes replay safe

* fix: scope idempotency ledger by command content

* fix: scope idempotency ledger by request ordinal

* Use published Pullfrog v0 for Codex review (#568)

* Start #700 Pullfrog action update

* Use published Pullfrog v0 for Codex review

* ci: pin both Pullfrog steps to the v0 release commit

SonarCloud githubactions:S7637 flags a moving tag as an unpinned dependency.
Pin the commit the v0 tag named on 2026-09-26, release 0.1.83.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: align worker PR handoff with ticket orders (#570)

* Fix child habit title messages on update for #651 (#572)

* Fix explicit culture formatting for #704 (#569)

* chore: begin #704 culture formatting work

* fix: format #704 notification prompts logs and goal tools explicitly

* fix: enforce invariant formatting across #704 prompts and machine text

* test: cover invariant decimal formatting for #704

* chore: refresh architecture artifacts for #704

* test: cover remaining culture formatting paths for #704

* Fix last completion date after habit type changes (#574)

* fix: preserve habit log slip kind at write time

* test: include slip kind in concurrent log fixture

* fix: classify old habit logs during rolling deploys

* Clean stale text in API repository (#575)

* Use neutral names in test fixtures

* Make repository text timeless

* Remove disallowed narration comment

* Add timeless text gate to API repository (#576)

* Add timeless text gate to API repository

* fix: read comments in XML build files

* fix: match script end tags with attributes

* fix: end a script only at a real end-tag delimiter

* Sync timeless checker with mobile main (#578)

* Sync timeless checker with mobile main

* fix: let a reviewed directory entry exempt any rule but machine-path

* fix: align timeless checker with reviewed version (#580)

Add regression coverage for hidden comments and introduced findings.

Refs thomasluizon/orbit-tickets#731

* Fix bulk habit replay plans across chat retries (#581)

* Fix bulk habit replay when selected chunks change

* Preserve bulk habit replay plans across retries

* Replay legacy bulk chunks and share replay planning

* Scope bulk replay plans by invocation and refuse legacy chat retries

* Fix bulk chat replay plan identity by invocation order

* Pin FluentValidation default messages to English (#582)

* fix: pin FluentValidation default messages to English

* chore: regenerate the architecture map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Return a code for habit ceiling failures (#583)

* fix: return a code when the habit ceiling is reached

* chore: regenerate the architecture map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix Google signup race during session save (#584)

* Fix Google signup race tracker cleanup

* Pin validator message test culture

* chore: generate architecture map outside git (#585)

* Cascade bulk habit deletion through subtrees (#587)

* fix: cascade bulk habit deletion through subtrees

* fix: keep bulk delete results aligned with requested habits

* Fix habit checklist reset and general-habit end dates (#586)

* Fix habit checklist, date, and reminder invariants

* Verify stored reminder JSON fields in migration test

* Keep habit checklist and end date fixes

* fix: keep database pool connections warm (#590)

* Complete error copy for synced habit guards

* Refresh gating matrix provenance after copy sync

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
thomasluizon added a commit that referenced this pull request Sep 27, 2026
* Add habit widget empty reason (#527)

* Start #372

* Add habit widget empty reason

* ORB-223: Generate gating matrix from PayGateService (#520)

* chore: start ORB-223

* feat: generate gating matrix

* fix: fail closed on unreadable plan gates

* fix: close gating matrix parser gaps

* fix: preserve plans in combined guards

* fix: distinguish quota lifted plans

* fix: carry plan provenance through quota aliases

Derived quota variables were recorded unscoped and only the variable that
literally contains the plan ternary was relabelled afterwards, so plan
provenance did not survive a local alias. A semantics-preserving
`var selectedLimit = user.HasProAccess ? proLimit : freeLimit;
var messageLimit = selectedLimit;` generated cleanly and changed
CanSendAiMessage.quotaLiftedByPlan from "Pro" to null.

The directly plan-selected variables are now labelled first, and the
fixed-point propagation carries the source variable's exact plan instead of
null. An assignment deriving from two different plans cannot have its
provenance proven, so generation fails closed with the same
"cannot derive plan requirement" error the other unprovable shapes use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: fail closed on unknown gating sources

* fix: derive feature flags from migrations

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: fall back to Claude only when the Codex reviewer step fails (#529)

Mirrors thomasluizon/orbit-ui-mobile#1022.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let the executor own the MCP confirmation gate (#599) (#534)

* fix: let the executor own the MCP confirmation gate (#599)

Every MCP tool whose capability requires a confirmation or a step-up was
refused forever. The selective-auth middleware evaluated the policy a second
time, in front of the executor, and it never received the caller's
confirmation token. A client that stepped up and retried with a valid token
got the identical refusal, because the token never reached that evaluation.

Carrying the token into the middleware cannot fix it. A confirmation token is
single use and is bound to the pending operation's fingerprint, and the
middleware computes a different fingerprint from the raw MCP arguments than
the executor computes from the operation id and its snake_case argument
object. One token cannot satisfy two gates.

So the middleware now steps aside for a confirmation-gated capability, the
same way it already steps aside for execute_agent_operation_v2. Both reach
IAgentOperationExecutor, which evaluates access and confirmation together,
holds the token, and writes the audit row.

Two guard tests pin the invariants the deferral rests on: a confirmation
requirement always sits on a mutation, and every confirmation-gated MCP tool
reaches the executor through McpExecutorBridge.

Also corrected: the step-up message and the three tool parameter descriptions
named verify_step_up_agent_operation_v2 as the source of the confirmation
token. It does not return one. confirm_agent_operation_v2 does.

Removed AgentPolicyEvaluationContext.StepUpSatisfied. Nothing ever set it and
nothing ever read it; the real step-up state lives on
PendingAgentOperationState.StepUpSatisfiedAtUtc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: thread the confirmation token through the bulk habit tools (#599)

bulk_log_habits and bulk_skip_habits declared no confirmationToken
parameter and their shared helper hardcoded confirmationToken: null, so
their HabitsBulkWrite capability (FreshConfirmation) could never see a
token and both tools stayed permanently refused. Both now declare the
parameter and ExecuteBulkHabitOperationAsync forwards it.

Three new guards in ConfirmationGatedMcpToolsRouteThroughExecutorTests
pin what the old one missed. The scan now asserts it reaches exactly the
catalog's gated MCP tools, that each tool's forwarded operation id
resolves to a capability with the same ConfirmationRequirement, and that
each tool accepts and forwards a confirmation token. The source scan
walks subfolders and keys members by their declaration rather than by the
first invocation-shaped token in the chunk.

AgentOperationExecutor writes an AgentAuditLogs row before returning
UnknownOperation, restoring the trail the middleware used to leave.

McpConfirmationGateTests now drives the real AgentTools recovery methods
end to end and pins that all three refuse an API-key credential.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: resolve the forwarded MCP confirmation token back to the tool parameter (#599)

The token guard whitelisted one spelling of null. It flagged a bridge call only
when the confirmation-token argument was absent or the literal `null`, so
`confirmationToken: default`, `null!`, `(string?)null` and `""` all passed while
the tool stayed refused forever. A longer literal list does not close that,
because the next spelling is not on the list either.

Assert the structural property instead: a gated tool declares a
`string? confirmationToken` parameter, and the expression it forwards into the
executor's token slot resolves back to that identifier, directly or through one
helper hop. Every other expression fails, whatever it spells.

The declaration check now reads the tool's parameter list rather than its whole
body, so a local named `confirmationToken` no longer satisfies it.

Also add `forwarded.Id == tool.Capability.Id` to the operation-id guard. It
compared only the confirmation requirement, so a gated tool could forward
another gated capability's operation id, keep confirmation firing, and have the
executor enforce the wrong scope.

The file's doc comment said four invariants and listed four; the file holds
five. Name the source-scan invariant the other three rest on.

Six mutations, each red on its own, real tree green at every step:
`confirmationToken: default`, `null!`, `(string?)null`, `""`, a gated tool
forwarding a different local, and `delete_tag` forwarding `delete_goal`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: strip only a real named-argument prefix in the MCP guard parser (#599)

ArgumentAt treated any colon as a named-argument separator, so a conditional
expression in the token slot or the operation-id slot collapsed to its
else-branch. `ids.Count > 0 ? null : confirmationToken` read as
`confirmationToken` and `tagId.Length > 0 ? "delete_goal" : "delete_tag"` read
as `"delete_tag"`, and both guards stayed green over the restored defect.

Match `^\w+\s*:(?!:)` instead, so only a leading `name:` prefix is stripped and
every other expression reaches the resolver whole. The anchor keeps a colon
inside a string literal and a `::` qualifier from matching.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: separate an omitted caller argument from a non-parameter expression (#599)

ResolveExpression returned the expression itself both when it was not a callee
parameter and when the caller supplied nothing at that index. The second case
handed back the helper's own parameter name, which then compared equal to
`confirmationToken` and passed. Giving the helper an optional
`string? confirmationToken = null` and calling it without that argument restored
the #599 defect with the guard green.

Return the `<omitted>` sentinel for the second case instead. It can never equal
the parameter name, and it resolves to no capability in the operation-id slot,
so both guards fail closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read a gated tool's direct and helper bridge calls together (#599)

CollectBridgeCalls returned early as soon as the tool body held one bridge
call, so a correct call in a dead branch hid every helper call in the same
tool. A decoy `if (tagId.Length == 0)` block forwarding `confirmationToken`
plus a same-file helper forwarding `null` restored the #599 defect with the
guard green.

Collect both sets instead. Every bridge call a gated tool can reach, directly
or through one helper hop, now has to forward the parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: refuse a gated tool that assigns to its own confirmationToken (#599)

The rule compared the forwarded expression's text only, so
`confirmationToken = null;` above the bridge call restored the #599 defect with
the forwarded identifier unchanged and the guard green.

Add an offender when the tool body, or a same-file helper it reaches, assigns
to the parameter. MemberSource now carries its statements with the parameter
list excluded, so the declaration's own `string? confirmationToken = null`
default does not match. The pattern `\bconfirmationToken\s*=[^=>]` leaves `==`,
`!=`, `>=`, `<=`, `??=` and a lambda arrow alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say what the MCP token resolver models, not more (#599)

Invariant 5 claimed "any other expression in the token slot refuses the tool
forever" and that the tool forwards the identifier "directly or through a
helper it calls". A conditional refuses the tool on some paths only, and the
resolver models exactly one same-file helper hop matched by argument position.

Replace both sentences with what the scan actually does, and state its limits:
two or more hops read as no bridge call and fail the routing guard, and
aliasing, reflection and an interface call are outside the model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: catch a compound assignment to confirmationToken (#599)

The assignment rule was anchored on `\s*=`, and `\s*` cannot cross the `??`
of `??=` or the `+` of `+=`. So `confirmationToken ??= tagId;` above the
bridge call compiled green with all five guards green, and a later change
that writes `confirmationToken ??= await ResolveStoredTokenAsync(...)` would
hand the executor a token the MCP caller never sent: HasFreshConfirmation is
then satisfied from state the caller does not control, while the middleware
has already stepped aside.

Admit the two compound operators a `string?` can carry. `==` still fails on
the second `=`, `!=`, `>=` and `<=` still fail because those characters break
`\s*` and are not in the alternation, `=>` still fails on the trailing
character class, and a bare `??` with no `=` still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read every same-named helper an MCP tool could be calling (#599)

The member index was a dictionary keyed on the bare name and built with
group.First(), so two same-named members collapsed to whichever was declared
first. The scan then analysed a method the tool never calls. It broke both
ways: a decoy overload declared after the real helper and forwarding null was
green, and a gated tool calling a correct overload reddened when an unrelated
same-named member happened to be declared first.

Hold every member under its name and, at each call site, read every candidate
whose parameter count can admit that many arguments. One candidate resolves
the call; more than one is ambiguous, so all of them are read and any bad one
reddens the tool. That fails closed instead of guessing, and it costs no false
red in the case above, where only the real helper carries a bridge call. A
name that admits no candidate stays unresolved, which hides nothing: the
bridge call inside it is not collected either, so the routing guard reddens
the tool.

Arity, not the parameter type, is what decides a candidate. The scan is a text
scan, so it cannot type-check an argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say what invariant 5 now refuses and what it still cannot see (#599)

The general claim "refuses a tool that assigns to the parameter" is now exact:
`=`, `??=` and `+=`. The helper hop no longer claims a "matching argument
position", because the resolver pairs by position alone and strips a
named-argument prefix without reading the name, so arguments named out of the
declared order are read wrong. The comment said nothing about overloads, so
say that an ambiguous name is read as every candidate and reddens the tool.

Close with the honest limit: a text scan is defeatable by an author who sets
out to defeat it, and what the guard closes is every shape an ordinary
refactor produces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Fix calendar event timezone projection (#521)

* Start calendar timezone fix

* Fix calendar event timezone projection

* Fix projected calendar end times

* Fix calendar end time during DST fallback

* Fix calendar end time minute precision

* Fix projected calendar recurrence rule

* Guard calendar recurrence across timezone shifts

* Project calendar recurrence weekdays

* Keep calendar recurrence unchanged

* Omit unrepresentable recurring calendar events

* Gate projected calendar recurrence on the whole fetch window

Closes the six review findings on pull request 521.

1. The weekday gate now walks every expanded occurrence Google returned for a
   recurring master, not the one sampled instance. The fetcher collects each
   instance start with the source calendar's own offset into a JsonIgnore'd
   ExpandedOccurrences list, so a Lisbon BYDAY=TH series that is stable in
   January and shifts to Wednesday in July is refused. The empty list still
   falls back to the sampled instance for an all-day series and for a
   suggestion row read back from the database.
2. The legacy title plus date plus time key only excludes a suggestion when
   exactly one candidate carries it. Inside a fall-back repeated hour two
   events project to the same local time, so the key proves nothing and both
   stay. This mirrors the group.Count() == 1 guard the auto sync reconciler
   already applies to the same key.
3. An omitted end time now logs its reason at Debug with the event id and the
   user id. EndUtc already ships the real duration, so the client keeps it.
4. The refusal here and the clamp in HabitScheduleService.IsMonthlyMatch are
   reconciled in a doc comment: Orbit clamps a rule it owns and refuses a rule
   it imports and cannot re-express without inventing a weekday.
5. Both new call sites pass the logger and the user id to FindTimeZone, which
   also catches InvalidTimeZoneException so a corrupt zone stops escaping as a
   500. User.SetTimeZone trims at the boundary and rejects a blank id.
6. Asia/Kathmandu at plus 05:45 and Pacific/Chatham at plus 12:45 cover the
   sub hour offset gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Prove calendar recurrence stability from both zones

Round 6 gated a BYDAY series on the expanded instances Google returned, and
GoogleCalendarApi only asks for sixty days, so a series whose transition falls
outside that window was admitted and a stored suggestion row never carried the
instances at all.

The gate now reads the source calendar's own timezone from the recurring master
the fetcher already fetches, and walks a year of dates at the occurrence's source
wall clock through both zones' rules. A series it cannot prove stable is withheld
from both feeds. StoredCalendarEventJson carries the source zone beside the stored
suggestion, so the suggestion feed judges a row on the same evidence the events
feed had.

Also in scope: the auto-sync reconciler projects a fetched event into the account
timezone before matching a legacy habit, the end-time omission logs whatever EndUtc
holds, and an all-day event no longer reports Google's exclusive end date as an
end instant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Keep calendar series a spring forward gap cannot move

A probe date whose source wall clock a spring forward gap removes is a date
the series does not fire on, not evidence the series moves. The walk ended
there with "unproved" and the gate withheld the whole series, so an account
reading a calendar kept in its own zone lost an hour wide band in thirteen
DST zones even though the projection is the identity.

HasUnrepresentableRecurrenceAfterProjection now answers false as soon as
TimeZoneInfo.HasSameRules holds, and the walk skips a wall clock its own zone
removes instead of ending. RFC 5545 section 3.3.5 does define the missing
case, but probing that normalized instant changes no decision for any of the
14,752 zone pairs that could distinguish it, and asserting an expansion
Google does not document would let one guessed date withhold a year.

Also pins what earlier rounds argued: auto-sync refusing to store or notify a
withheld series, JsonIgnore keeping SourceTimeZone off the response body, the
repeated hour needing both of its instants, the probe reaching a full year,
and an all day event logging no dropped end time. A stored SourceTimeZone key
holding a number now reads as no source zone rather than failing the request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fix recurring calendar timezone proof and legacy refresh

* Avoid refreshing legacy calendar rows with duplicate event IDs

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Bump FluentAssertions and 23 others (#538)

Bumps FluentAssertions from 8.10.0 to 8.11.0
Bumps Google.Apis.AndroidPublisher.v3 from 1.75.0.4246 to 1.76.0.4277
Bumps Hangfire.AspNetCore from 1.8.24 to 1.8.25
Bumps Hangfire.Core from 1.8.24 to 1.8.25
Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.11 to 10.0.12
Bumps Microsoft.AspNetCore.OpenApi from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Design from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Relational from 10.0.11 to 10.0.12
Bumps Microsoft.EntityFrameworkCore.Sqlite from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.ApiDescription.Server from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Abstractions from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.Memory from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Caching.StackExchangeRedis from 10.0.11 to 10.0.12
Bumps Microsoft.Extensions.Http from 10.0.11 to 10.0.12
Bumps Microsoft.IdentityModel.JsonWebTokens from 8.22.0 to 8.23.0
Bumps Microsoft.NET.Test.Sdk from 18.9.0 to 18.10.1
Bumps OpenAI from 2.13.0 to 2.14.0
Bumps PostHog from 2.14.0 to 2.15.7
Bumps PostHog.AspNetCore from 2.9.0 to 2.9.7
Bumps Scalar.AspNetCore from 2.17.1 to 2.17.9
Bumps Sentry.AspNetCore from 6.9.0 to 6.11.1
Bumps Stripe.net from 52.3.0 to 52.4.2

---
updated-dependencies:
- dependency-name: Google.Apis.AndroidPublisher.v3
  dependency-version: 1.76.0.4277
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.AspNetCore
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Hangfire.Core
  dependency-version: 1.8.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.AspNetCore.OpenApi
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Design
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Relational
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.ApiDescription.Server
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Abstractions
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.StackExchangeRedis
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Http
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
  dependency-version: 8.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: OpenAI
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: PostHog
  dependency-version: 2.15.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: PostHog.AspNetCore
  dependency-version: 2.9.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Scalar.AspNetCore
  dependency-version: 2.17.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Sentry.AspNetCore
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Stripe.net
  dependency-version: 52.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.Extensions.Caching.Memory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: FluentAssertions
  dependency-version: 8.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite
  dependency-version: 10.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Give API key management one authorization concept with two doors (#528)

* Start #529 API key step up

* Require API key management step up

* chore: regenerate the architecture map

The step up change moved the API key endpoints' shape, so `architecture.json`
and `architecture.html` no longer matched the tree and the `drift` check
failed on `git diff --exit-code`. Regenerated with `node tools/arch-map.mjs`,
which reports 45 entities and 0 untested feature folders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Give API key management one authorization concept with two doors

Listing, creating and revoking API keys now read one grant. Two doors open
it, and both end in a six-digit code emailed to the account owner: the HTTP
challenge, and a verified agent step-up.

Creating key material spends the grant. Listing and revoking only read it,
so one emailed code authorizes a management session instead of exactly one
revoke. A revoke no longer locks the person out of the key list.

get_api_keys gets a door. RequireApiKeyCreationStepUp raises the read
capability to a step-up inside the executor, so the read opens a pending
operation the caller can step up against, and the MCP read routes through
McpExecutorBridge like the mutation does.

AppConfigService now parses a stored row strictly and throws by name when a
row is malformed, so a typo cannot leave the gate off while the read-back
reports it on.

Both controller actions declare 403 and 428, and openapi.json carries them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Cover API key step up through merged MCP gate

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Expose habit emoji in MCP tools (#513) (#546)

* Open ticket 513 implementation review

* Expose habit emoji across MCP tools (#513)

* Prevent stale pull request workflow runs for #659 (#555)

* Start #659 workflow concurrency change

* Group core PR workflows by pull request for #659

* Group remaining PR workflows by pull request for #659

* Fix #657 logout revocation across a session family (#553)

* Start #657 session family fix

* Revoke the full auth session after logout (#657)

* Explain temporary legacy token allowance (#657)

* Handle duplicate refresh history race (#657)

* Fix #588 bulk habit interval weeks (#539)

* Start #588 bulk interval weeks fix

* Fix #588 bulk habit interval weeks mapping

* Guarantee crisis resources in Astra chat for #319 (#547)

* Add crisis guidance to Astra static prompt for #319

* Add curated crisis detection and regression cases for #319

* Wire crisis guard across chat delivery and metrics for #319

* Refine crisis delivery and cover fallback regression for #319

* Return static crisis support when AI chat fails for #319

* Keep crisis FAQ regression isolated for #319

* Fix crisis replies to bypass AI quota and provider

* Record streak freeze source for #571 (#540)

* Start #571 freeze source work

* Record streak freeze origin for #571

* Require explicit origin for new streak freezes

* Keep support tool available for support entry point (#541)

* Expose scheduled streak repair gap dates for #505 (#542)

* Fix standalone sub habit title validation for #628 (#543)

* Start #628 sub habit title validation fix

* Use sub habit title messages in standalone validation (#628)

* Carry the onboarding repeat interval into the created habit (#596) (#533)

A signed-out person who set a weekly repeat interval during onboarding lost
it at sign-up. ApplyHabitInput carried no interval field at all, so the apply
path dropped the value between the screen and the record.

Add an optional IntervalWeeks to ApplyHabitInput, additive only, and pass it
to Habit.Create alongside the other schedule options. No existing field
changes name, shape or nullability, so a client that sends nothing keeps the
behaviour it has today: IntervalWeeks stays null and the habit repeats every
week.

Validate the new field with the shared SharedHabitRules.AddIntervalWeeksRules
range, the same 1 to 52 bound every other create path uses.

Regenerate openapi.json and architecture.json/html for the new field and for
the test class that now touches HabitScheduleService.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Expose recurrence time zone in calendar events for #591 (#544)

* Start #591 recurrence time zone work

* Expose recurring calendar event time zone for #591

* Restore recurrence time zone in legacy suggestions for #591

* Project safe calendar BYDAY recurrences (#569) (#545)

* Start calendar BYDAY projection (#569)

* Project uniform calendar BYDAY recurrences (#569)

* Cover alternate week recurrence projection (#569)

* Keep shifted calendar rules importable by installed clients

* Add closed week and year recap ranges for #369 (#548)

* Fix Google sign-in after concurrent user updates (#551)

* chore: start ticket 324 review

* fix: retry concurrent Google sign-in updates (#324)

* Fix duplicate agent confirmation consumption (#557)

* Start fix for ticket 671 confirmation race

* Claim agent confirmation once under concurrent saves

* chore: regenerate the architecture map for the confirmation claim migration (#671)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: show newest habit completion date in profile (#549)

* chore: start ticket 391

* fix: expose latest habit completion date in profile

* fix: exclude bad habit slips from last completion date

* fix: pass the freeze origin in the profile freeze test after #540 (#391)

The merge with main brought StreakFreeze.Create's required origin
parameter (#540). The test now asserts that a freeze without a
completion leaves lastCompletionDate null for both origins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: preserve last completion across habit cleanup (#391)

* Preserve descendant completions during sync cleanup

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Recover concurrent habit log duplicates (#325) (#552)

* Start #325 habit log duplicate recovery

* Recover duplicate habit logs without losing idempotency response

* chore(deps): bump the github-actions group across 1 directory with 5 updates (#530)

* chore(deps): bump the github-actions group across 1 directory with 5 updates

Bumps the github-actions group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `5` | `7` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.8` | `4.38.1` |
| [actions/setup-java](https://github.com/actions/setup-java) | `5` | `6` |
| [oasdiff/oasdiff-action/breaking](https://github.com/oasdiff/oasdiff-action) | `0.1.13` | `0.1.17` |



Updates `actions/upload-artifact` from 5 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v5...v7)

Updates `github/codeql-action/init` from 4.37.8 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...1c5b675)

Updates `github/codeql-action/analyze` from 4.37.8 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...1c5b675)

Updates `actions/setup-java` from 5 to 6
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](actions/setup-java@v5...v6)

Updates `oasdiff/oasdiff-action/breaking` from 0.1.13 to 0.1.17
- [Release notes](https://github.com/oasdiff/oasdiff-action/releases)
- [Commits](oasdiff/oasdiff-action@2649ebe...5e81b5c)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: oasdiff/oasdiff-action/breaking
  dependency-version: 0.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: restore optional response removal gate (#664)

* fix: keep the oasdiff levels file out of the repository root and allowlist .oasdiff.yaml (#664)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Thomas Luizon Rodrigues Gregorio <thomaslrgregorio@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ORB-101: verify Turnstile tokens on anonymous writes (#550)

* chore: open ORB-101 review branch

* feat: gate anonymous writes with optional Turnstile verification

* test: cover Turnstile gate and verifier failures

* test: assert valid Turnstile request returns success

* test: ground Turnstile responses in observed Siteverify results

Document web, Android, and landing activation checks for ORB-101.

* Fix Astra goal access for every plan (#677) (#560)

* fix: accept stored recap week anchors after preference changes (#683) (#559)

* fix: resolve stored recap week anchors without the current preference

A closed week anchor lives in the recap share link and in the stored
snapshot key. Validating it against today's WeekStartDay rejected a
Monday anchor after the person moved to Sunday, before the stored
snapshot could be read. Accept any supported week start (Sunday or
Monday) for an explicit closed anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: use closed week anchor for recap cache misses

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Prune idle Hangfire session connections for #564 (#554)

* Bump Scalar.AspNetCore from 2.17.9 to 2.17.10 (#558)

---
updated-dependencies:
- dependency-name: Scalar.AspNetCore
  dependency-version: 2.17.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Make unsubscribe token tampering test byte-accurate (#673) (#561)

* Make goal completion timestamp test deterministic (#676) (#562)

* Start #676 goal clock fix

* Make goal completion timestamp test deterministic (#676)

* chore: regenerate the architecture map for the goal clock test

The test now references HabitLog, so node tools/arch-map.mjs adds it to
the test's references. Two runs produce the same output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Pin Pullfrog reviewer to GPT 6 Sol medium (#636) (#537)

* Pin Pullfrog reviewer model and effort for #636

* Set Pullfrog effort to medium on Thomas's decision (#636)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Describe the pinned Pullfrog reviewer in the workflow comment (#636)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: run the pinned Pullfrog commit so GPT-6 Sol medium applies

The published pullfrog 0.1.82 has no gpt-6-sol entry, so the effort
input was dropped. The review step now runs the action's own code at
pullfrog/pullfrog main 405f60c2, whose model table has gpt-6-sol with a
medium rung. Return to @v0 once 0.1.83 is published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Take Pullfrog model and effort from the repository config (#565)

The app dispatch payload carries model and effort and outranks action
inputs (utils/payload.ts:320-326 at 405f60c2), so the inputs from #537
never applied (run 36187465385 resolved effort xhigh). Effort is now
0.25 (medium on GPT-6 Sol) in the Pullfrog repo config.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix relative reminders across local day boundaries (#566)

* chore: start reminder boundary fix for #695

* test: inject a fixed clock into relative reminder checks

* fix: send relative reminders on their local occurrence day

* chore: ratchet the scheduler suppression count and regenerate the map

#695 removed the two relative-reminder UTC-date suppressions, so the
closed allowlist now declares 2 ORBIT0004 sites in
ReminderSchedulerService.cs. The architecture map is regenerated for
the new clock dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: fire relative reminders at the first local due time across DST

Pullfrog review of 9ebf379: on a fall-back day a 01:30 due time is
ambiguous and ConvertTimeToUtc picked the later occurrence, an hour
after the old scheduler. A repeated hour now resolves to its first
occurrence, and a due time inside a spring-forward gap resolves to the
moment the clock jumps past it instead of being skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix bulk habit replay idempotency (#567)

* chore: open ticket 389 review early

* fix: make bulk habit writes replay safe

* fix: scope idempotency ledger by command content

* fix: scope idempotency ledger by request ordinal

* Use published Pullfrog v0 for Codex review (#568)

* Start #700 Pullfrog action update

* Use published Pullfrog v0 for Codex review

* ci: pin both Pullfrog steps to the v0 release commit

SonarCloud githubactions:S7637 flags a moving tag as an unpinned dependency.
Pin the commit the v0 tag named on 2026-09-26, release 0.1.83.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: align worker PR handoff with ticket orders (#570)

* Fix child habit title messages on update for #651 (#572)

* Fix explicit culture formatting for #704 (#569)

* chore: begin #704 culture formatting work

* fix: format #704 notification prompts logs and goal tools explicitly

* fix: enforce invariant formatting across #704 prompts and machine text

* test: cover invariant decimal formatting for #704

* chore: refresh architecture artifacts for #704

* test: cover remaining culture formatting paths for #704

* Fix last completion date after habit type changes (#574)

* fix: preserve habit log slip kind at write time

* test: include slip kind in concurrent log fixture

* fix: classify old habit logs during rolling deploys

* Clean stale text in API repository (#575)

* Use neutral names in test fixtures

* Make repository text timeless

* Remove disallowed narration comment

* Add timeless text gate to API repository (#576)

* Add timeless text gate to API repository

* fix: read comments in XML build files

* fix: match script end tags with attributes

* fix: end a script only at a real end-tag delimiter

* Sync timeless checker with mobile main (#578)

* Sync timeless checker with mobile main

* fix: let a reviewed directory entry exempt any rule but machine-path

* fix: align timeless checker with reviewed version (#580)

Add regression coverage for hidden comments and introduced findings.

Refs thomasluizon/orbit-tickets#731

* Fix bulk habit replay plans across chat retries (#581)

* Fix bulk habit replay when selected chunks change

* Preserve bulk habit replay plans across retries

* Replay legacy bulk chunks and share replay planning

* Scope bulk replay plans by invocation and refuse legacy chat retries

* Fix bulk chat replay plan identity by invocation order

* Pin FluentValidation default messages to English (#582)

* fix: pin FluentValidation default messages to English

* chore: regenerate the architecture map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Return a code for habit ceiling failures (#583)

* fix: return a code when the habit ceiling is reached

* chore: regenerate the architecture map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix Google signup race during session save (#584)

* Fix Google signup race tracker cleanup

* Pin validator message test culture

* chore: generate architecture map outside git (#585)

* Cascade bulk habit deletion through subtrees (#587)

* fix: cascade bulk habit deletion through subtrees

* fix: keep bulk delete results aligned with requested habits

* Fix habit checklist reset and general-habit end dates (#586)

* Fix habit checklist, date, and reminder invariants

* Verify stored reminder JSON fields in migration test

* Keep habit checklist and end date fixes

* fix: keep database pool connections warm (#590)

* Project scheduler reads to required columns (#589)

* Project scheduler reads to notification fields

* Deduplicate reminder scheduler projections

* Project streak and achievement reads to reduce database egress (#591)

* Project distinct streak completion dates in SQL

* Project achievement streak and time log fields

* Project gamification log inputs for streaks and awards

* Bound goal and skip log reads to relevant windows

* Project habit schedule fields for streak computations

* Update streak repair fixtures for projected reads

* fix: preserve backdated standard goal completions

* Bound Today schedule log reads (#592)

* fix: bound today schedule log reads

* test: cover today schedule log read branches

* Fix repeated bulk habit calls in keyed requests (#595)

* fix: preserve distinct bulk habit call positions (#751)

* test: update habit log index expectations (#751)

* fix: enforce flexible habit completion target in domain (#751)

* test: keep interval month fixture within flexible target (#751)

* Preserve relative reminders at local times for due habits (#594)

* Fix relative reminders across due times and daylight saving

* Align habit tool tests with clock reminder storage

* Guard relative reminders without due times and legacy sends

* Keep folded clock reminders visible to legacy clients

* Fix relative reminder defaults and delivery deduplication

* Fix repeat sends for after-due reminders across UTC boundary

* Cache reminder scheduler reads between changes (#596)

* fix: cache reminder scheduler reads between changes

Refresh candidates when habit or user data changes, and bound cached history to one hour. Preserve database uniqueness when scheduler instances overlap.

Refs thomasluizon/orbit-tickets#760

* chore: drop the reminder scheduler allowlist entry its suppression no longer needs

Refs thomasluizon/orbit-tickets#760

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Reduce completion date query rows for #762 (#598)

* test: pin completion date behavior for ticket 762

* fix: project distinct completion dates for freeze and history

* fix: narrow achievement streak evidence read

* test: measure projected rows for large account

* Reduce daily summary log egress for ticket 763 (#599)

* test: pin daily summary log behavior for ticket 763

* fix: narrow daily summary log reads to needed dates and values

* test: compare narrowed summary prompt with legacy read

* test: assert summary log SQL omits unused columns

* Add account event stream for live account updates (#601)

* Add account event stream with commit-aware delivery

* Map event routes and stabilize achievement fixture

* Verify reset resync after transaction commit

* Verify event ticket claims from a validated access token

* fix: publish notification account events

* Share habit schedule reads across logging checks (#597)

* Share rule eligible habit schedule across log checks

* Assert excluded habit logs still count toward achievements

* Prove shared schedule SQL on large account seed

* Fix habit schedule snapshot invalidation

* fix: invalidate habit schedule on transaction retry

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Add child creation timestamps to habit responses (#604)

* ORB-194 part 1: remove the ad reward from Astra and deprecate its route (#600)

* Remove ad reward claim paths in ORB-194 part 1

* Restore deprecated ad reward endpoint

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep user id out of shared schedule snapshots (#605)

* Aggregate reminder scheduler probe (#606)

* fix: aggregate reminder scheduler probe

* fix: support in memory scheduler tests

* fix: preserve reminder probe invalidation across owner swaps

* fix: invalidate reminder snapshot on account reset

* chore: drop obsolete reminder fold trigger (#608)

* fix: keep user id out of shared schedule snapshots (#605)

(cherry picked from commit b81d4bf)

* Aggregate reminder scheduler probe (#606)

* fix: aggregate reminder scheduler probe

* fix: support in memory scheduler tests

* fix: preserve reminder probe invalidation across owner swaps

* fix: invalidate reminder snapshot on account reset

(cherry picked from commit 2103aec)

* chore: drop obsolete reminder fold trigger (#608)

(cherry picked from commit 0a66234)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant