Sync main fixes into redesign API for #746 - #593
Conversation
* Start #372 * Add habit widget empty reason
* 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>
) Mirrors thomasluizon/orbit-ui-mobile#1022. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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>
* 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>
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>
* 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>
* 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
) 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>
* chore: start ticket 324 review * fix: retry concurrent Google sign-in updates (#324)
* 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>
* 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>
…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>
* 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.
…#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>
--- 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>
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>
* 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>
* 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
* 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>
* 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: preserve habit log slip kind at write time * test: include slip kind in concurrent log fixture * fix: classify old habit logs during rolling deploys
* Use neutral names in test fixtures * Make repository text timeless * Remove disallowed narration comment
* 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 * fix: let a reviewed directory entry exempt any rule but machine-path
Add regression coverage for hidden comments and introduced findings. Refs thomasluizon/orbit-tickets#731
* 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
* 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>
* 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 tracker cleanup * Pin validator message test culture
* fix: cascade bulk habit deletion through subtrees * fix: keep bulk delete results aligned with requested habits
* Fix habit checklist, date, and reminder invariants * Verify stored reminder JSON fields in migration test * Keep habit checklist and end date fixes
There was a problem hiding this comment.
Important
Two behavior risks need attention before merging: repeated bulk chat actions can be deduplicated into one, and the one-time data repair can miss writes from an older API instance during rollout.
Reviewed changes Main's API fixes were synced into the redesign branch while retaining redesign chat and preview behavior.
- Bulk replay and hierarchy mutations: Added persisted filter selections and fingerprinted log/skip commands, plus subtree-aware bulk deletion with optional descendant IDs.
- Habit and time semantics: Tightened general-habit end-date rules with a data repair, reset recurring checklists, corrected historical completion reads, and anchored closed-week recap schedules.
- Infrastructure and verification: Cleared tracking on a Google signup race, adjusted database pools and validation language, moved architecture maps to artifacts, and expanded unit coverage. The mobile bulk-delete schema tolerates the added optional response property.
⚠️ One-time habit repair can miss writes during rollout
HealHabitModelData clears general-habit end dates only when a new instance starts. During a rolling deployment, an older instance still accepts general habits with end dates, so a write after the migration has run leaves a permanent legacy row that the new validation and migration never repair; date-limited general habits can remain hidden from schedule views.
Technical details
# Ensure general-habit end dates cannot survive rollout
## Affected sites
- src/Orbit.Infrastructure/Migrations/20260926142546_HealHabitModelData.cs:13-16 — one-time backfill can precede final writes by an older instance.
- src/Orbit.Application/Habits/Validators/SharedHabitRules.cs:76-79 — new instances reject this write while older instances still allow it.
- src/Orbit.Application/Habits/Services/HabitScheduleService.cs:573 — existing general habits with an end date can be filtered out when it passes.
## Required outcome
- Either ensure all old writers have drained before the cleanup or arrange for the cleanup to cover writes that arrive after migration startup, with an explicit rollout plan.GPT Sol | 𝕏
|
Review dispositions for head a6a6521. (1) Identical bulk calls sharing one idempotency slot: filed as thomasluizon/orbit-tickets#751. The code is identical on |
|
@pullfrog review |
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes Re-reviewed the complete PR diff at the same head as the prior Pullfrog review; no commits landed in between.
- Main sync: Carried replay, habit, calendar, authentication, and pool fixes into the redesign API while retaining redesign chat and preview behavior.
- Contract compatibility: Confirmed the added optional bulk-delete response field does not invalidate the mobile consumer's existing schema.
GPT Sol | 𝕏

Syncs main through ba0c3e6 into redesign/main for thomasluizon/orbit-tickets#746. The branch merge commit contains all 53 main commits absent from redesign/main. The resulting tree keeps redesign previews, cards, copy, and intent routing while adding main's replay, habit, calendar, auth, and pool fixes.
The changes land in the existing API, Application, Domain, Infrastructure, migration, and test files. Bulk replay uses the main planner for ordinary selections and the redesign's fixed revised items for confirmed edits. This keeps the persisted replay plan format stable.
Conflict resolutions
Each path appears under the latest redesign commit that touched it.
c3c7a92, prior redesign sync
3e39abc, editable pending operation previews
3888201, accent gate removal
a2f5469, redesign cleanup
1bb4e85, Astra stream steps and bulk previews
a644692, record list pages
23396af, copy redesign
Migration order
Main's HealHabitModelData migration precedes redesign's AddPendingOperationRevision migration by identifier. The model snapshot includes both. The EF pending model check reports no changes. No migration ordering question remains.
Verification
External interface evidence
Assumptions