<permissions instructions>
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted.
# Escalation Requests

Commands are run outside the sandbox if they are approved by the user, or match an existing rule that allows it to run unrestricted. The command string is split into independent command segments at shell control operators, including but not limited to:

- Pipes: |
- Logical operators: &&, ||
- Command separators: ;
- Subshell boundaries: (...), $(...)

Each resulting segment is evaluated independently for sandbox restrictions and approval requirements.

Example:

git pull | tee output.txt

This is treated as two command segments:

["git", "pull"]

["tee", "output.txt"]

Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) will not be evaluated against rules, to limit the scope of what an approved rule allows.

## How to request escalation

IMPORTANT: To request approval to execute a command that will require escalated privileges:

- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a short question asking the user if they want to allow the action in `justification` parameter. e.g. "Do you want to download and install dependencies for this project?"
- Optionally suggest a `prefix_rule` - this will be shown to the user with an option to persist the rule approval for future sessions.

If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with "require_escalated". ALWAYS proceed to use the `justification` parameter - do not message the user before requesting approval for the command.

## When to request escalation

While commands are running inside the sandbox, here are some scenarios that will require escalation outside the sandbox:

- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
- If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with `require_escalated`. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters. do not message the user before requesting approval for the command.
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.
- Be judicious with escalating, but if completing the user's request requires it, you should do so - don't try and circumvent approvals by using other tools.

## prefix_rule guidance

When choosing a `prefix_rule`, request one that will allow you to fulfill similar requests from the user in the future without re-requesting escalation. It should be categorical and reasonably scoped to similar capabilities. You should rarely pass the entire command into `prefix_rule`.

### Banned prefix_rules
Avoid requesting overly broad prefixes that the user would be ill-advised to approve. For example, do not request ["python3"], ["python", "-"], or other similar prefixes that would allow arbitrary scripting.
NEVER provide a prefix_rule argument for destructive commands like rm.
NEVER provide a prefix_rule if your command uses a heredoc or herestring.

### Examples
Good examples of prefixes:
- ["npm", "run", "dev"]
- ["gh", "pr", "check"]
- ["cargo", "test"]

## Approved command prefixes
The following prefix rules have already been approved: - ["curl"]
- ["./gradlew"]
- ["git", "add"]
- ["git", "push"]
- ["ss", "-ltnp"]
- ["git", "fetch"]
- ["git", "commit"]
- ["git", "rebase"]
- ["git", "switch"]
- ["hostname", "-I"]
- ["bundle", "install"]
- ["rm", "/home/Messier82/projects/site/assets/screenshots/vehicle-history-history-dark.png"]
- ["gh", "pr", "diff"]
- ["gh", "pr", "list"]
- ["gh", "pr", "view"]
- ["gh", "pr", "merge"]
- ["gh", "pr", "create"]
- ["gh", "issue", "list"]
- ["gh", "issue", "view"]
- ["gh", "pr", "comment"]
- ["gh", "api", "graphql"]
- ["gh", "pr", "checkout"]
- ["gh", "issue", "comment"]
- ["sudo", "dnf", "install"]
- ["bundle", "exec", "jekyll"]
- ["git", "push", "--force-with-lease"]
- ["gh", "api", "repos/yorvana/site/pulls/1/comments"]
- ["gh", "api", "repos/yorvana/android/pulls/102/comments"]
- ["gh", "api", "repos/yorvana/android/pulls/104/comments"]
- ["pkill", "-f", "jekyll serve --host 0.0.0.0 --port 4000 --no-watch"]
- ["pkill", "-f", "jekyll serve --host 127.0.0.1 --port 4000 --no-watch"]
- ["gh", "pr", "view", "97", "--json", "number,title,headRefName,baseRefName,files,mergeStateStatus,isDraft,statusCheckRollup"]
 The writable roots are `/home/Messier82/.codex/memories`, `/home/Messier82/projects/service-book`, `/tmp`.
</permissions instructions>

<collaboration_mode># Plan Mode (Conversational)

You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed—intent- and implementation-wise—so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.

## Mode rules (strict)

You are in **Plan Mode** until a developer message explicitly ends it.

Plan Mode is not changed by user intent, tone, or imperative language. If a user asks for execution while still in Plan Mode, treat it as a request to **plan the execution**, not perform it.

## Plan Mode vs update_plan tool

Plan Mode is a collaboration mode that can involve requesting user input and eventually issuing a `<proposed_plan>` block.

Separately, `update_plan` is a checklist/progress/TODOs tool; it does not enter or exit Plan Mode. Do not confuse it with Plan mode or try to use it while in Plan mode. If you try to use `update_plan` in Plan mode, it will return an error.

## Execution vs. mutation in Plan Mode

You may explore and execute **non-mutating** actions that improve the plan. You must not perform **mutating** actions.

### Allowed (non-mutating, plan-improving)

Actions that gather truth, reduce ambiguity, or validate feasibility without changing repo-tracked state. Examples:

* Reading or searching files, configs, schemas, types, manifests, and docs
* Static analysis, inspection, and repo exploration
* Dry-run style commands when they do not edit repo-tracked files
* Tests, builds, or checks that may write to caches or build artifacts (for example, `target/`, `.cache/`, or snapshots) so long as they do not edit repo-tracked files

### Not allowed (mutating, plan-executing)

Actions that implement the plan or change repo-tracked state. Examples:

* Editing or writing files
* Running formatters or linters that rewrite files
* Applying patches, migrations, or codegen that updates repo-tracked files
* Side-effectful commands whose purpose is to carry out the plan rather than refine it

When in doubt: if the action would reasonably be described as "doing the work" rather than "planning the work," do not do it.

## PHASE 1 — Ground in the environment (explore first, ask second)

Begin by grounding yourself in the actual environment. Eliminate unknowns in the prompt by discovering facts, not by asking the user. Resolve all questions that can be answered through exploration or inspection. Identify missing or ambiguous details only if they cannot be derived from the environment. Silent exploration between turns is allowed and encouraged.

Before asking the user any question, perform at least one targeted non-mutating exploration pass (for example: search relevant files, inspect likely entrypoints/configs, confirm current implementation shape), unless no local environment/repo is available.

Exception: you may ask clarifying questions about the user's prompt before exploring, ONLY if there are obvious ambiguities or contradictions in the prompt itself. However, if ambiguity might be resolved by exploring, always prefer exploring first.

Do not ask questions that can be answered from the repo or system (for example, "where is this struct?" or "which UI component should we use?" when exploration can make it clear). Only ask once you have exhausted reasonable non-mutating exploration.

## PHASE 2 — Intent chat (what they actually want)

* Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs.
* Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet—ask.

## PHASE 3 — Implementation chat (what/how we’ll build)

* Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints.

## Asking questions

Critical rules:

* Strongly prefer using the `request_user_input` tool to ask any questions.
* Offer only meaningful multiple‑choice options; don’t include filler choices that are obviously wrong or irrelevant.
* In rare cases where an unavoidable, important question can’t be expressed with reasonable multiple‑choice options (due to extreme ambiguity), you may ask it directly without the tool.

You SHOULD ask many questions, but each question must:

* materially change the spec/plan, OR
* confirm/lock an assumption, OR
* choose between meaningful tradeoffs.
* not be answerable by non-mutating commands.

Use the `request_user_input` tool only for decisions that materially change the plan, for confirming important assumptions, or for information that cannot be discovered via non-mutating exploration.

## Two kinds of unknowns (treat differently)

1. **Discoverable facts** (repo/system truth): explore first.

   * Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants).
   * Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent.
   * If asking, present concrete candidates (paths/service names) + recommend one.
   * Never ask questions you can answer from your environment (e.g., “where is this struct”).

2. **Preferences/tradeoffs** (not discoverable): ask early.

   * These are intent or implementation preferences that cannot be derived from exploration.
   * Provide 2–4 mutually exclusive options + a recommended default.
   * If unanswered, proceed with the recommended option and record it as an assumption in the final plan.

## Finalization rule

Only output the final plan when it is decision complete and leaves no decisions to the implementer.

When you present the official plan, wrap it in a `<proposed_plan>` block so the client can render it specially:

1) The opening tag must be on its own line.
2) Start the plan content on the next line (no text on the same line as the tag).
3) The closing tag must be on its own line.
4) Use Markdown inside the block.
5) Keep the tags exactly as `<proposed_plan>` and `</proposed_plan>` (do not translate or rename them), even if the plan content is in another language.

Example:

<proposed_plan>
plan content
</proposed_plan>

plan content should be human and agent digestible. The final plan must be plan-only, concise by default, and include:

* A clear title
* A brief summary section
* Important changes or additions to public APIs/interfaces/types
* Test cases and scenarios
* Explicit assumptions and defaults chosen where needed

When possible, prefer a compact structure with 3-5 short sections, usually: Summary, Key Changes or Implementation Changes, Test Plan, and Assumptions. Do not include a separate Scope section unless scope boundaries are genuinely important to avoid mistakes.

Prefer grouped implementation bullets by subsystem or behavior over file-by-file inventories. Mention files only when needed to disambiguate a non-obvious change, and avoid naming more than 3 paths unless extra specificity is necessary to prevent mistakes. Prefer behavior-level descriptions over symbol-by-symbol removal lists. For v1 feature-addition plans, do not invent detailed schema, validation, precedence, fallback, or wire-shape policy unless the request establishes it or it is needed to prevent a concrete implementation mistake; prefer the intended capability and minimum interface/behavior changes.

Keep bullets short and avoid explanatory sub-bullets unless they are needed to prevent ambiguity. Prefer the minimum detail needed for implementation safety, not exhaustive coverage. Within each section, compress related changes into a few high-signal bullets and omit branch-by-branch logic, repeated invariants, and long lists of unaffected behavior unless they are necessary to prevent a likely implementation mistake. Avoid repeated repo facts and irrelevant edge-case or rollout detail. For straightforward refactors, keep the plan to a compact summary, key edits, tests, and assumptions. If the user asks for more detail, then expand.

Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `<proposed_plan>` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.

Only produce at most one `<proposed_plan>` block per turn, and only when you are presenting a complete spec.

If the user stays in Plan mode and asks for revisions after a prior `<proposed_plan>`, any new `<proposed_plan>` must be a complete replacement.
</collaboration_mode>

<apps_instructions>
## Apps (Connectors)
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connector_id})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps.
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it.
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
</apps_instructions>

<skills_instructions>
## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
### Available skills
- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas. (file: /home/Messier82/.codex/skills/.system/imagegen/SKILL.md)
- openai-docs: Use when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; prioritize OpenAI docs MCP tools, use bundled references only as helper context, and restrict any fallback browsing to official OpenAI domains. (file: /home/Messier82/.codex/skills/.system/openai-docs/SKILL.md)
- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, or generate or update marketplace entries for plugin ordering and availability metadata. (file: /home/Messier82/.codex/skills/.system/plugin-creator/SKILL.md)
- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /home/Messier82/.codex/skills/.system/skill-creator/SKILL.md)
- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /home/Messier82/.codex/skills/.system/skill-installer/SKILL.md)
- gmail:gmail: Manage Gmail inbox triage, mailbox search, thread summaries, action extraction, reply drafting, and email forwarding through connected Gmail data. Use when the user wants to inspect a mailbox or thread, search email with Gmail query syntax, summarize messages, extract decisions and follow-ups, prepare replies or forwarded messages, or organize messages with explicit confirmation before send, archive, delete, or label actions. (file: /home/Messier82/.codex/plugins/cache/openai-curated/gmail/0d4f5414/skills/gmail/SKILL.md)
- gmail:gmail-inbox-triage: Triage a Gmail inbox into actionable buckets such as urgent, needs reply soon, waiting, and FYI using connected Gmail data. Use when the user asks to triage the inbox, rank what needs attention, find what still needs a reply, or separate important mail from noise. (file: /home/Messier82/.codex/plugins/cache/openai-curated/gmail/0d4f5414/skills/gmail-inbox-triage/SKILL.md)
- google-calendar:google-calendar: Manage scheduling and conflicts in connected Google Calendar data. Use when the user wants to inspect calendars, compare availability, review conflicts, find a meeting room, review event notes or attachments, add or adjust reminders, place temporary holds, or draft exact create, update, reschedule, or cancel changes with timezone-aware details. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar/SKILL.md)
- google-calendar:google-calendar-daily-brief: Build polished one-day Google Calendar briefs. Use when the user asks for today, tomorrow, or a specific date summary with an agenda, conflict flags, free windows, remaining-meeting readouts, or a calendar brief, and the Google Calendar connector is available. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-daily-brief/SKILL.md)
- google-calendar:google-calendar-free-up-time: Find ways to open up meaningful free time in a connected Google Calendar. Use when the user wants to clear up their day, make room for focus time, create a longer uninterrupted block, or see the smallest set of calendar changes that would give time back. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-free-up-time/SKILL.md)
- google-calendar:google-calendar-group-scheduler: Find and rank good meeting times for multiple people using connected Google Calendar data. Use when the user wants to schedule a group meeting, compare candidate slots across several attendees, find the best compromise time, or add a room check after narrowing the attendee-compatible options. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-group-scheduler/SKILL.md)
- google-calendar:google-calendar-meeting-prep: Build a practical meeting prep brief from a connected Google Calendar event and its nearby context. Use when the user wants to prepare for an upcoming meeting, understand what to read beforehand, pull in linked notes or docs, or get a concise brief on what the meeting appears to require. (file: /home/Messier82/.codex/plugins/cache/openai-curated/google-calendar/0d4f5414/skills/google-calendar-meeting-prep/SKILL.md)
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
  1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
  2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
  3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
  4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
  - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
  - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
  - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
  - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
  - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
</skills_instructions>

<plugins_instructions>
## Plugins
A plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.
### Available plugins
- `Gmail`: Work with Gmail using the configured Gmail app connector.
- `Google Calendar`: Connect Google Calendar for scheduling, availability, daily briefs, and event management.
### How to use plugins
- Discovery: The list above is the plugins available in this session.
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.
</plugins_instructions>


# AGENTS.md instructions for /home/Messier82/projects/service-book

<INSTRUCTIONS>
# Repository Guidelines

## Project Structure & Module Organization

Yorvana is a single-module Android app. Main Kotlin source lives in `app/src/main/java/com/yorvana`, organized by layer: `data` for repositories and storage, `domain` for shared models, `ui` for Compose screens/components, and `util` for app helpers. Android resources are in `app/src/main/res`. JVM, Robolectric, and screenshot tests live in `app/src/test/java`; instrumented device tests live in `app/src/androidTest/java`, with test assets in `app/src/androidTest/assets`. Project docs are in `docs/`, build configuration is in `build.gradle.kts`, `app/build.gradle.kts`, `gradle/libs.versions.toml`, and quality rules are in `config/detekt/`.

## Build, Test, and Development Commands

- `./gradlew assembleDebug`: build a debug APK.
- `./gradlew testDebugUnitTest`: run JVM unit, Robolectric, and local Compose tests.
- `./gradlew ktlintCheck detekt lintDebug`: run formatting, static analysis, and Android lint checks used by the pre-commit hook.
- `./gradlew verifyRoborazziDebug`: verify screenshot baselines.
- `./gradlew recordRoborazziDebug`: update screenshot baselines after intentional UI changes.
- `./gradlew pixel2api33DebugAndroidTest`: run instrumented tests on the managed Pixel 2 API 33 device.
- `./gradlew verifyWithCoverage -Pcoverage`: generate combined JVM and smoke-test coverage reports.

## Coding Style & Naming Conventions

Use Kotlin with Java 17 targets and Jetpack Compose Material 3. Follow ktlint defaults and the detekt rules in `config/detekt/detekt.yml`; do not work around baseline files without fixing the underlying issue. Keep package paths under `com.yorvana`. Name Compose screen files `FeatureScreen.kt`, ViewModels `FeatureViewModel.kt`, repository interfaces `ThingRepository`, and implementations `ThingRepositoryImpl`. Prefer immutable UI state exposed through `StateFlow`.

## Testing Guidelines

Use JUnit 4, Robolectric, MockK, Truth, Turbine, Compose UI tests, and Roborazzi. Name fast JVM tests `*Test.kt`, local integration tests `*IT.kt`, and screenshot tests `*ScreenshotTest.kt`. Add new Android-dependent integration tests to `app/src/test/java` when Robolectric is sufficient; use `app/src/androidTest/java` only for real OS, SAF, UiAutomator, hardware, or instrumentation-only behavior. Refresh `app/coverage-baselines/gmd_smoke.ec` with `./gradlew generateGmdCoverage` when smoke-covered production flows change.

## Commit & Pull Request Guidelines

Recent history uses short imperative subjects, with conventional prefixes where useful: `build: bump ...`, `docs: add ...`, or plain feature summaries such as `Add contact option to settings (#102)`. Keep commits focused and avoid committing local secrets or generated noise. PRs should describe user-visible behavior, list verification commands run, link related issues, and include screenshots or updated Roborazzi baselines for UI changes. For automated dependency updates, see [Dependabot Resolution Workflow](docs/dependabot-workflow.md).

## Security & Configuration Tips

Do not commit `local.properties` or real signing credentials. Use `app/keystore.properties.template` for release signing setup and local `sentry.dsn.*` values for Sentry configuration. If coverage baselines are Git LFS pointers, run `git lfs pull` before `verifyWithCoverage`.

</INSTRUCTIONS>

<environment_context>
  <cwd>/home/Messier82/projects/service-book</cwd>
  <shell>bash</shell>
  <current_date>2026-05-22</current_date>
  <timezone>Etc/UTC</timezone>
</environment_context>


# Plan Mode (Conversational)

You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed—intent- and implementation-wise—so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.

## Mode rules (strict)

You are in **Plan Mode** until a developer message explicitly ends it.

Plan Mode is not changed by user intent, tone, or imperative language. If a user asks for execution while still in Plan Mode, treat it as a request to **plan the execution**, not perform it.

## Plan Mode vs update_plan tool

Plan Mode is a collaboration mode that can involve requesting user input and eventually issuing a `<proposed_plan>` block.

Separately, `update_plan` is a checklist/progress/TODOs tool; it does not enter or exit Plan Mode. Do not confuse it with Plan mode or try to use it while in Plan mode. If you try to use `update_plan` in Plan mode, it will return an error.

## Execution vs. mutation in Plan Mode

You may explore and execute **non-mutating** actions that improve the plan. You must not perform **mutating** actions.

### Allowed (non-mutating, plan-improving)

Actions that gather truth, reduce ambiguity, or validate feasibility without changing repo-tracked state. Examples:

* Reading or searching files, configs, schemas, types, manifests, and docs
* Static analysis, inspection, and repo exploration
* Dry-run style commands when they do not edit repo-tracked files
* Tests, builds, or checks that may write to caches or build artifacts (for example, `target/`, `.cache/`, or snapshots) so long as they do not edit repo-tracked files

### Not allowed (mutating, plan-executing)

Actions that implement the plan or change repo-tracked state. Examples:

* Editing or writing files
* Running formatters or linters that rewrite files
* Applying patches, migrations, or codegen that updates repo-tracked files
* Side-effectful commands whose purpose is to carry out the plan rather than refine it

When in doubt: if the action would reasonably be described as "doing the work" rather than "planning the work," do not do it.

## PHASE 1 — Ground in the environment (explore first, ask second)

Begin by grounding yourself in the actual environment. Eliminate unknowns in the prompt by discovering facts, not by asking the user. Resolve all questions that can be answered through exploration or inspection. Identify missing or ambiguous details only if they cannot be derived from the environment. Silent exploration between turns is allowed and encouraged.

Before asking the user any question, perform at least one targeted non-mutating exploration pass (for example: search relevant files, inspect likely entrypoints/configs, confirm current implementation shape), unless no local environment/repo is available.

Exception: you may ask clarifying questions about the user's prompt before exploring, ONLY if there are obvious ambiguities or contradictions in the prompt itself. However, if ambiguity might be resolved by exploring, always prefer exploring first.

Do not ask questions that can be answered from the repo or system (for example, "where is this struct?" or "which UI component should we use?" when exploration can make it clear). Only ask once you have exhausted reasonable non-mutating exploration.

## PHASE 2 — Intent chat (what they actually want)

* Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs.
* Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet—ask.

## PHASE 3 — Implementation chat (what/how we’ll build)

* Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints.

## Asking questions

Critical rules:

* Strongly prefer using the `request_user_input` tool to ask any questions.
* Offer only meaningful multiple‑choice options; don’t include filler choices that are obviously wrong or irrelevant.
* In rare cases where an unavoidable, important question can’t be expressed with reasonable multiple‑choice options (due to extreme ambiguity), you may ask it directly without the tool.

You SHOULD ask many questions, but each question must:

* materially change the spec/plan, OR
* confirm/lock an assumption, OR
* choose between meaningful tradeoffs.
* not be answerable by non-mutating commands.

Use the `request_user_input` tool only for decisions that materially change the plan, for confirming important assumptions, or for information that cannot be discovered via non-mutating exploration.

## Two kinds of unknowns (treat differently)

1. **Discoverable facts** (repo/system truth): explore first.

   * Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants).
   * Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent.
   * If asking, present concrete candidates (paths/service names) + recommend one.
   * Never ask questions you can answer from your environment (e.g., “where is this struct”).

2. **Preferences/tradeoffs** (not discoverable): ask early.

   * These are intent or implementation preferences that cannot be derived from exploration.
   * Provide 2–4 mutually exclusive options + a recommended default.
   * If unanswered, proceed with the recommended option and record it as an assumption in the final plan.

## Finalization rule

Only output the final plan when it is decision complete and leaves no decisions to the implementer.

When you present the official plan, wrap it in a `<proposed_plan>` block so the client can render it specially:

1) The opening tag must be on its own line.
2) Start the plan content on the next line (no text on the same line as the tag).
3) The closing tag must be on its own line.
4) Use Markdown inside the block.
5) Keep the tags exactly as `<proposed_plan>` and `</proposed_plan>` (do not translate or rename them), even if the plan content is in another language.

Example:

<proposed_plan>
plan content
</proposed_plan>

plan content should be human and agent digestible. The final plan must be plan-only, concise by default, and include:

* A clear title
* A brief summary section
* Important changes or additions to public APIs/interfaces/types
* Test cases and scenarios
* Explicit assumptions and defaults chosen where needed

When possible, prefer a compact structure with 3-5 short sections, usually: Summary, Key Changes or Implementation Changes, Test Plan, and Assumptions. Do not include a separate Scope section unless scope boundaries are genuinely important to avoid mistakes.

Prefer grouped implementation bullets by subsystem or behavior over file-by-file inventories. Mention files only when needed to disambiguate a non-obvious change, and avoid naming more than 3 paths unless extra specificity is necessary to prevent mistakes. Prefer behavior-level descriptions over symbol-by-symbol removal lists. For v1 feature-addition plans, do not invent detailed schema, validation, precedence, fallback, or wire-shape policy unless the request establishes it or it is needed to prevent a concrete implementation mistake; prefer the intended capability and minimum interface/behavior changes.

Keep bullets short and avoid explanatory sub-bullets unless they are needed to prevent ambiguity. Prefer the minimum detail needed for implementation safety, not exhaustive coverage. Within each section, compress related changes into a few high-signal bullets and omit branch-by-branch logic, repeated invariants, and long lists of unaffected behavior unless they are necessary to prevent a likely implementation mistake. Avoid repeated repo facts and irrelevant edge-case or rollout detail. For straightforward refactors, keep the plan to a compact summary, key edits, tests, and assumptions. If the user asks for more detail, then expand.

Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `<proposed_plan>` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.

Only produce at most one `<proposed_plan>` block per turn, and only when you are presenting a complete spec.

If the user stays in Plan mode and asks for revisions after a prior `<proposed_plan>`, any new `<proposed_plan>` must be a complete replacement.

# Repository Guidelines

## Project Structure & Module Organization

Yorvana is a single-module Android app. Main Kotlin source lives in `app/src/main/java/com/yorvana`, organized by layer: `data` for repositories and storage, `domain` for shared models, `ui` for Compose screens/components, and `util` for app helpers. Android resources are in `app/src/main/res`. JVM, Robolectric, and screenshot tests live in `app/src/test/java`; instrumented device tests live in `app/src/androidTest/java`, with test assets in `app/src/androidTest/assets`. Project docs are in `docs/`, build configuration is in `build.gradle.kts`, `app/build.gradle.kts`, `gradle/libs.versions.toml`, and quality rules are in `config/detekt/`.

## Build, Test, and Development Commands

- `./gradlew assembleDebug`: build a debug APK.
- `./gradlew testDebugUnitTest`: run JVM unit, Robolectric, and local Compose tests.
- `./gradlew ktlintCheck detekt lintDebug`: run formatting, static analysis, and Android lint checks used by the pre-commit hook.
- `./gradlew verifyRoborazziDebug`: verify screenshot baselines.
- `./gradlew recordRoborazziDebug`: update screenshot baselines after intentional UI changes.
- `./gradlew pixel2api33DebugAndroidTest`: run instrumented tests on the managed Pixel 2 API 33 device.
- `./gradlew verifyWithCoverage -Pcoverage`: generate combined JVM and smoke-test coverage reports.

## Coding Style & Naming Conventions

Use Kotlin with Java 17 targets and Jetpack Compose Material 3. Follow ktlint defaults and the detekt rules in `config/detekt/detekt.yml`; do not work around baseline files without fixing the underlying issue. Keep package paths under `com.yorvana`. Name Compose screen files `FeatureScreen.kt`, ViewModels `FeatureViewModel.kt`, repository interfaces `ThingRepository`, and implementations `ThingRepositoryImpl`. Prefer immutable UI state exposed through `StateFlow`.

## Testing Guidelines

Use JUnit 4, Robolectric, MockK, Truth, Turbine, Compose UI tests, and Roborazzi. Name fast JVM tests `*Test.kt`, local integration tests `*IT.kt`, and screenshot tests `*ScreenshotTest.kt`. Add new Android-dependent integration tests to `app/src/test/java` when Robolectric is sufficient; use `app/src/androidTest/java` only for real OS, SAF, UiAutomator, hardware, or instrumentation-only behavior. Refresh `app/coverage-baselines/gmd_smoke.ec` with `./gradlew generateGmdCoverage` when smoke-covered production flows change.

## Commit & Pull Request Guidelines

Recent history uses short imperative subjects, with conventional prefixes where useful: `build: bump ...`, `docs: add ...`, or plain feature summaries such as `Add contact option to settings (#102)`. Keep commits focused and avoid committing local secrets or generated noise. PRs should describe user-visible behavior, list verification commands run, link related issues, and include screenshots or updated Roborazzi baselines for UI changes. For automated dependency updates, see [Dependabot Resolution Workflow](docs/dependabot-workflow.md).

## Security & Configuration Tips

Do not commit `local.properties` or real signing credentials. Use `app/keystore.properties.template` for release signing setup and local `sentry.dsn.*` values for Sentry configuration. If coverage baselines are Git LFS pointers, run `git lfs pull` before `verifyWithCoverage`.


Theres a review comment


I’ll inspect the PR review comments and the current branch state so I can identify exactly what needs to change before drafting the fix plan.


gh pr view 123 --json number,title,headRefName,baseRefName,reviewDecision,comments,reviews,statusCheckRollup

Allow reading the GitHub PR review details so I can plan the requested review-comment fix.


Output:
Chunk ID: f3096d
Wall time: 0.1258 seconds
Process exited with code 0
Original token count: 17
Output:
## issue-114-regression-infra...origin/issue-114-regression-infra


Output:
Chunk ID: d378c7
Wall time: 0.9216 seconds
Process exited with code 0
Original token count: 1365
Output:
{"baseRefName":"master","comments":[{"id":"IC_kwDOR2Hmq88AAAABDWGo2Q","author":{"login":"Mesya82"},"authorAssociation":"MEMBER","body":"## Code review\n\n### Overview\nPhase 1 of the regression scaffold: five JUnit marker annotations (`Smoke`, `Regression`, `RegressionFull`, `BillingSandbox`, `Quarantined`), four Gradle aggregates (`regressionCheck`, `regressionFullNightlyCoreCheck`, `regressionFullExtendedCheck`, `regressionFullCheck`) implemented as `GradleBuild` child invocations against `pixel2api33DebugAndroidTest`, a workflow pair (`regression.yml` + `regression-label.yml`) mirroring smoke, `androidTest` helpers (`Permissions`, `Camera`, `SystemPicker`, `FixtureAssetsRule`) + a minimal `test_receipt.pdf`, and a JVM `RegressionTierScannerTest` that source-scans `regression/` for tier-annotation invariants. `SmokeTest` gains `@Smoke`; the smoke-device runner-arg now applies `annotation = Smoke` unless an explicit annotation is passed.\n\n### Correctness\n\n**1. Nightly schedule runs the non-quarantined regression set twice.** `regression.yml` invokes `regressionCheck regressionFullCheck`, and `regressionFullCheck` already depends on `regressionFullNightlyCoreCheck` whose `annotation=Regression` filter is a superset of `regressionCheck`'s `Regression && !Quarantined`. Because each `GradleBuild` is a fresh nested invocation, this is real wall-clock + CI-minute waste — not a free fail-fast (the child build re-configures from scratch). Either drop `regressionCheck` from the schedule line, or keep it and comment the fail-fast intent.\n\n**2. `isExplicitAnnotationRun` is captured eagerly at outer-build configuration time** (`app/build.gradle.kts:222`). It's a `Boolean`, not a `Provider<Boolean>`, so `providers.gradleProperty(...).isPresent` resolves when outer `afterEvaluate` runs. Fine for child `GradleBuild` invocations (they configure fresh) and fine for direct `-P` overrides. Worth a one-line note so the next reader doesn't try to switch it to a provider chain.\n\n**3. `gradle.startParameter.projectProperties + runnerArguments.mapKeys{…}`** (build.gradle.kts:282) makes the hard-coded annotation/notAnnotation filters win over any outer `-P…annotation=Foo`. Almost certainly the intent — but silent override is a footgun for anyone trying to narrow a run from the CLI. A doc-comment on the task descriptions would suffice.\n\n### Conventions\n\n**4. `regressionFullNightlyCoreCheck` bakes a CI cadence into a build-graph identifier.** What it *is* is \"the core regression tier, including quarantined\". A consumer who runs it manually from `gradlew tasks` will be misled. Suggest `regressionFullCoreCheck`; let the workflow file own the \"nightly\" framing.\n\n**5. `RegressionTierScanner` parses Kotlin with line-oriented regex.** It handles the present fixtures, but several common shapes will trip it: multi-line class headers, primary-constructor parameter lists that span lines, generic type parameters before the supertype, nested classes (`pendingAnnotations` resets on inner-class declarations and leaks across siblings since there's no brace tracking). Phase 1 has zero files under `regression/`, so the production scan is a no-op today — but when Phase 2 lands real test classes the scanner will silently miss violations rather than fail loudly. Two cheap mitigations: (a) header comment naming the supported shapes; (b) one fixture that asserts an unsupported shape currently *passes*, so the limit is documented in tests rather than folklore.\n\n### Workflows\n- `regression.yml` carrying `workflow_dispatch` + `workflow_call` + `schedule` in one file is fine; the `schedule` only fires from `master`, consistent with GitHub's behavior.\n- `regression-label.yml` and `smoke-label.yml` are byte-for-byte structural twins. Acceptable.\n\n### Tests / fixtures\n- `RegressionTierScannerTest` covers the four main violation paths (zero-tier, two-tier, orphan-quarantine, inherited tier) and the forward-compat annotation-argument case. Good coverage of the parser's actual contract.\n- `FixtureAssetsRule.copyToCache` re-appends to `cacheFiles` if called twice with the same asset; cleanup is idempotent, so harmless.\n- `Permissions.acceptIfShown` worst-case is 4 × 2s; `Camera.captureAndAccept` is up to 5 × 10s. Worth knowing when a test hangs.\n\n### Docs\n**6. `plan-regression-tests.md` lists \"one small PDF and one small JPG\"**, but only `test_receipt.pdf` was added. If the JPG is intentionally Phase-2, change the doc to \"one small PDF (JPG follows in Phase 2)\"; otherwise add the JPG now.\n\n### Security\nNo surface-area concerns. Workflows are `contents: read`; the label workflow's `pull-requests: read` is the minimum needed for `pull_request` events. No new secrets, no network calls outside the existing GMD pipeline.","createdAt":"2026-05-22T14:12:51Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/yorvana/android/pull/123#issuecomment-4519471321","viewerDidAuthor":true}],"headRefName":"issue-114-regression-infra","number":123,"reviewDecision":"","reviews":[],"statusCheckRollup":[{"__typename":"CheckRun","completedAt":"2026-05-22T14:09:38Z","conclusion":"SUCCESS","detailsUrl":"https://github.com/yorvana/android/actions/runs/26292654963/job/77396749831","name":"Semgrep Scan","startedAt":"2026-05-22T14:08:55Z","status":"COMPLETED","workflowName":"Semgrep"}],"title":"Add regression test infrastructure"}


The PR has one review-style comment with six concrete items. I’m going to inspect the affected Gradle, workflow, docs, and scanner snippets so the plan can be specific rather than generic.


nl -ba app/build.gradle.kts | sed -n '200,330p'


nl -ba .github/workflows/regression.yml | sed -n '1,220p'


rg -n "small PDF|test_receipt|RegressionTierScanner|regressionFullNightlyCoreCheck|regressionFull" docs/plan-regression-tests.md app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt app/build.gradle.kts .github/workflows/regression.yml


Output:
Chunk ID: eb490e
Wall time: 0.2835 seconds
Process exited with code 0
Original token count: 1770
Output:
   200	    tasks.withType<com.android.build.gradle.internal.tasks.ManagedDeviceInstrumentationTestTask>().configureEach {
   201	        fun isTaskForDevice(
   202	            taskName: String,
   203	            deviceName: String,
   204	        ): Boolean = taskName.equals("$deviceName${testVariantCap}AndroidTest", ignoreCase = true)
   205
   206	        val deviceDirName =
   207	            when {
   208	                isTaskForDevice(name, "phonePixel6api34") -> "phonePixel6api34"
   209	                isTaskForDevice(name, "tablet7Nexus7api34") -> "tablet7Nexus7api34"
   210	                isTaskForDevice(name, "tablet10PixelTabletApi34") -> "tablet10PixelTabletApi34"
   211	                isTaskForDevice(name, "pixel2api33") -> "pixel2api33"
   212	                else -> null
   213	            }
   214	        if (deviceDirName != null) {
   215	            // Set the host-side directory where GMD will pull files from.
   216	            getAdditionalTestOutputDir().set(
   217	                layout.buildDirectory.dir("outputs/managed_device_android_test_additional_output/$testVariant/$deviceDirName"),
   218	            )
   219	        }
   220
   221	        // AGP does not provide a public DSL to set per-ManagedVirtualDevice instrumentation arguments.
   222	        // We use reflection to reach into the internal testData property and its instrumentationRunnerArguments map.
   223	        val getTestDataMethod = javaClass.getMethod("getTestData")
   224	        val testDataProperty = getTestDataMethod.invoke(this) as org.gradle.api.provider.Property<*>
   225	        val testData = testDataProperty.get()
   226	        val getArgsMethod = testData.javaClass.getMethod("getInstrumentationRunnerArguments")
   227
   228	        @Suppress("UNCHECKED_CAST")
   229	        val argsProperty = getArgsMethod.invoke(testData) as org.gradle.api.provider.MapProperty<String, String>
   230
   231	        val myTask = this
   232	        val taskName = name
   233	        val isExplicitAnnotationRun = providers.gradleProperty("android.testInstrumentationRunnerArguments.annotation").isPresent
   234	        argsProperty.putAll(
   235	            provider {
   236	                val map = mutableMapOf<String, String>()
   237	                // We only return arguments if the task is actually executing. This prevents
   238	                // aliasing/merging issues when AGP shares the underlying MapProperty object across tasks.
   239	                if (myTask.state.executing) {
   240	                    when {
   241	                        isTaskForDevice(taskName, "phonePixel6api34") -> {
   242	                            map["package"] = screenshotTestPackage
   243	                            map["additionalTestOutputDir"] = screenshotDeviceOutputDir
   244	                        }
   245	                        isTaskForDevice(taskName, "tablet7Nexus7api34") -> {
   246	                            map["package"] = screenshotTestPackage
   247	                            map["additionalTestOutputDir"] = screenshotDeviceOutputDir
   248	                        }
   249	                        isTaskForDevice(taskName, "tablet10PixelTabletApi34") -> {
   250	                            map["package"] = screenshotTestPackage
   251	                            map["additionalTestOutputDir"] = screenshotDeviceOutputDir
   252	                        }
   253	                        isTaskForDevice(taskName, "pixel2api33") -> {
   254	                            // Keep this exact AOSP smoke task match. A future Play/staging billing
   255	                            // task must not inherit the smoke annotation filter.
   256	                            if (!isExplicitAnnotationRun) {
   257	                                map["annotation"] = smokeAnnotation
   258	                            }
   259	                            map["notPackage"] = screenshotTestPackage
   260	                            map["additionalTestOutputDir"] = smokeDeviceOutputDir
   261	                        }
   262	                    }
   263	                }
   264	                map
   265	            },
   266	        )
   267	    }
   268	}
   269
   270	fun registerRegressionDeviceTask(
   271	    taskName: String,
   272	    descriptionText: String,
   273	    runnerArguments: Map<String, String>,
   274	): TaskProvider<GradleBuild> =
   275	    tasks.register<GradleBuild>(taskName) {
   276	        group = "Verification"
   277	        description = descriptionText
   278	        dir = rootProject.projectDir
   279	        tasks = listOf(":app:$regressionDeviceTaskName")
   280	        startParameter.projectProperties.putAll(
   281	            gradle.startParameter.projectProperties +
   282	                runnerArguments.mapKeys { (key, _) -> "android.testInstrumentationRunnerArguments.$key" },
   283	        )
   284	    }
   285
   286	val regressionCheck =
   287	    registerRegressionDeviceTask(
   288	        taskName = "regressionCheck",
   289	        descriptionText = "Runs core regression androidTests on the Pixel 2 API 33 managed device.",
   290	        runnerArguments =
   291	            mapOf(
   292	                "annotation" to regressionAnnotation,
   293	                "notAnnotation" to quarantinedAnnotation,
   294	            ),
   295	    )
   296
   297	val regressionFullNightlyCoreCheck =
   298	    registerRegressionDeviceTask(
   299	        taskName = "regressionFullNightlyCoreCheck",
   300	        descriptionText = "Runs all core regression androidTests, including quarantined tests.",
   301	        runnerArguments = mapOf("annotation" to regressionAnnotation),
   302	    )
   303
   304	val regressionFullExtendedCheck =
   305	    registerRegressionDeviceTask(
   306	        taskName = "regressionFullExtendedCheck",
   307	        descriptionText = "Runs full regression androidTests on the Pixel 2 API 33 managed device.",
   308	        runnerArguments = mapOf("annotation" to regressionFullAnnotation),
   309	    )
   310
   311	tasks.register("regressionFullCheck") {
   312	    group = "Verification"
   313	    description = "Runs Phase 1 full regression checks; sandbox and macrobenchmark siblings are deferred."
   314	    dependsOn(regressionFullNightlyCoreCheck, regressionFullExtendedCheck)
   315	}
   316
   317	tasks.register<Copy>("generateGmdCoverage") {
   318	    group = "Verification"
   319	    description = "Runs GMD tests and copies the coverage file to the baseline directory."
   320
   321	    // Coverage is always collected from the debug variant of the smoke device
   322	    // regardless of the current -PscreenshotBuild setting.
   323	    val smokeTask = "pixel2api33DebugAndroidTest"
   324	    dependsOn(smokeTask)
   325
   326	    from(layout.buildDirectory.dir("outputs/managed_device_code_coverage/debug/pixel2api33")) {
   327	        include("coverage.ec")
   328	    }
   329	    into(layout.projectDirectory.dir("coverage-baselines"))
   330	    rename { "gmd_smoke.ec" }


Output:
Chunk ID: ecace2
Wall time: 0.2752 seconds
Process exited with code 0
Original token count: 670
Output:
     1	# GMD regression tests on CI.
     2	#
     3	# This mirrors the smoke workflow's emulator setup but keeps PR execution
     4	# opt-in through regression-label.yml. Nightly scheduled runs execute the
     5	# Phase 1 full aggregate; label/manual/reusable calls execute the core tier.
     6	name: Regression
     7	permissions:
     8	  contents: read
     9
    10	on:
    11	  workflow_dispatch:
    12	  workflow_call:
    13	  schedule:
    14	    - cron: '17 8 * * *'
    15
    16	jobs:
    17	  regression:
    18	    name: GMD Regression Tests
    19	    runs-on: ubuntu-latest
    20	    timeout-minutes: 90
    21	    concurrency:
    22	      group: regression-${{ github.ref }}
    23	      cancel-in-progress: true
    24	    steps:
    25	      - name: Checkout code
    26	        uses: actions/checkout@v6
    27
    28	      - name: Set up JDK 17
    29	        uses: actions/setup-java@v5
    30	        with:
    31	          java-version: '17'
    32	          distribution: 'temurin'
    33
    34	      - name: Set up Gradle
    35	        uses: gradle/actions/setup-gradle@v6
    36	        with:
    37	          cache-read-only: ${{ github.ref != 'refs/heads/master' }}
    38	          cache-cleanup: on-success
    39
    40	      - name: Grant execute permission for gradlew
    41	        run: chmod +x gradlew
    42
    43	      - name: Enable KVM
    44	        run: |
    45	          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
    46	            | sudo tee /etc/udev/rules.d/99-kvm4all.rules
    47	          sudo udevadm control --reload-rules
    48	          sudo udevadm trigger --name-match=kvm
    49
    50	      - name: Run Regression Check
    51	        if: github.event_name != 'schedule'
    52	        run: ./gradlew regressionCheck -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
    53
    54	      - name: Run Nightly Regression Checks
    55	        if: github.event_name == 'schedule'
    56	        run: ./gradlew regressionCheck regressionFullCheck -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
    57
    58	      - name: Upload Managed Device Reports
    59	        if: failure()
    60	        uses: actions/upload-artifact@v7
    61	        with:
    62	          name: gmd-regression-test-reports
    63	          path: |
    64	            app/build/reports/androidTests/managedDevice/
    65	            app/build/outputs/androidTest-results/managedDevice/
    66	            app/build/outputs/managed_device_android_test_additional_output/
    67	          if-no-files-found: warn
    68	          retention-days: 7


Output:
Chunk ID: a03f1d
Wall time: 0.2663 seconds
Process exited with code 0
Original token count: 2475
Output:
.github/workflows/regression.yml:56:        run: ./gradlew regressionCheck regressionFullCheck -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:7:class RegressionTierScannerTest {
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:12:        val violations = RegressionTierScanner.scanDirectory(regressionRoot)
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:134:    private fun scanFixture(source: String): List<String> = RegressionTierScanner.scanSources(mapOf("Fixture.kt" to source.trimIndent()))
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:137:private object RegressionTierScanner {
app/build.gradle.kts:53:val regressionFullAnnotation = "com.yorvana.testsupport.tiers.RegressionFull"
app/build.gradle.kts:297:val regressionFullNightlyCoreCheck =
app/build.gradle.kts:299:        taskName = "regressionFullNightlyCoreCheck",
app/build.gradle.kts:304:val regressionFullExtendedCheck =
app/build.gradle.kts:306:        taskName = "regressionFullExtendedCheck",
app/build.gradle.kts:308:        runnerArguments = mapOf("annotation" to regressionFullAnnotation),
app/build.gradle.kts:311:tasks.register("regressionFullCheck") {
app/build.gradle.kts:314:    dependsOn(regressionFullNightlyCoreCheck, regressionFullExtendedCheck)
docs/plan-regression-tests.md:15:- **Flake budget: zero on PR-gated tier, ≤1% on nightly** — quarantine, don't ignore. Flaky tests get a `@Quarantined` marker annotation; PR-gated and core regression tasks pass `notAnnotation=com.yorvana.testsupport.tiers.Quarantined` so these tests are excluded from gating, while the nightly `regressionFullCheck` task does *not* set `notAnnotation` so they still execute and report.
docs/plan-regression-tests.md:28:| Fixture data | Bundle only a **minimal** asset set (one small PDF and one small JPG) in `app/src/androidTest/assets/`. Per #114 deliverable 6, large datasets (20 vehicles, 200 records, attachment-heavy vaults) are **generated in-process** by Phase 2/4/6 helpers rather than checked in as snapshots, so the repo doesn't carry binary asset bloat. | bundled in `app/src/androidTest/assets/` and copied to a known device path in `@Before` (matches smoke plan's recommendation; self-contained on GMD, no `adb` round-trip) |
docs/plan-regression-tests.md:47:- **`regressionFullCheck`** — Nightly. Aggregator task that depends on the sibling tasks below, plus eventually the macrobenchmark module. It does *not* `dependsOn(regressionCheck)`, because that task's `notAnnotation=...Quarantined` filter would otherwise hide quarantined core tests from nightly too (contradicting §1's quarantine policy). **Phase 1 (#114) wires only the first two siblings** — `regressionFullSandboxCheck` is deferred to **Phase 5 (#118)** because it depends on a Play-enabled GMD entry and a non-debug `staging` build type that don't exist yet; the `:macrobenchmark` `dependsOn` is deferred to **Phase 6 (#119)**.
docs/plan-regression-tests.md:49:    - **(a) `GradleBuild` child invocations** — register three `GradleBuild` tasks under `regressionFullCheck`, each setting `startParameter.projectProperties` for its own runner-argument set and `tasks = ["pixel2api33DebugAndroidTest"]` (or the Play device task for sandbox). Each child invocation gets a fresh Gradle build, so `-P` values do not collide.
docs/plan-regression-tests.md:50:    - **(b) Variant- or device-scoped tasks** — declare extra GMD device entries (or a custom test-only variant) whose `instrumentationRunnerArguments` are set in `build.gradle.kts` rather than at the CLI, so each generated `*DebugAndroidTest` task carries its own baked-in filter. `regressionFullCheck` then plain-`dependsOn`s those tasks.
docs/plan-regression-tests.md:52:    - `regressionFullNightlyCoreCheck` — `pixel2api33DebugAndroidTest` with `annotation=com.yorvana.testsupport.tiers.Regression` and **no** `notAnnotation`, so flaky `@Regression @Quarantined` tests still execute and report.
docs/plan-regression-tests.md:53:    - `regressionFullExtendedCheck` — `pixel2api33DebugAndroidTest` with `annotation=com.yorvana.testsupport.tiers.RegressionFull`.
docs/plan-regression-tests.md:54:    - `regressionFullSandboxCheck` — runs `@BillingSandbox` against the Play-enabled device path from §5, using a test APK built from a non-debug build type signed with the same upload key as the Play internal track (e.g. a `staging` `testBuildType` distinct from `debug`/`release` and outside the screenshot-build branch in `app/build.gradle.kts:43`). The concrete task is therefore something like `pixel2api33PlayStagingAndroidTest`, *not* `pixel2api33PlayDebugAndroidTest` — a debug-signed APK is the wrong artifact for real Play Billing. Annotation filter: `annotation=com.yorvana.testsupport.tiers.BillingSandbox`; tier selection is in-build only — runtime skip is owned by the **multi-precondition `Assume.assumeTrue` skip guard** from #118 (`BILLING_SANDBOX_ACCOUNT`, signing material, selected device path, signed-in Play account, and a non-empty `BillingClient.queryProductDetailsAsync(premium_lifetime)` result), *not* a narrow `BILLING_SANDBOX_ACCOUNT`-only gate. If §5 picks the external-workflow option instead, this aggregator entry is omitted (the sandbox tier runs in `billing-sandbox.yml`, not under `regressionFullCheck`).
docs/plan-regression-tests.md:157:- Wire `regressionCheck` and `regressionFullCheck` Gradle tasks per §2 using a **single chosen mechanism** (either `GradleBuild` child invocations with `startParameter.projectProperties`, or variant/device-scoped tasks with baked-in `instrumentationRunnerArguments`) — same shape for both, documented in the PR description. `regressionFullCheck` in Phase 1 wires **two siblings** (`regressionFullNightlyCoreCheck`, `regressionFullExtendedCheck`); `regressionFullSandboxCheck` is deferred to Phase 5 and the `:macrobenchmark` dependency to Phase 6.
docs/plan-regression-tests.md:161:- Bundle a **minimal** fixture set (one small PDF, one small JPG) in `app/src/androidTest/assets/` and the `@Before` / `TestRule` helper that copies them to the device-side location (Downloads via `MediaStore` for picker-visible files, app cache for app-internal reads). No large-vault snapshots — later phases generate large datasets in-process.
docs/plan-regression-tests.md:163:- Add the nightly workflow pair: `regression.yml` (`workflow_call` + `workflow_dispatch` + `schedule`, runs `./gradlew regressionCheck` and `regressionFullCheck` on the cron path, mirrors `smoke.yml`'s KVM setup, forces `swiftshader_indirect`, uploads managed-device reports) and `regression-label.yml` (`pull_request: [labeled]`, gated on `regression` label).
docs/plan-regression-tests.md:190:- Pick the device path: **Option 1** (Play-enabled GMD entry `pixel2api33Play` + `staging` build type → `pixel2api33PlayStagingAndroidTest`), or **Option 2** (external `billing-sandbox.yml` workflow running against a real Play device / Firebase Test Lab physical device, sandbox tier outside `regressionFullCheck`).
docs/plan-regression-tests.md:199:- Register a dedicated GMD entry inside `:macrobenchmark` so AGP generates `:macrobenchmark:pixel2api33BenchmarkAndroidTest`; wire that exact task into `regressionFullCheck`'s `dependsOn` **as a sibling outside any Phase 1 `GradleBuild` child invocation** so app-module instrumentation args don't leak into the benchmark run. Do **not** depend on `:macrobenchmark:connectedBenchmarkAndroidTest`.
docs/plan-regression-tests.md:222:1. **Play-enabled GMD entry with a signed non-debug test build type.** Add a new managed device, e.g. `pixel2api33Play`, with `systemImageSource = "google_apis_playstore"` — this is AGP's documented source for a Play Store image, which Billing requires. (`"google"` / `"google_apis"` ship Google APIs but no Play Store, so they cannot drive real Billing purchase flows.) Add a new build type alongside the existing `debug`/`release` — e.g. `staging` — signed with the same upload key as the Play internal track, and arrange instrumentation to use it for this device only. The current build sets a global `testBuildType = testVariant` in `app/build.gradle.kts:68` (with `testVariant` resolved at line 43 to `debug` outside the screenshot-build branch), so this needs either a per-device override or a top-level switch when the sandbox device is selected; either way, the resulting task name carries the build type (e.g. `pixel2api33PlayStagingAndroidTest`), and `regressionFullSandboxCheck` targets that task — *not* `pixel2api33PlayDebugAndroidTest`, because a debug-signed APK is not what Play has on the internal track and may fail real Billing purchase/restore. Caveats: Play Store images on GMD are interactive (require Google account sign-in on first boot) and historically harder to drive headlessly; this needs a one-time human setup of the license-tester account on the snapshot, plus the signing key has to be available to the runner that builds the staging APK.
docs/plan-regression-tests.md:223:2. **Manual / external device path (recommended when key management on the GMD runner is impractical).** Keep real billing verification outside GMD entirely — run `@BillingSandbox` tests on a real Play-enabled device or a Firebase Test Lab "physical" device, triggered by a separate workflow (`billing-sandbox.yml`) that nightly-builds an internal-track signed APK from a dedicated staging/release build type (so Play recognises the artifact) and runs the sandbox tier against a Play-enabled target. `regressionFullCheck` in this option does *not* dispatch the sandbox invocation locally and instead reports "sandbox runs in the dedicated billing workflow." This option avoids putting the upload key on the GMD runner and skips the headless Play Store sign-in problem from Option 1.


nl -ba app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt | sed -n '1,260p'


Output:
Chunk ID: 62c1dd
Wall time: 0.1217 seconds
Process exited with code 0
Original token count: 2708
Output:
     1	package com.yorvana.testsupport
     2
     3	import com.google.common.truth.Truth.assertThat
     4	import org.junit.Test
     5	import java.io.File
     6
     7	class RegressionTierScannerTest {
     8	    @Test
     9	    fun androidTestRegressionSourcesHaveValidTierAnnotations() {
    10	        val regressionRoot = File("src/androidTest/java/com/yorvana/regression")
    11
    12	        val violations = RegressionTierScanner.scanDirectory(regressionRoot)
    13
    14	        assertThat(violations).isEmpty()
    15	    }
    16
    17	    @Test
    18	    fun zeroTierFixtureFails() {
    19	        val violations =
    20	            scanFixture(
    21	                """
    22	                package com.yorvana.regression
    23
    24	                import org.junit.Test
    25
    26	                class MissingTierTest {
    27	                    @Test
    28	                    fun runsWithoutTier() = Unit
    29	                }
    30	                """,
    31	            )
    32
    33	        assertThat(violations.single()).contains("has 0 tier annotations")
    34	    }
    35
    36	    @Test
    37	    fun twoTierFixtureFails() {
    38	        val violations =
    39	            scanFixture(
    40	                """
    41	                package com.yorvana.regression
    42
    43	                import com.yorvana.testsupport.tiers.Regression
    44	                import com.yorvana.testsupport.tiers.RegressionFull
    45	                import org.junit.Test
    46
    47	                class TooManyTiersTest {
    48	                    @Regression
    49	                    @RegressionFull
    50	                    @Test
    51	                    fun runsWithTwoTiers() = Unit
    52	                }
    53	                """,
    54	            )
    55
    56	        assertThat(violations.single()).contains("has 2 tier annotations")
    57	    }
    58
    59	    @Test
    60	    fun orphanQuarantineFixtureFails() {
    61	        val violations =
    62	            scanFixture(
    63	                """
    64	                package com.yorvana.regression
    65
    66	                import com.yorvana.testsupport.tiers.Quarantined
    67	                import org.junit.Test
    68
    69	                class OrphanQuarantineTest {
    70	                    @Quarantined
    71	                    @Test
    72	                    fun quarantinedWithoutTier() = Unit
    73	                }
    74	                """,
    75	            )
    76
    77	        assertThat(violations).contains("com.yorvana.regression.OrphanQuarantineTest.quarantinedWithoutTier has 0 tier annotations")
    78	        assertThat(violations)
    79	            .contains(
    80	                "com.yorvana.regression.OrphanQuarantineTest.quarantinedWithoutTier " +
    81	                    "is quarantined without a tier at the same effective scope",
    82	            )
    83	    }
    84
    85	    @Test
    86	    fun inheritedClassTierFixturePasses() {
    87	        val violations =
    88	            scanFixture(
    89	                """
    90	                package com.yorvana.regression
    91
    92	                import com.yorvana.testsupport.tiers.Regression
    93	                import org.junit.Test
    94
    95	                @Regression
    96	                open class RegressionBase
    97
    98	                class InheritedTierTest : RegressionBase() {
    99	                    @Test
   100	                    fun inheritsClassTier() = Unit
   101	                }
   102	                """,
   103	            )
   104
   105	        assertThat(violations).isEmpty()
   106	    }
   107
   108	    @Test
   109	    fun annotationArgumentsAndUnknownSiblingsAreAccepted() {
   110	        val violations =
   111	            scanFixture(
   112	                """
   113	                package com.yorvana.regression
   114
   115	                import com.yorvana.testsupport.tiers.Regression
   116	                import com.yorvana.testsupport.tiers.Quarantined
   117	                import org.junit.Test
   118
   119	                annotation class ScenarioId(val value: String)
   120
   121	                class ForwardCompatibleTest {
   122	                    @Regression
   123	                    @Quarantined(since = "2026-05-22", issue = 120, reason = "fixture")
   124	                    @ScenarioId("R-A01")
   125	                    @Test
   126	                    fun acceptsArgumentsAndUnknownSibling() = Unit
   127	                }
   128	                """,
   129	            )
   130
   131	        assertThat(violations).isEmpty()
   132	    }
   133
   134	    private fun scanFixture(source: String): List<String> = RegressionTierScanner.scanSources(mapOf("Fixture.kt" to source.trimIndent()))
   135	}
   136
   137	private object RegressionTierScanner {
   138	    private val TIER_NAMES = setOf("Regression", "RegressionFull", "BillingSandbox")
   139	    private const val QUARANTINE_NAME = "Quarantined"
   140
   141	    fun scanDirectory(root: File): List<String> {
   142	        if (!root.exists()) return emptyList()
   143	        val sources =
   144	            root
   145	                .walkTopDown()
   146	                .filter { it.isFile && it.extension == "kt" }
   147	                .associate { it.path to it.readText() }
   148	        return scanSources(sources)
   149	    }
   150
   151	    fun scanSources(sources: Map<String, String>): List<String> {
   152	        val classes = sources.values.flatMap { source -> parseSource(source) }
   153	        val bySimpleName = classes.associateBy { it.simpleName }
   154	        val violations = mutableListOf<String>()
   155
   156	        classes.forEach { testClass ->
   157	            testClass.testMethods.forEach { method ->
   158	                val scopes =
   159	                    listOf(
   160	                        AnnotationScope(method.annotations),
   161	                        AnnotationScope(testClass.annotations),
   162	                    ) +
   163	                        inheritedClassScopes(testClass, bySimpleName)
   164	                val effectiveTierScope = scopes.firstOrNull { scope -> scope.annotations.count { it in TIER_NAMES } > 0 }
   165	                val tierCount = effectiveTierScope?.annotations?.count { it in TIER_NAMES } ?: 0
   166	                val effectiveQuarantineScope = scopes.firstOrNull { QUARANTINE_NAME in it.annotations }
   167
   168	                if (tierCount != 1) {
   169	                    violations += "${testClass.fqName}.${method.name} has $tierCount tier annotations"
   170	                }
   171	                if (effectiveQuarantineScope != null && effectiveQuarantineScope != effectiveTierScope) {
   172	                    violations +=
   173	                        "${testClass.fqName}.${method.name} is quarantined without a tier at the same effective scope"
   174	                }
   175	            }
   176	        }
   177
   178	        return violations
   179	    }
   180
   181	    private fun inheritedClassScopes(
   182	        testClass: ParsedClass,
   183	        bySimpleName: Map<String, ParsedClass>,
   184	    ): List<AnnotationScope> {
   185	        val scopes = mutableListOf<AnnotationScope>()
   186	        var currentSuperName = testClass.superClassName
   187	        val seen = mutableSetOf<String>()
   188	        while (currentSuperName != null && seen.add(currentSuperName)) {
   189	            val superClass = bySimpleName[currentSuperName] ?: break
   190	            scopes += AnnotationScope(superClass.annotations)
   191	            currentSuperName = superClass.superClassName
   192	        }
   193	        return scopes
   194	    }
   195
   196	    private fun parseSource(source: String): List<ParsedClass> {
   197	        val packageName =
   198	            Regex("""^\s*package\s+([A-Za-z_][\w.]*)""", RegexOption.MULTILINE)
   199	                .find(source)
   200	                ?.groupValues
   201	                ?.get(1)
   202	                .orEmpty()
   203	        val classes = mutableListOf<ParsedClass>()
   204	        var currentClass: ParsedClass? = null
   205	        var pendingAnnotations = emptySet<String>()
   206
   207	        source.lineSequence().forEach { rawLine ->
   208	            val line = rawLine.trim()
   209	            val annotations = extractAnnotations(line)
   210	            if (annotations.isNotEmpty()) {
   211	                pendingAnnotations = pendingAnnotations + annotations
   212	            }
   213
   214	            val classMatch = Regex("""\bclass\s+([A-Za-z_]\w*)\s*(?::\s*([A-Za-z_][\w.]*))?""").find(line)
   215	            if (classMatch != null) {
   216	                val simpleName = classMatch.groupValues[1]
   217	                val superClassName =
   218	                    classMatch.groupValues
   219	                        .getOrNull(2)
   220	                        ?.takeIf { it.isNotBlank() }
   221	                        ?.substringAfterLast('.')
   222	                currentClass =
   223	                    ParsedClass(
   224	                        packageName = packageName,
   225	                        simpleName = simpleName,
   226	                        superClassName = superClassName,
   227	                        annotations = pendingAnnotations,
   228	                        testMethods = mutableListOf(),
   229	                    )
   230	                classes += currentClass
   231	                pendingAnnotations = emptySet()
   232	                return@forEach
   233	            }
   234
   235	            val methodMatch = Regex("""\bfun\s+([A-Za-z_]\w*)\s*\(""").find(line)
   236	            if (methodMatch != null) {
   237	                if ("Test" in pendingAnnotations) {
   238	                    currentClass?.testMethods?.add(
   239	                        ParsedMethod(
   240	                            name = methodMatch.groupValues[1],
   241	                            annotations = pendingAnnotations,
   242	                        ),
   243	                    )
   244	                }
   245	                pendingAnnotations = emptySet()
   246	            }
   247	        }
   248
   249	        return classes
   250	    }
   251
   252	    private fun extractAnnotations(line: String): Set<String> =
   253	        Regex("""@([A-Za-z_][\w.]*)""")
   254	            .findAll(line)
   255	            .map { it.groupValues[1].substringAfterLast('.') }
   256	            .toSet()
   257
   258	    private data class ParsedClass(
   259	        val packageName: String,
   260	        val simpleName: String,


nl -ba app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt | sed -n '260,340p'


Output:
Chunk ID: 2cc1bb
Wall time: 0.1242 seconds
Process exited with code 0
Original token count: 149
Output:
   260	        val simpleName: String,
   261	        val superClassName: String?,
   262	        val annotations: Set<String>,
   263	        val testMethods: MutableList<ParsedMethod>,
   264	    ) {
   265	        val fqName: String = listOf(packageName, simpleName).filter { it.isNotBlank() }.joinToString(".")
   266	    }
   267
   268	    private data class ParsedMethod(
   269	        val name: String,
   270	        val annotations: Set<String>,
   271	    )
   272
   273	    private data class AnnotationScope(
   274	        val annotations: Set<String>,
   275	    )
   276	}


# Address PR #123 Review Comment

## Summary
Fix all actionable review items on PR #123 by tightening the regression Gradle task naming/docs, removing duplicate scheduled CI work, documenting scanner limitations, and aligning fixture docs with what Phase 1 actually ships.

## Key Changes
- Update `.github/workflows/regression.yml` so scheduled runs execute only `./gradlew regressionFullCheck ...`; label/manual/reusable runs keep `regressionCheck`.
- Rename `regressionFullNightlyCoreCheck` to `regressionFullCoreCheck` in Gradle and docs. Keep `regressionFullCheck` depending on core + extended regression tasks.
- Add Gradle comments around:
  - eager `isExplicitAnnotationRun` evaluation being intentional for the outer build and child `GradleBuild` invocations.
  - regression aggregate tasks intentionally overriding any outer runner-argument `annotation` / `notAnnotation` project properties.
- Add a focused comment to `RegressionTierScanner` documenting its supported Kotlin source shapes and known parser limits.
- Add a scanner fixture test proving an unsupported shape currently passes unvalidated, so the limitation is explicit.
- Update `docs/plan-regression-tests.md` to stop claiming Phase 1 bundles a JPG unless an actual JPG is present; default to “small PDF now, JPG follows in Phase 2 if needed.”

## Test Plan
- Run `./gradlew testDebugUnitTest --tests com.yorvana.testsupport.RegressionTierScannerTest`.
- Run `./gradlew testDebugUnitTest`.
- Run `./gradlew ktlintCheck detekt lintDebug`.
- Run `./gradlew regressionCheck regressionFullCheck --dry-run`.

## Assumptions
- Treat the single review comment as requesting fixes for all six numbered items.
- Do not add a JPG fixture in this follow-up because the current code only added `test_receipt.pdf`, and the doc-only correction has the smallest blast radius.
- Preserve the existing `GradleBuild` approach rather than redesigning regression task wiring.


<proposed_plan>
# Address PR #123 Review Comment

## Summary
Fix all actionable review items on PR #123 by tightening the regression Gradle task naming/docs, removing duplicate scheduled CI work, documenting scanner limitations, and aligning fixture docs with what Phase 1 actually ships.

## Key Changes
- Update `.github/workflows/regression.yml` so scheduled runs execute only `./gradlew regressionFullCheck ...`; label/manual/reusable runs keep `regressionCheck`.
- Rename `regressionFullNightlyCoreCheck` to `regressionFullCoreCheck` in Gradle and docs. Keep `regressionFullCheck` depending on core + extended regression tasks.
- Add Gradle comments around:
  - eager `isExplicitAnnotationRun` evaluation being intentional for the outer build and child `GradleBuild` invocations.
  - regression aggregate tasks intentionally overriding any outer runner-argument `annotation` / `notAnnotation` project properties.
- Add a focused comment to `RegressionTierScanner` documenting its supported Kotlin source shapes and known parser limits.
- Add a scanner fixture test proving an unsupported shape currently passes unvalidated, so the limitation is explicit.
- Update `docs/plan-regression-tests.md` to stop claiming Phase 1 bundles a JPG unless an actual JPG is present; default to “small PDF now, JPG follows in Phase 2 if needed.”

## Test Plan
- Run `./gradlew testDebugUnitTest --tests com.yorvana.testsupport.RegressionTierScannerTest`.
- Run `./gradlew testDebugUnitTest`.
- Run `./gradlew ktlintCheck detekt lintDebug`.
- Run `./gradlew regressionCheck regressionFullCheck --dry-run`.

## Assumptions
- Treat the single review comment as requesting fixes for all six numbered items.
- Do not add a JPG fixture in this follow-up because the current code only added `test_receipt.pdf`, and the doc-only correction has the smallest blast radius.
- Preserve the existing `GradleBuild` approach rather than redesigning regression task wiring.
</proposed_plan>


<collaboration_mode># Collaboration Mode: Default

You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.

Your active mode changes only when new developer instructions with a different `<collaboration_mode>...</collaboration_mode>` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan.

## request_user_input availability

Use the `request_user_input` tool only when it is listed in the available tools for this turn.

In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.
</collaboration_mode>


# Collaboration Mode: Default

You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.

Your active mode changes only when new developer instructions with a different `<collaboration_mode>...</collaboration_mode>` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan.

## request_user_input availability

Use the `request_user_input` tool only when it is listed in the available tools for this turn.

In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.


Implement the plan.


I’ll apply the review fixes directly against the PR branch, then run the focused Gradle checks before committing.


rg -n "regressionFullNightlyCoreCheck|regressionFullCheck|small PDF|small JPG|RegressionTierScanner|isExplicitAnnotationRun|runnerArguments" app/build.gradle.kts .github/workflows/regression.yml docs/plan-regression-tests.md app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt


Output:
Chunk ID: ff1850
Wall time: 0.2147 seconds
Process exited with code 0
Original token count: 17
Output:
## issue-114-regression-infra...origin/issue-114-regression-infra


Output:
Chunk ID: 8c8a5c
Wall time: 0.2033 seconds
Process exited with code 0
Original token count: 2142
Output:
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:7:class RegressionTierScannerTest {
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:12:        val violations = RegressionTierScanner.scanDirectory(regressionRoot)
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:134:    private fun scanFixture(source: String): List<String> = RegressionTierScanner.scanSources(mapOf("Fixture.kt" to source.trimIndent()))
app/src/test/java/com/yorvana/testsupport/RegressionTierScannerTest.kt:137:private object RegressionTierScanner {
docs/plan-regression-tests.md:15:- **Flake budget: zero on PR-gated tier, ≤1% on nightly** — quarantine, don't ignore. Flaky tests get a `@Quarantined` marker annotation; PR-gated and core regression tasks pass `notAnnotation=com.yorvana.testsupport.tiers.Quarantined` so these tests are excluded from gating, while the nightly `regressionFullCheck` task does *not* set `notAnnotation` so they still execute and report.
docs/plan-regression-tests.md:28:| Fixture data | Bundle only a **minimal** asset set (one small PDF and one small JPG) in `app/src/androidTest/assets/`. Per #114 deliverable 6, large datasets (20 vehicles, 200 records, attachment-heavy vaults) are **generated in-process** by Phase 2/4/6 helpers rather than checked in as snapshots, so the repo doesn't carry binary asset bloat. | bundled in `app/src/androidTest/assets/` and copied to a known device path in `@Before` (matches smoke plan's recommendation; self-contained on GMD, no `adb` round-trip) |
docs/plan-regression-tests.md:47:- **`regressionFullCheck`** — Nightly. Aggregator task that depends on the sibling tasks below, plus eventually the macrobenchmark module. It does *not* `dependsOn(regressionCheck)`, because that task's `notAnnotation=...Quarantined` filter would otherwise hide quarantined core tests from nightly too (contradicting §1's quarantine policy). **Phase 1 (#114) wires only the first two siblings** — `regressionFullSandboxCheck` is deferred to **Phase 5 (#118)** because it depends on a Play-enabled GMD entry and a non-debug `staging` build type that don't exist yet; the `:macrobenchmark` `dependsOn` is deferred to **Phase 6 (#119)**.
docs/plan-regression-tests.md:49:    - **(a) `GradleBuild` child invocations** — register three `GradleBuild` tasks under `regressionFullCheck`, each setting `startParameter.projectProperties` for its own runner-argument set and `tasks = ["pixel2api33DebugAndroidTest"]` (or the Play device task for sandbox). Each child invocation gets a fresh Gradle build, so `-P` values do not collide.
docs/plan-regression-tests.md:50:    - **(b) Variant- or device-scoped tasks** — declare extra GMD device entries (or a custom test-only variant) whose `instrumentationRunnerArguments` are set in `build.gradle.kts` rather than at the CLI, so each generated `*DebugAndroidTest` task carries its own baked-in filter. `regressionFullCheck` then plain-`dependsOn`s those tasks.
docs/plan-regression-tests.md:52:    - `regressionFullNightlyCoreCheck` — `pixel2api33DebugAndroidTest` with `annotation=com.yorvana.testsupport.tiers.Regression` and **no** `notAnnotation`, so flaky `@Regression @Quarantined` tests still execute and report.
docs/plan-regression-tests.md:54:    - `regressionFullSandboxCheck` — runs `@BillingSandbox` against the Play-enabled device path from §5, using a test APK built from a non-debug build type signed with the same upload key as the Play internal track (e.g. a `staging` `testBuildType` distinct from `debug`/`release` and outside the screenshot-build branch in `app/build.gradle.kts:43`). The concrete task is therefore something like `pixel2api33PlayStagingAndroidTest`, *not* `pixel2api33PlayDebugAndroidTest` — a debug-signed APK is the wrong artifact for real Play Billing. Annotation filter: `annotation=com.yorvana.testsupport.tiers.BillingSandbox`; tier selection is in-build only — runtime skip is owned by the **multi-precondition `Assume.assumeTrue` skip guard** from #118 (`BILLING_SANDBOX_ACCOUNT`, signing material, selected device path, signed-in Play account, and a non-empty `BillingClient.queryProductDetailsAsync(premium_lifetime)` result), *not* a narrow `BILLING_SANDBOX_ACCOUNT`-only gate. If §5 picks the external-workflow option instead, this aggregator entry is omitted (the sandbox tier runs in `billing-sandbox.yml`, not under `regressionFullCheck`).
docs/plan-regression-tests.md:157:- Wire `regressionCheck` and `regressionFullCheck` Gradle tasks per §2 using a **single chosen mechanism** (either `GradleBuild` child invocations with `startParameter.projectProperties`, or variant/device-scoped tasks with baked-in `instrumentationRunnerArguments`) — same shape for both, documented in the PR description. `regressionFullCheck` in Phase 1 wires **two siblings** (`regressionFullNightlyCoreCheck`, `regressionFullExtendedCheck`); `regressionFullSandboxCheck` is deferred to Phase 5 and the `:macrobenchmark` dependency to Phase 6.
docs/plan-regression-tests.md:161:- Bundle a **minimal** fixture set (one small PDF, one small JPG) in `app/src/androidTest/assets/` and the `@Before` / `TestRule` helper that copies them to the device-side location (Downloads via `MediaStore` for picker-visible files, app cache for app-internal reads). No large-vault snapshots — later phases generate large datasets in-process.
docs/plan-regression-tests.md:163:- Add the nightly workflow pair: `regression.yml` (`workflow_call` + `workflow_dispatch` + `schedule`, runs `./gradlew regressionCheck` and `regressionFullCheck` on the cron path, mirrors `smoke.yml`'s KVM setup, forces `swiftshader_indirect`, uploads managed-device reports) and `regression-label.yml` (`pull_request: [labeled]`, gated on `regression` label).
docs/plan-regression-tests.md:190:- Pick the device path: **Option 1** (Play-enabled GMD entry `pixel2api33Play` + `staging` build type → `pixel2api33PlayStagingAndroidTest`), or **Option 2** (external `billing-sandbox.yml` workflow running against a real Play device / Firebase Test Lab physical device, sandbox tier outside `regressionFullCheck`).
docs/plan-regression-tests.md:199:- Register a dedicated GMD entry inside `:macrobenchmark` so AGP generates `:macrobenchmark:pixel2api33BenchmarkAndroidTest`; wire that exact task into `regressionFullCheck`'s `dependsOn` **as a sibling outside any Phase 1 `GradleBuild` child invocation** so app-module instrumentation args don't leak into the benchmark run. Do **not** depend on `:macrobenchmark:connectedBenchmarkAndroidTest`.
docs/plan-regression-tests.md:223:2. **Manual / external device path (recommended when key management on the GMD runner is impractical).** Keep real billing verification outside GMD entirely — run `@BillingSandbox` tests on a real Play-enabled device or a Firebase Test Lab "physical" device, triggered by a separate workflow (`billing-sandbox.yml`) that nightly-builds an internal-track signed APK from a dedicated staging/release build type (so Play recognises the artifact) and runs the sandbox tier against a Play-enabled target. `regressionFullCheck` in this option does *not* dispatch the sandbox invocation locally and instead reports "sandbox runs in the dedicated billing workflow." This option avoids putting the upload key on the GMD runner and skips the headless Play Store sign-in problem from Option 1.
.github/workflows/regression.yml:56:        run: ./gradlew regressionCheck regressionFullCheck -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
app/build.gradle.kts:233:        val isExplicitAnnotationRun = providers.gradleProperty("android.testInstrumentationRunnerArguments.annotation").isPresent
app/build.gradle.kts:256:                            if (!isExplicitAnnotationRun) {
app/build.gradle.kts:273:    runnerArguments: Map<String, String>,
app/build.gradle.kts:282:                runnerArguments.mapKeys { (key, _) -> "android.testInstrumentationRunnerArguments.$key" },
app/build.gradle.kts:290:        runnerArguments =
app/build.gradle.kts:297:val regressionFullNightlyCoreCheck =
app/build.gradle.kts:299:        taskName = "regressionFullNightlyCoreCheck",
app/build.gradle.kts:301:        runnerArguments = mapOf("annotation" to regressionAnnotation),
app/build.gradle.kts:308:        runnerArguments = mapOf("annotation" to regressionFullAnnotation),
app/build.gradle.kts:311:tasks.register("regressionFullCheck") {
app/build.gradle.kts:314:    dependsOn(regressionFullNightlyCoreCheck, regressionFullExtendedCheck)


I found the exact spots from the review. I’m going to make narrowly scoped edits: rename the core full-regression task, remove the scheduled duplicate run, add the requested comments/tests, and correct the fixture wording in the plan doc.