Imported from Pear-Commerce/pear-ai-skills (
skills/pear-engineering-workflow/SKILL.md). Install upstream withnpx skills add Pear-Commerce/pear-ai-skills --skill pear-engineering-workflow. Copyright stays with the author.
Pear Engineering Workflow
Keep Pear code work grounded in repo patterns, real data, and the actual browser flow.
Skill Source
Canonical repo: https://github.com/Pear-Commerce/pear-ai-skills. For skill edits, update and push skills/pear-engineering-workflow/SKILL.md there first, then sync installed/vendored copies. For app repos other than api.pearcommerce.com, commit synced copies directly after verification; for api.pearcommerce.com, use a codex/ branch and PR.
Shell Environment
Pear repos rely on Alex's interactive shell setup for common tools such as gh, node, npm, npx, zx, nvm, SSH agent state, and repo helpers like devops/env.sh. For Pear work, keep commands in the default Codex login shell or run them through zsh -lc '...' when the command may need those tools.
Avoid switching to non-login Bash, especially shell=/bin/bash with login=false, for deploys, GitHub CLI work, Node-backed scripts, devops/* helpers, dry runs that call repo scripts, or checks that shell out to Pear tooling. If Bash is genuinely needed, first seed PATH from a known-good login zsh or explicitly include the active nvm Node bin directory, then verify with command -v gh npx node npm before running the real command. If a Pear command fails with gh, node, npm, npx, or zx missing, treat it as a shell-environment issue before debugging repo behavior.
For CloudWatch, RDS, or other AWS-backed investigations, also check the login shell before deciding tools are missing: zsh -lc 'command -v aws python3 pip3'. Use $pear-log-search for saved or cross-environment application logs, db.sh for data, and pear-prod-jsp for live JVM state. Reserve devops/logs.sh for immediate single-instance tailing. If direct AWS API access is genuinely needed and aws/boto3 is unavailable, keep any temporary Python dependency install in a temp venv/path outside the repo, avoid changing repo dependency files, and describe that as a local tooling workaround rather than a production or application finding.
AWS SSO Prerequisite
Before running any AWS CLI command in this skill (AppConfig, SSM, CloudWatch, db.sh, logs.sh, JSP via SSM), proactively run:
aws sso login --profile pear-sso
This opens the user's Chrome browser for authentication and blocks until approved. Never attempt AWS commands with stale credentials — if you see UnrecognizedClientException or Token has expired, run the login command first and retry. See $pear-aws for full credential troubleshooting.
AppConfig Changes And Publishing
Use the bundled scripts/appconfig-set-value.sh for Pear API Base (All) AppConfig changes. It encodes the production IDs, preserves JSON types, uses optimistic version locking, creates the hosted version with fileb://, deploys it, and verifies both the value and deployment state.
Before changing a value, inspect the exact code callsite and its default. Confirm the namespace, key, expected type, and whether the code reads it live or only during startup. A normal getBooleanNow/getIntegerNow/getStringNow call is live-read and normally needs no application restart; startup-gated behavior may still require a service redeploy.
Run a no-write preview first:
APP_CONFIG_SETTER="${PEAR_AI_SKILLS_REPO:-$HOME/pear-ai-skills}/skills/pear-engineering-workflow/scripts/appconfig-set-value.sh"
"$APP_CONFIG_SETTER" \
--namespace instacart-prewarm-pool \
--key max-workers \
--json-value '300' \
--label ic-workers-300 \
--description 'Set list-scraper worker limit to 300'
Only add --publish when the user has explicitly authorized the live change and publication:
"$APP_CONFIG_SETTER" \
--namespace instacart-prewarm-pool \
--key max-workers \
--json-value '300' \
--label ic-workers-300 \
--description 'Set list-scraper worker limit to 300' \
--publish
--json-value must be valid typed JSON: use 'true', '120', or '"text"', not unquoted text. Existing keys preserve their schema. For a genuinely new key, also pass --type boolean|number|string; the script adds the matching flag attribute and refuses to exceed AppConfig's 25-attribute namespace limit. Do not repurpose or delete an existing attribute merely to bypass that limit without explicit approval.
Canonical Pear API AppConfig resources are application k54elgs (Pear API), profile dohcrfo (Base (All)), environment zxye02b (Base (All)), region us-east-1, and immediate deployment strategy id0iigs. Override these only when intentionally targeting a different resource.
Common first-attempt failures this script avoids:
- Updating only
.valuesfor a new key while omitting.flags.<namespace>.attributes.<key>. - Passing a string where the schema expects a boolean or number.
- Using
file://instead offileb://forcreate-hosted-configuration-version. - Creating from a stale base version;
--latest-version-numbermust match the current hosted version. - Treating a created hosted version as published;
start-deploymentis a separate required call. - Reporting success without checking
get-deploymentand re-reading the new hosted value.
If the helper is unavailable, reproduce its sequence exactly: download the latest hosted version, update it with jq --argjson, validate with jq -e, create a new version with --latest-version-number, start a deployment, then verify the deployment state and hosted value. Never edit a stale local AppConfig JSON file and publish it over newer changes.
Offers Deploy Safety
For offers.pearcommerce.com, never repair or deploy CDN/static asset content by pushing local workstation files to S3/R2/CloudFront/Cloudflare or any production bucket. Do not use aws s3 cp, aws s3 sync, s3api put-object, R2 object writes, Cloudflare direct uploads, or equivalent local artifact pushes for Offers production or staging assets, including emergency fixes.
Never deploy production from a local or dev build. Production Offers artifacts must be built and uploaded by the GitHub Actions/CI deploy pipeline from the merged source branch, using production configuration. If a live asset is stale or wrong, fix the source, deploy script, cache headers, or invalidation path in code; merge through PR/CI; trigger or rerun the CI deploy; then verify the public URLs and browser flow.
CDN cache purges/invalidations may be run when needed to make already-deployed CI artifacts visible, but they must not be paired with local object-content writes. If a situation appears to require manual production object mutation, stop and escalate instead of improvising from local files.
Pear API Curl Header
When curling Pear API hosts from a local shell, include the trusted-edge header before treating a Cloudflare 403/block page as endpoint behavior. This applies to https://api.pearcommerce.com and https://test.api.pearcommerce.com requests, including validation probes, cache invalidation calls, retailer-list reads, JSP raw-output curls, and other direct API checks. The header shape comes from the Admin/Offers Cloudflare invalidation scripts:
PEAR_TRUSTED_EDGE_VALUE="${PEAR_TRUSTED_EDGE_HEADER:-${PEAR_TRUSTED_EDGE:-a1360351-32b2-4410-9c87-ec294e780c25}}"
curl -fsS -H "x-pear-trusted-edge: ${PEAR_TRUSTED_EDGE_VALUE}" "https://api.pearcommerce.com/v1/..."
Use this only for Pear API hosts. Do not add it to third-party retailer APIs, partners.pearcommerce.com, raw Offers page loads, Cloudflare's own API, or unrelated domains. If the trusted-edge header still returns a Cloudflare block, switch to browser verification or a narrow JSP/server-side read path instead of escalating local curl variants.
DevRev Ticket Links
Every Pear PR should carry a DevRev work link before it is opened for review or handed back as ready. Treat this as a SOC2 auditability requirement: code changes need a durable work record that ties intent, approval context, implementation, and verification back to the merged PR. Before creating, updating, or backfilling a PR in Pear repos, check the title and body for an existing DevRev ID or link, such as ISS-7045, TKT-717, or https://app.devrev.ai/pearcommerce/works/ISS-7045.
If no DevRev work exists for the change, create one before the PR is ready for review whenever DevRev tooling or credentials are available. The ticket should be specific enough to stand on its own: use the PR or task title, summarize the user-visible change or operational reason, include the repo name, PR URL if one exists, and enough implementation/testing context for a reviewer to understand why the code changed. Assign the DevRev work to the PR submitter when that GitHub author can be resolved to an active DevRev user; for historical backfills this is more accurate than assigning everything to the operator running the backfill. If the PR submitter cannot be resolved, use the best accountable fallback owner and record that fallback in the ticket body or backfill notes. If DevRev access is unavailable, do not silently skip the link; mark the PR as blocked on ticket creation or report the missing credential/tooling in the final response.
This requirement applies to process-only, documentation-only, skill-only, and compliance-only changes too; do not skip DevRev linkage just because the change has no application runtime behavior.
When creating a new (non-backfill) DevRev issue for a PR, set applies_to_part to PROD-9 "Engineering Projects" as the default, unless the change clearly belongs to a more specific existing part (e.g. a named capability like Locator or PDP). Reserve ENH-27 "SOC2" for work that is actually about SOC2/compliance itself (audit findings, control implementation, compliance-driven remediation) — not as a catch-all for general PR-traceability tickets just because DevRev linkage is a SOC2 auditability requirement.
Use this PR body format unless the repo template already has a better location:
## DevRev Ticket
https://app.devrev.ai/pearcommerce/works/ISS-####
For newly created Codex PRs, include the DevRev link in the initial body. For existing PRs, edit the PR body rather than leaving the ticket only in a comment. For closed and merged PR backfills, only process PRs whose mergedAt/merged_at is set; skip closed-unmerged PRs. Create or reuse one DevRev work item per PR unless the user explicitly asks to group related PRs, then edit the historical PR body with a ## DevRev Ticket section so future searches find the link.
Once a DevRev work ID is known for new work, prefix the commit subject with that ID and a colon. Use the same primary ID that appears in the PR body:
git commit -m "ISS-1234: Add foo to bar"
If a branch genuinely spans multiple DevRev works, use the primary issue or ticket that best describes the PR. Do not rewrite old merged commits solely to retrofit this convention during historical PR-body backfills.
Terse PR Summaries
PR bodies must be as terse as possible; long AI-generated PR descriptions are hard to read and reviewers skip them. Rules:
- Body = DevRev link + a short why (a sentence or two) + at most ~5 bullets, one line each. Must fit one screen.
- No
Summary/Scan notes/Validation/Test plansection walls. Link to the CI run, ticket, or log query instead of pasting evidence. - Do not restate the diff or the title, and do not narrate process. Say what changed, why, and anything a reviewer could not infer from the code.
- Titles: imperative and specific, e.g.
Add X to Y, notSome changes to X.
The PR-improvement guide (codex-pr-improvement-goal.md, "Terse PR Summaries") enforces the same rules at review time.
Review Rules
Before calling code changes done, read the PR-improvement guide. Prefer:
sed -n '1,240p' /Users/alexwyler/pear-ai-skills/skills/pear-engineering-workflow/references/codex-pr-improvement-goal.md
Fallbacks:
sed -n '1,240p' skills/pear-engineering-workflow/references/codex-pr-improvement-goal.md
curl -fsSL https://raw.githubusercontent.com/Pear-Commerce/pear-ai-skills/main/skills/pear-engineering-workflow/references/codex-pr-improvement-goal.md | sed -n '1,240p'
sed -n '1,240p' /Users/alexwyler/.codex/skills/pear-engineering-workflow/references/codex-pr-improvement-goal.md
Apply it as a checklist: clear ownership, existing helpers first, explicit async/failure behavior, useful observability, focused deterministic tests, reviewable diff. Keep most new behavior in purpose-owned modules; existing-code touchpoints should stay minimal, e.g. shared utility updates, registry hooks, dependency wiring, or thin call-site handoffs.
When reviewing your own code, compare the final diff against the user's stated goal and remove superfluous changes before calling the work done. Treat unrelated cleanup, speculative abstractions, incidental formatting churn, extra workflow tweaks, and diagnostic/profiling leftovers as review blockers unless the user explicitly asked for them or they are required for the requested behavior.
Prefer importing classes over inlining fully-qualified class names in Java code. Add import java.util.List; at the top of the file and use List in the body, not java.util.List<...> inline. Fully-qualified names make expressions harder to scan and are only justified when two classes share the same simple name from different packages. The PR-improvement guide enforces the same rule at review time.
Before adding utility-like code, search for the existing home and use or extend it. Environment names belong in ServerEnv; AppConfig parsing/default shaping belongs in AWSAppConfigUtil; queue/concurrency helpers should follow pear-concurrency/pear-jobs; SimpleORM data access should follow pear-orm. Do not duplicate normalization, parsing, retry, locking, or config code in a feature module when a shared utility exists.
New feature/domain helpers must default to Spring services with explicit constructor injection. Put @Autowired on non-empty constructors in new/changed Spring classes, including services and controllers, so dependency wiring is obvious in review. Avoid field injection, manual new, static registries/helpers for behavior, and feature-owned state hidden behind global calls unless the code is a pure reusable value utility with no collaborators and no expected test injection. Before finalizing, scan touched packages for @Autowired, Resources.global, ManagedResourcesConfig.getBean, static, and new <feature class> and move collaborators into small injected @Service modules where practical.
For retailer integrations, this is a hard default: client/helper classes that fetch live pages or APIs, parse retailer payloads, resolve UPCs, load store artifacts, compute PDP URLs, or check availability should be @Service beans injected into resolvers/updaters/tests. Static constants and DTOs are fine; static behavior helpers in production retailer code are not.
Pear Entity Serialization
Production API paths may serialize PearEntity objects through SimpleORM, which emits only id and @SimpleORMField fields. Plain public fields, transient, and @JsonProperty can pass local ObjectMapper tests yet disappear from real responses.
Strict rule: never put any value the client must receive on a PearEntity as a transient field. Treat transient as not JSON-serialized in real PearEntity API responses, even if a local Jackson/ObjectMapper test appears to include it or a @JsonProperty annotation is present. For hydrated/computed/UI-only/response-only data, use an explicit response DTO or response-shaping mapper. Add @SimpleORMField only for intentional DB schema/storage. Tests should exercise the endpoint response, DTO, or production serializer; avoid new ObjectMapper() entity tests unless that is the real call path.
Serialized JSON DTOs
For external API response/request DTOs, scraper payload models, JSON-LD/schema.org models, app/webhook payloads, and other classes whose fields are populated by Jackson or Pear JSON, prefer representing as many upstream JSON fields as practical. These fields document the payload shape for future readers, make debugger inspection easier, and reduce rediscovery when another scraper/resolver path later needs the same data.
For simple Jackson/Gson/Pear JSON payload shapes, prefer public static nested DTO classes with public fields when that is the local convention. This is the default for external API request/response DTOs, scraper payloads, JSON-LD models, and webhook/app payloads that are just data carriers.
If Error Prone reports EffectivelyPrivate on public fields inside a private nested JSON DTO, fix the class visibility first: make the DTO public static when the public-field payload shape is intentional. Do not "fix" that warning by making serialized/deserialized DTO fields private or package-private, and do not add @JsonProperty solely to preserve names after narrowing field visibility. Keeping the DTO class public and the fields public is the intended Pear convention for these simple payload models.
Do not make DTO classes or fields private/package-private solely to satisfy EffectivelyPrivate or similar unused-field warnings when framework serialization/deserialization depends on visibility or the public-field shape is intentional.
Do not remove serialized JSON DTO fields solely because the current production code does not read them. A Copilot or reviewer comment like "field X is deserialized but never read" is usually not sufficient reason to delete it. Stand firm graciously: explain that unused-but-real DTO fields intentionally preserve the upstream contract, especially in retailer integrations and API clients.
This guidance is different from dead behavior. Remove stale helpers, duplicate DTO classes, fields that are proven not to exist upstream, sensitive fields we should not retain, fields that actively mislead readers, or fields whose parsing has meaningful performance/memory cost in a hot path. When in doubt, keep the field and add a short comment only if the retention would otherwise look surprising.
Spring Tests
In api.pearcommerce.com, any JUnit test that needs Spring-managed beans, method-parameter @Autowired, awsAppConfigUtil, Persistence, Resources, or the Pear app test context should usually extend BasePearScript because it loads the Spring/Pear test context. If a test sees null Persistence.global(), missing Resources, or uninitialized autowired collaborators, first check whether it should be based on BasePearScript. Keep pure unit tests plain, but do not add ad hoc Spring annotations or manual context setup when BasePearScript is the repo pattern. Make Spring-backed tests deterministic by creating required SimpleORM rows in the test instead of assuming CI seed data contains them.
In api.pearcommerce.com, default Gradle tests that load Spring, SimpleORM, Resources, AppConfig, Snowflake, UPC resolution scripts, vendors, or real entity data to the shared dev DB, not local MySQL. Prefix those ./gradlew test, testCI, or similar commands with:
MYSQL_CREDENTIALS_SECRET=prod-db-10-2025 \
MYSQL_HOST=analytics-database.pearcommerce.com \
MYSQL_HOST_READ=analytics-database.pearcommerce.com \
MYSQL_HOST_WRITE=analytics-database.pearcommerce.com \
SNOWFLAKE_CREDENTIALS_SECRET=snowflake-2025-12-01 \
./gradlew test --tests ...
Pure compile checks and pure unit tests that do not touch Pear resources can run without the DB prefix. If a local test or app page is missing vendors, users, UPC imports, resolver rows, or auth-related data, suspect an accidental local-DB run before debugging feature code.
Browser Profiles For HTTP Work
For scraper, resolver, availability, store-locator, or API-client work using LoggedJurl/JurlProxyFallback, remember that browser-like headers are not always enough. If .asChrome() and copied Chrome headers still produce 403/429s, bot shells, empty app responses, or behavior that differs from local Chrome, try LoggedJurl.withBrowserProfile(...) to reproduce Chrome's TLS/HTTP2 fingerprint.
Prefer ChromeShim.getMostRecentChromeRelease().getBrowserProfile() on production-like boxes, especially with proxy types that explicitly require a browser profile. If local script/dev data has no BrowserProfileConfiguration, do not prematurely mark the route impossible: for feasibility probes, use a documented long-lived captured/check-in Chrome TLS profile as a fallback and note that production should use the latest DB-backed profile when present.
Keep request shape coherent. API/XHR routes should use browser profile plus explicit CORS/API headers when .asChrome() v1 adds document-navigation headers that conflict with the copied request. Avoid sending duplicate accept, referer, or sec-fetch-* values through proxy providers; split browser-profile and provider-header experiments if needed.
Concurrent Repo Work
Before editing a repo, explicitly choose the worktree. Do this before the first apply_patch, IDE edit, formatter, generated-code command, or test command that might write files.
Run:
pwd
git status --short --branch
git worktree list --porcelain
Use a sibling worktree on a codex/ branch for the task unless the current checkout is already a task-owned worktree for this exact thread. A task-owned worktree means its path and branch clearly match the current task/retailer/PR and it was created for this Codex task. The primary checkout, such as $HOME/api.pearcommerce.com, $HOME/admin.pearcommerce.com, or $HOME/offers.pearcommerce.com, is a shared/user checkout by default even when it is clean, even when it is already on a codex/ branch, and even when the same PR branch is being updated. Do not treat a clean primary checkout as "dedicated" unless the user explicitly tells you to edit that checkout.
If git status --short --branch shows staged, unstaged, untracked, generated, or unknown files, do not edit there. Treat every existing change as someone else's active work and create/use a sibling worktree, even for docs or tiny changes. Do not stash, reset, rebase, clean, or otherwise rearrange the user's checkout to make room.
git fetch origin master --prune
git worktree add -b codex/<short-task-name> ../<repo-name>-<short-task-name> origin/master
Edit, test, commit, push, and open the PR from that worktree. Use unique task names; remove only worktrees you no longer need.
For an existing PR update, first resolve the PR head branch and use a worktree for that branch instead of editing the primary checkout:
BRANCH="$(gh pr view PR_NUMBER --json headRefName --jq .headRefName)"
git fetch origin "$BRANCH" --prune
git worktree add --detach ../<repo-name>-<short-task-name> "origin/$BRANCH"
Make the PR update in that detached worktree, then push back to the PR branch with:
git push origin HEAD:"$BRANCH"
If the branch is not checked out anywhere else and a normal branch worktree is more convenient, git worktree add ../<repo-name>-<short-task-name> "$BRANCH" is also fine. If the PR branch is user-authored, shared, or unsafe to update from a detached worktree, stop and report the risk instead of editing the primary checkout.
When updating an existing PR branch with latest master, main, or another PR base, rebase the branch onto the base tip and force-push with lease after verification. Do not use git merge origin/master, git merge origin/main, or any update-branch flow that creates a merge commit. If the branch is shared or unsafe to rewrite, stop and ask/report instead of making a merge commit. For stacked PRs, rebase and push the parent first, then rebase each child onto the updated parent.
Real Data
When data would clarify behavior, edge cases, IDs, ownership, or UI state, query devops/db.sh instead of guessing. Prefer the safest relevant target, usually devops/db.sh --dev; use production only when requested or clearly required. The helper needs the split-tunnel AWS Client VPN active — if it is not running, start devops/vpn.sh (second terminal, or backgrounded) and leave it running; never use --start-vpn. It then resolves the current private database target through VPC DNS and connects with TLS. Targets: --prod (database.pearcommerce.com), --dev/--analytics (dev-database.pearcommerce.com), --read (Aurora prod reader endpoint), --maria (pear-mariadb-6). For Snowflake data questions, use the snow CLI or the JDBC paths described in the $snowflake-jdbc skill. Default to read-only queries and summarize facts instead of dumping broad output.
Pass non-interactive SQL as one quoted shell argument, for example devops/db.sh --dev "SELECT * FROM RetailPartner WHERE name LIKE '%SHOPRITE%'". There is no SSM/eval database hop now, so ordinary SQL string literals are preserved; MySQL hex literals remain optional, not required for shell-escaping workarounds.
Kill orphaned long-running reads. A db.sh session, JSP read, local test, or bootRun query that is Ctrl-C'd, disconnected, or abandoned client-side can leave the server-side read transaction running. Long-running InnoDB read transactions hold undo history, and on shared dev/analytics databases that lets undo length (history list length) spiral out of control for everyone. When discovery or implementation work runs queries that may go long, verify your sessions actually terminated; before finishing, check SHOW FULL PROCESSLIST (or information_schema.PROCESSLIST) for long-running reads you started and KILL those query IDs rather than leaving them orphaned.
For searchable server logs, load and use $pear-log-search. VictoriaLogs/Grafana is the default for historical, cross-environment, field/facet, Jurl, and incident searches; Scalyr has been canceled and is no longer available. For terminal-based agent queries, use $fetch-pear-logs (session-cookie CLI) or the SSM-based scripts/query-victorialogs.sh in $pear-log-search. Use devops/logs.sh -e <env> only when an immediate live tail from one instance is specifically useful. For UPC resolution live tails, devops/logs.sh -e upc-resolution --single avoids threading all instances together.
Live Java Instance Probes
Use pear-prod-jsp when the answer requires code running inside a live Pear Java server rather than a local JVM or SQL query. Good fits include live Resources/Persistence, Spring beans, AppConfig/secrets/IAM, process-local caches, in-memory registries, browser profiles, proxy behavior, service methods, job helpers, or controller-adjacent code that only makes sense with the production classpath and runtime state.
Prefer db.sh for pure data questions, local tests for pure code questions, and real HTTP/browser requests for endpoint routing, filters, auth, serialization, and user-visible behavior. Reach for a JSP when you need to call or inspect live Java methods directly. If a controller is involved, be explicit about what is being validated: use the real endpoint for request/response behavior; use a JSP to get Spring beans or call service/controller methods only when the live app context itself is the important part.
When this applies, load and follow pear-prod-jsp: no-parameter preview with a Run button, no side effects on the preview path, deploy the preview without --single, show the full human run report on run=true, use output=raw only for formal artifacts, avoid secrets/customer dumps in source or output, and capture the run URL plus S3 source key. Treat controller/service/job calls that may write database rows, S3/R2, cache, queues, or downstream systems as writes/triggers and require the approval path from pear-prod-jsp before running them.
Deploy And Sync Commands
When the user says "sync to deploy", run Pear's deploy-branch sync script from a clean, up-to-date checkout of the target repo:
/Users/alexwyler/pear-scripts/sync-deploy-branch.sh
For api.pearcommerce.com, this syncs master into the API release-candidate deploy branch alias, such as deploy-YYYY-MM-DD-HH-MM. For admin.pearcommerce.com, this syncs master into the deploy branch; that deploy-branch push is the admin production deploy trigger.
When the user says "deploy" for API code, use the API repo's GitHub Actions deploy trigger, not a local build artifact. Run it from a clean, up-to-date api.pearcommerce.com checkout. In Codex shells, prefer zsh -lc so nvm exposes node, npm, and npx for devops/env.sh:
zsh -lc './devops/trigger-deploy.sh -c master -e pear-commerce-dashboard'
zsh -lc './devops/trigger-deploy.sh -c master -e pear-commerce-upc-resolution'
zsh -lc './devops/trigger-deploy.sh -c master -e jobs-2026'
For a combined API deploy to dashboard, UPC resolution, and jobs:
zsh -lc './devops/trigger-deploy.sh -c master -e pear-commerce-dashboard,pear-commerce-upc-resolution,jobs-2026'
Short user phrases map naturally: "deploy to upc-resolution" means -e pear-commerce-upc-resolution; "deploy to dashboard" means -e pear-commerce-dashboard; "deploy to jobs" means -e jobs-2026. API environment deploys go through Elastic Beanstalk and often take a long time; after triggering one, do not babysit it to completion by default. Instead, use gh run list --workflow deployment.yml once to capture and report the run URL, target environment, commit SHA, and queued/in-progress/completed status. Only run gh run watch, poll until completion, or investigate deploy logs when the user explicitly asks you to wait, when the deploy fails immediately, or when the task requires synchronous deploy verification. Do not deploy Offers production from local artifacts; keep following the Offers deploy safety rules above.
End-To-End Checks
Consider browser E2E for user-facing admin/offers/API-backed flows, especially UI state, auth, extension behavior, API wiring, server/client errors, or displayed data.
For Chrome/unpacked extension work, treat manifest.json versioning as part of the change. Bump the manifest version whenever extension behavior changes, verify Chrome is loading the path you edited (for example the profile's extension details or Secure Preferences path), reload the extension in that profile, and confirm chrome://extensions shows the new version. If the version does not change after reload, you probably edited a different checkout than the one Chrome has loaded; sync or patch the loaded path explicitly before retesting.
For local dashboard work, inspect IntelliJ run configs before starting services. In api.pearcommerce.com, mirror SpringBootTomcat and always use the shared dev DB unless the user explicitly asks for a disposable local DB. Set PEAR_LOCAL_USER_ID for local API starts so local auth can fall back to a known user when the browser has no valid auth-token-v2 cookie. Ask which local user id to impersonate before choosing a value; for Alex's local Codex sessions on his workstation, default to PEAR_LOCAL_USER_ID=2. If using the repo helper, prefix it:
PEAR_LOCAL_USER_ID=<chosen-user-id> ./devops/boot-run-from-intellij-config.py SpringBootTomcat
Never start :bootRun with bare ./gradlew :bootRun; use the env prefix and verify startup logs include MYSQL_HOST=analytics-database.pearcommerce.com and process env includes the chosen PEAR_LOCAL_USER_ID. Gradle example:
ENV=LOCAL \
LOCAL_IP_ZIPCODE_OVERRIDE=55408 \
PEAR_LOCAL_USER_ID=<chosen-user-id> \
MYSQL_CREDENTIALS_SECRET=prod-db-10-2025 \
MYSQL_HOST=analytics-database.pearcommerce.com \
MYSQL_HOST_READ=analytics-database.pearcommerce.com \
MYSQL_HOST_WRITE=analytics-database.pearcommerce.com \
SECRETS_MANAGER_AUTH0_CLIENT_SECRET=AUTH0_CLIENT_SECRET \
SECRETS_MANAGER_AUTH0_MANAGEMENT_SECRET=AUTH0_MANAGEMENT_SECRET \
SNOWFLAKE_CREDENTIALS_SECRET=snowflake-2025-12-01 \
./gradlew :bootRun
Do not let :bootRun fall into empty/local MySQL by accident. If login, vendor pages, UPC imports, or other seeded data look empty/missing, first check whether the API was started without the dev-DB prefix. For admin.pearcommerce.com, use the existing npm/gulp/browser-sync workflow, and when starting or debugging local admin, verify the paired local API on 8080 was started with the intended PEAR_LOCAL_USER_ID as well as the shared dev-DB env. The admin process does not consume PEAR_LOCAL_USER_ID directly; local admin login depends on the API auth fallback. If delegating startup/browser work, pass these expectations to the subagent.
Before browser checks, reuse already-running API/admin processes when available; otherwise start from repo patterns, track sessions, and stop only processes you started. When feasible, verify:
- load the local admin page with the user’s authenticated Chrome profile when extension/auth state matters
- exercise the primary action, not just page load
- inspect visible UI state and browser console errors
- inspect server logs for exceptions or malformed requests
- repeat after small fixes until the specific flow is clean, or clearly state what was not re-tested
If the user cancels or defers E2E, continue with focused static/unit checks.
AngularJS DI Parameter Validation
Pear's AngularJS apps (the offers.pearcommerce.com picker, the /product-locator/ AngularJS app, admin UIs, and any other app using ui-router resolves or Angular factory/controller/service/directive definitions) receive dependencies via implicit dependency injection — parameter names are matched to Angular services by string at runtime. Unlike TypeScript or Java, there is no compile-time check that every referenced service is actually declared in the function's parameter list.
This caused a production-wide PDP outage (PR #1395 / commit ed1658e8): a $rootScope reference was added inside a resolve's .then() callback without adding $rootScope to the resolve's DI parameter list. The resulting ReferenceError: $rootScope is not defined rejected the resolve on every page view, aborted the ui-router transition, and left #pear-app-content empty — blanking every picker-served PDP and landing page. Neither Copilot nor human reviewers caught it because the diff looked syntactically correct.
Review-Time Guard
When reviewing or creating a PR that touches any file containing ui-router resolves (resolve: { ... }) or Angular factory/controller/service/directive definitions — in any Pear AngularJS app, not just the offers picker — apply these checks. Common hot spots include static/js/offers/picker.js, static/js/offers/offers_app.js, static/js/offers/productLocatorController.js, and static/js/offers/landingPageController.js in offers.pearcommerce.com, but the same failure mode exists anywhere implicit DI is used.
-
For every function inside a
resolve:block, check that each Angular service referenced in the function body ($rootScope,$scope,$api,$stateParams,$timeout,$q,$state,$injector,$window,$document,$location,$config,$async, etc.) appears in that function's parameter list. Resolves do not inherit the controller's injected services — each resolve function is independently injected. -
For
.then()or.catch()callbacks inside resolves, check that any Angular service referenced inside the callback is also in the outer resolve function's parameter list. Closures capture the parameters of the resolve function, not the controller's injections. -
For shared resolve objects (e.g.
pickerResolves,pickerResolvesForModification,demoResolves) that are spread viaObject.assign({}, pickerResolves, { ... })into multiple states, a DI bug in the shared object affects every state that inherits it. A single missing parameter can blank dozens of routes simultaneously. -
Run
rg 'function \$[a-zA-Z]+' --type js(scoped to the changed app's JS tree, e.g.static/js/offers/) to surface all DI-parameter-shaped names and cross-reference them against the functions that reference those services in their bodies but don't declare them as parameters.
E2E Guard
offers.pearcommerce.com has Playwright E2E tests in test/locator-e2e/. The test angularjs-pdp-boot.spec.cjs boots the real AngularJS app through the /agnostic-pdp/{offerId} and /agnostic-pdp-label/{offerId}/{label} routes, mocks backend APIs, and asserts that #pear-app-content renders visible content with no ReferenceError in the console. This test would have caught the ed1658e8 bug. When changing resolve logic in offers, confirm this test still passes:
npx playwright test -c playwright.locator.config.cjs test/locator-e2e/angularjs-pdp-boot.spec.cjs
If adding a new ui-router state with a resolve that references Angular services, add a corresponding E2E test that boots that route and asserts the widget renders.