Skip to content

feat(observability): PostHog + Langfuse + Sentry integration#7

Draft
twn-lloyd wants to merge 32 commits into
developfrom
feat/observability-posthog-langfuse-sentry
Draft

feat(observability): PostHog + Langfuse + Sentry integration#7
twn-lloyd wants to merge 32 commits into
developfrom
feat/observability-posthog-langfuse-sentry

Conversation

@twn-lloyd

Copy link
Copy Markdown

Summary

  • Adds @open-mercato/observability package registering PostHog, Langfuse, and Sentry as Integration Marketplace providers.
  • Cloud and self-hosted parity via credential-level host/dsn fields — no code changes required to point at a self-hosted deployment.
  • Server event forwarding (wildcard subscriber), admin + portal browser instrumentation, AI assistant LLM tracing via an additive llmTracer DI token with a noop default.
  • Env-preset bootstrap (OM_INTEGRATION_*), per-tenant PII scrubbing (keys + 8KB truncation), per-tenant config cache with event-driven invalidation.
  • Additive — no DB schema changes, no breaking contract impact. The ai-assistant package remains functional when observability is not installed (covered by dedicated test).

Spec: .ai/specs/2026-04-18-observability-integration-posthog-langfuse-sentry.md
Plan: .ai/plans/2026-04-18-observability-integration-posthog-langfuse-sentry.md

Test plan

  • Unit tests: 38 observability + 148 ai-assistant — all pass
  • Typecheck clean for @open-mercato/observability
  • Integration tests (yarn test:integration — lifecycle + PostHog wiring specs added, need CI run against an ephemeral env)
  • Manual: marketplace listing shows PostHog/Langfuse/Sentry tiles, credentials save/load, health checks return status
  • Manual: /api/observability/client-config returns posthog/sentry enabled shape, omits Langfuse secret
  • Manual: AI assistant routing still works without observability enabled (noop tracer path)

🤖 Generated with Claude Code

twn-lloyd and others added 30 commits April 18, 2026 17:52
New workspace package @open-mercato/observability registering three
integration-marketplace providers: PostHog (product analytics), Langfuse
(LLM tracing), and Sentry (errors/perf). Covers server + admin browser +
customer portal. Both self-hosted and cloud via credential-level host/DSN
config. Additive - no DB changes, no breaking contract impact.
…try integration

Covers 29 TDD tasks across 8 phases: package scaffolding, shared utilities,
provider clients, subscribers, DI wiring, API routes, browser shell
integration, and integration tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add core observability module files with PostHog, Langfuse, and Sentry
integration definitions, ACL features, and credential validation schemas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…elds, use url field type

- Sentry tracesSampleRate: zod now z.coerce.number() so form string input works
- Remove allowlist/denylist/redactionKeys from credential schemas — not exposed
  in UI fields; will land as separate settings when needed
- host fields use CredentialFieldType 'url' instead of 'text'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PII scrubber for event payloads. Redacts sensitive keys case-insensitively
(password, secret, token, apiKey, authorization, etc.), supports opt-in
extra keys, truncates strings over 8KB.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e enabled

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reads OM_INTEGRATION_{POSTHOG,LANGFUSE,SENTRY}_* env vars into a
presettable credentials shape for setup.ts/CLI consumption. Requires
full credential pairs for Langfuse; applies default hosts for PostHog
and Langfuse; defaults Sentry environment to NODE_ENV and tracesSampleRate
to 0.1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…et hook

Adds ModuleSetupConfig with:
- defaultRoleFeatures for superadmin/admin covering view/manage/credentials.manage
- onTenantCreated hook that reads OM_INTEGRATION_* env vars and calls
  applyPreset() to seed integration credentials and enable state for any
  provider with fully populated env config

Extends preset.ts with applyPreset() that writes credentials via
CredentialsService.save and flips isEnabled via IntegrationStateService.upsert
for each configured provider.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- configure-from-env: reruns env preset seeding for a tenant/org
- test-capture: emits a synthetic PostHog event to verify forwarding
- help: prints usage and supported env vars

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds module title/description, feature labels, and provider copy for
English and Polish. Matches gateway-stripe i18n layout (JSON with TS
re-export).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds package README with admin UI + env var configuration, self-hosted
guidance, allowlist/denylist defaults, Sentry multi-tenant caveat, and
CLI usage. Appends release note entry for the new observability package.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Exercises the observability providers end-to-end against the running
mercato app:
- confirms PostHog/Langfuse/Sentry appear in /api/integrations listing
- verifies /api/observability/client-config shape + Langfuse secret
  never leaks to the browser-safe payload
- pings each provider's health endpoint

Skips gracefully when the integrations listing is unavailable so the
test is portable across ephemeral environments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Validates the Integration Marketplace surface the event-forwarding
subscriber depends on: saving PostHog credentials, flipping isEnabled,
and observing the resulting /api/observability/client-config payload.
Skips when the PostHog provider is not registered or the state endpoint
is unavailable in the current environment.

The actual wildcard subscriber flow is covered by unit tests for the
event mapper, tenant config, and forwarder — this test complements them
by proving the admin-side contract matches runtime consumption.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the noopTracer contract the ai-assistant falls back to when the
observability package is not installed. Three cases: return value
passthrough, recordGeneration is a no-op, errors propagate unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
posthog-js v1.x does not accept session_recording.enabled on init;
use the documented disable_session_recording flag instead. Inverts
the cfg.posthog.sessionRecording flag to produce the correct value.

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

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b8fab44-4dda-463e-82f2-eeed05d6688f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/observability-posthog-langfuse-sentry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

twn-lloyd and others added 2 commits April 18, 2026 20:32
- events.ts now uses createModuleEvents so entry.config.events is iterable; the empty {} shape crashed /_not-found data collection with "Cannot read properties of undefined (reading 'id')" during Next.js build
- pin rollup to 4.40.0 via yarn resolution to close GHSA-mw96-cpmx-2vgc pulled in transitively through @sentry/nextjs@8.55.1 → rollup@3.29.5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4.40.0 still falls within the >=4.0.0 <4.59.0 vulnerable range. 4.60.1 is the first release outside both vulnerable ranges.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
…ocale (carry-forward of open-mercato#1730) (open-mercato#1781)

* feat(auth,ui): sidebar customization page with variants, DnD, cross-locale

Adds /backend/sidebar-customization with multi-variant CRUD, role apply,
DnD reorder of items inside groups (persisted itemOrder), cascade hide
for parent items, and a friendly add-new dialog. Variants and preferences
are now scoped per (user, tenant) — locale dropped from unique constraints
with a dedupe migration. Search input migrated to DS Input primitive,
section sidebar styling synced with main sidebar (active marker visible
again after removing inner overflow on the section scroll container).
Init effect drops the cancelled flag so React Strict Mode in dev cannot
abort the first init pass and leave the editor stuck on the loading
skeleton.

Adds 5 integration tests TC-AUTH-034..038 covering variant CRUD,
duplicate-name 409 with friendly error, soft-delete + recreate (partial
unique index regression guard), itemOrder round-trip, and add-new
dialog UI flow.

* refactor(ui): sticky sidebar with scroll affordance + settings search + DS layout polish

- Aside is now sticky (lg:sticky lg:top-0 lg:h-svh) with a hidden scrollbar
  (.scrollbar-hide utility) and a gradient-fade chevron at the bottom that
  flips 180 degrees when the user reaches the end of the scroll.
- Settings sidebar gets the DS Input search (mirrors main sidebar) with
  query-based item filtering; the redundant back-to-main link is removed.
- SectionPage moves padding from <aside> to the inner scroll container so
  the absolute active marker stays inside the padding-box (CSS clip happens
  at the padding-box edge, not outside it).
- SidebarCustomizationEditor wraps its content in <Page>/<PageBody> for
  consistency with other backend pages, replaces the role status text with
  DS Tags (info / error + AlertTriangle for "will clear preset"), and
  simplifies the init effect so React Strict Mode in dev no longer aborts
  the first init pass and leaves the editor stuck on the loading skeleton.

* chore(i18n): sync de/es translations for sidebar customization keys

Sidebar customization editor added new appShell.sidebarCustomization*
keys to en.json without backfilling de/es/pl. CI test job runs
i18n-check-sync which fails on missing keys. `yarn tsx
scripts/i18n-check-sync.ts --fix` backfills de/es with EN values as
placeholders (translation TODO), sorts en/pl. No runtime change.

* test(qa): scope getByRole textbox Search to exact match

Sidebar customization adds a settings search input with
aria-label='Search navigation'. Pre-existing tests used
getByRole('textbox', { name: 'Search' }) which matches by substring,
hitting both the sidebar input and the DataTable search input,
producing 'strict mode violation: 2 elements'.

Switch to { name: 'Search', exact: true } in the shared authUi helper
plus 8 spec files (TC-AUTH-010/011/012/013, TC-CAT-012,
TC-ADMIN-001/002/007, TC-INT-004) so the selector stays scoped to the
page-level search input that has aria-label exactly 'Search'.

* test(qa): extend TC-MSG-009 safeFill timeouts for CI shard load

The local safeFill helper used Playwright's default 5s expect timeout
and called keyboard.type immediately after click+focus+clear. Under
CI shard 9 parallel load the inline composer sometimes needs longer
than 5s to finish its mount/state-sync before the textarea is fully
ready; the typed characters then race the React commit and the value
assertion times out with Received "".

Defense in depth: assert toBeVisible + toBeEnabled before interacting
(10s each), extend toHaveValue to 60s, and bump the test-level timeout
to 180s so multiple safeFill chains plus waitForResponse fit. Same
pattern that stabilised TC-CRM-002 in open-mercato#1739.

Local: TC-MSG-009 passes in 7.9s (well under the 180s cap) with the
new timeouts.

* test(qa): use locator.fill + pre-submit reassert in TC-MSG-009

CI shard 9 trace (run 25095248488 retry #1):
- safeFill toHaveValue 60s timeout, 59 polls = textarea empty under
  parallel-shard load.
- Initial run safeFill passed but waitForResponse 180s timeout, page
  snapshot showed the inline reply textarea empty at 180s — controlled
  state had been silently dropped between safeFill (typed body) and the
  submit button click.

keyboard.type races the React state commit when MessageComposer mounts
inside MessageDetailPageClient and runs its own effects in parallel —
characters land but are immediately overwritten before the caller can
proceed. locator.fill is atomic (`element.value = …` + dispatched
input event) and removes the per-keystroke race entirely.

Two fixes:
1. safeFill switched from click + focus + Ctrl+A + Delete + keyboard.type
   to locator.fill, with toHaveValue staying as the commit gate.
2. Pre-submit re-assertion in the inline reply scenario — fail loudly
   on a state drop instead of silently sending an empty-body POST that
   the response filter rejects.

Local: TC-MSG-009 passes in 6.3s.

* fix(auth,ui): address sidebar customization review findings

Resolves the P1/P2 findings from the haxiorz auto-review on PR open-mercato#1730.

- Switch all read paths in sidebarPreferencesService and the preferences /
  variants API routes from raw em.find/em.findOne to findWithDecryption /
  findOneWithDecryption with explicit { tenantId, organizationId } scope so
  tenant data encryption helpers run consistently and the project's
  encryption-aware ORM rule holds (P1 #2).
- Wrap every write in SidebarCustomizationEditor (variant POST/PUT,
  delete, toggleActive, preferences PUT) in useGuardedMutation.runMutation
  with a stable contextId so global mutation injections (record locks,
  conflict UI) run, and surface retryLastMutation in the injection
  context. The PUT preferences sync is now error-checked: a sync failure
  flashes the save error instead of silently flashing success while the
  AppShell sidebar reads the unsynced preference (P1 #3).
- Drop the locale predicate from both nativeDelete sites in the sidebar
  preferences route (PUT clearRoleIds path + DELETE handler). Save and
  load helpers are cross-locale (unique key (role, tenantId)); filtering
  delete by locale orphaned rows created under another locale (P1 #4).
- Add the three missing AppShell search keys (searchNavPlaceholder,
  searchNavAria, searchNavClear) to en/pl/de/es, and remove seven dead
  appShell.sidebar* keys that were never referenced from any source
  file (P2 #5).

* test(qa): extend TC-CRM-007 + TC-INT-002 timeouts for CI shard 6 load

Both deal-creation specs were timing out on CI shard 6/15 with three
deterministic failures across reruns:
  - TC-CRM-007: timedOut at selectByFieldId clicking a still-disabled
    Status combobox (DictionaryEntrySelect.loading > 20s under shard load)
  - TC-INT-002: failed at toHaveURL('/customers/deals$') because Title
    was empty + "This field is required" — a late dictionary load
    re-triggered CrudForm's initialValues merge and clobbered the typed
    value before submit, so validation rejected the request

Both tests pass in ~3-8s locally in the ephemeral Docker environment
with the same code, so the regression is purely CI shard 6 parallel
load competing with 49 other specs for resources. Match the proven
TC-MSG-009 fix pattern (commit ac37d01):
  - test.setTimeout(120_000 / 180_000) per test
  - expect(combobox).toBeEnabled({ timeout: 30_000 }) before every
    selectByFieldId click — gates on dictionary load completion
  - expect(titleInput).toHaveValue(...) immediately after fill —
    atomic confirmation the controlled state has committed
  - defensive title re-fill right before submit so a late initialValues
    merge that clobbers the value still produces a valid POST
  - expect(option).toBeVisible() before option click — gates on
    Radix portal mount

Test-only change; no application code touched. Verified locally with
yarn test:integration:ephemeral on both specs — 2 passed (22.9s).

* fix(auth,ui): address Patryk review findings on sidebar customization

Resolves the High and Medium findings from the @patrykk-com review on PR open-mercato#1730.

High:
- Migration-snapshot drift on sidebar_variants: the snapshot still listed the
  legacy `sidebar_variants_user_id_tenant_id_locale_name_unique` constraint
  even though Migration20260427124900 + 20260427143311 dropped it and replaced
  it with a partial unique index `WHERE deleted_at IS NULL` (which a `@Unique`
  decorator cannot represent). Drop the @unique decorator on `SidebarVariant`
  and remove the stale snapshot entry; partial index is owned by raw SQL in
  the migration. A follow-up `yarn db:generate` now diffs cleanly. (H #1)
- Move inline zod schemas (sidebarSettingsSchema, createVariantInputSchema,
  updateVariantInputSchema, variantRecordSchema) from variants route handlers
  into `data/validators.ts` and import them in both routes. Settings shape is
  shared with `sidebarPreferencesInputSchema` so the constraint definitions
  no longer drift. (H #2)

Medium:
- Replace `as any` / `: any` across the new sidebar code with `EntityManager`
  + typed `FilterQuery`. `parsed.data.settings as any` casts are gone now
  that service signatures accept `Partial<SidebarPreferencesSettings>` (which
  matches the inferred zod type). (M #3)
- Add explicit one-line rationale on every empty-catch block in AppShell
  (localStorage / cookie blocked in private mode — non-critical) and
  SidebarCustomizationEditor (`window.dispatchEvent` with no listener —
  AppShell refreshes on next navigation). (M #4)
- Replace raw `<button>` drag handle in SortableItemRow with `<IconButton
  variant="ghost" size="sm">` and use the existing forwardRef so
  `setActivatorNodeRef` and dnd-kit listeners still wire correctly. (M #5)
- i18n hardcoded strings in SidebarPreview (`Search...`, `No groups to
  preview.`, `Drag to reorder`) — wrapped in `t(...)` and added 3 new keys
  to en/pl/de/es. (M #6)
- Switch primitive: replace inline `shadow-[0_1px_2px_rgba(10,13,20,...)`
  arbitrary-value shadow with the new `--shadow-switch-thumb` CSS custom
  property in light + dark themes (and synced into the standalone template
  globals.css). Switch now uses `shadow-switch-thumb` Tailwind utility. (M #7)
- Behavior regression for non-admin users: `requireFeatures:
  ['auth.sidebar.manage']` on the sidebar-customization page meta locked
  every non-admin user out of personal-scope customization, even though the
  variants/preferences APIs only gate role-application via that feature.
  Drop the page-level requireFeatures so any authenticated user can reach
  the page; the editor already conditionally hides "Apply to roles" via
  `canApplyToRoles` (server-checked against `auth.sidebar.manage`). (M open-mercato#8)

New tests:
- 6 unit tests in `sidebarPreferencesService.scope.test.ts` lock down the
  cross-tenant + cross-user scope guards on `loadSidebarVariant`,
  `updateSidebarVariant`, `deleteSidebarVariant`. Each test stubs
  `findOneWithDecryption` and asserts the exact `{ id, user, tenantId,
  deletedAt: null }` filter shape so a future refactor can't silently
  drop the user or tenant filter. (M open-mercato#9)

All 405 core test suites (3,329 tests) and 71 UI test suites (363 tests)
pass; build:packages clean across 18 packages.

* fix(auth): align sidebar preferences snapshot with partial unique indexes

UserSidebarPreference and RoleSidebarPreference still carried @unique
decorators that included locale, so the MikroORM snapshot kept the old
locale-scoped unique constraints even though Migration20260427143311
replaced them with partial unique indexes scoped to live rows. The next
yarn db:generate would have emitted a fixup migration trying to drop a
constraint already gone and add one colliding with the partial index.

Mirror the SidebarVariant approach: drop the @unique decorators
(partial indexes can't be expressed via the decorator), document the
ownership in raw SQL, and remove the stale unique entries from the
snapshot so it reflects the post-143311 state. yarn db:generate now
reports auth: no changes.

* test(auth,ui): add SidebarCustomizationEditor unit smoke test

The spec at .ai/specs/2026-04-27-ds-sidebar-customization-page.md
required SidebarCustomizationEditor.test.tsx covering load/save/cancel
flows, error states, role-apply target rendering, and drag-handle DOM
presence. Service-layer scope guards and Playwright integration tests
already shipped, but the editor's React state transitions had no unit
coverage. Adds a 5-test smoke suite that mocks apiCall/flash/injection
and asserts: skeleton-before-data, drag handles after load, load-error
surfacing on 500, role-apply targets when canApplyToRoles=true, and
that the role list is hidden when canApplyToRoles=false.

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

---------

Co-authored-by: zielivia <zielivia@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
… with better agent -> human communication (open-mercato#1593)

* docs(runs): add execution plan for ai-framework-unification

Seed .ai/runs/2026-04-18-ai-framework-unification/ with PLAN.md, HANDOFF.md,
and NOTIFY.md so the branch is resumable via auto-continue-pr from the first
commit on. Phase 1 covers the auto-create-pr/auto-continue-pr skill rework
that follows in the next commit; Phase 2+ (ai-framework unification proper)
is a placeholder to be expanded once the user provides direction.

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

* docs(skills): rework auto-create-pr/auto-continue-pr around per-spec run folders

Migrate auto-create-pr and auto-continue-pr from a flat .ai/runs/<date>-<slug>.md
plan file to a per-spec run folder (.ai/runs/<date>-<slug>/) containing PLAN.md,
HANDOFF.md, NOTIFY.md, and per-step proofs/ subfolders. Tighten the planning
discipline so every Step is 1:1 with a commit, require per-commit verification
proofs (typecheck + unit tests always; Playwright + screenshot when UI-facing
and the dev env is runnable, never as a dev-blocker), mandate a live HANDOFF.md
rewritten after each Step, and introduce an append-only UTC-timestamped
NOTIFY.md. Cap optional subagent parallelism at two (e.g. dev + reviewer) with
conflict avoidance as the first priority.

Migrate auto-sec-report and auto-qa-scenarios to the same folder layout and
add HANDOFF/NOTIFY paths alongside PLAN.md. auto-update-changelog keeps its
existing .ai/runs/ prefix filter (works for both layouts) with a clarifying
comment. Refresh .ai/runs/README.md to document the new contract end to end.

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

* docs(runs): mark ai-framework-unification step 1.1 complete

Flip Phase 1 Step 1.1 in PLAN.md to [x] with commit sha bacbc59ec, rewrite
HANDOFF.md with the post-Phase-1 state, and append NOTIFY.md entries for the
run-folder commit, the Step 1.1 commit, and the Phase 1 completion. Phase 2
remains a placeholder until the user provides ai-framework unification scope.

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

* docs(runs): fix placeholder UTC timestamps in ai-framework-unification log

Replace the synthetic T00:xx:xxZ placeholders in NOTIFY.md with realistic
timestamps derived from the actual session timeline, update HANDOFF.md's
Last updated to the current time, add Step 1.2 (this commit) and Step 1.3
(in-progress label discipline) under Phase 1 in PLAN.md, and record the
append-only-rule repair in NOTIFY.md so the correction is auditable.

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

* docs(skills): require auto-create-pr to hold the three-signal in-progress lock

Per root AGENTS.md, auto-skills that mutate PRs or issues MUST claim with all
three signals (assignee, in-progress label, claim comment) and MUST release
on completion or failure. auto-create-pr previously opened the PR and mutated
it in steps 10-12 without a formal lock, relying on auto-review-pr to claim
it during the peer-review sub-run.

Tighten the discipline:

- Add a new step 9b that claims the PR with all three signals immediately
  after gh pr create returns.
- Step 11 now temporarily releases the in-progress label before invoking
  auto-review-pr so the sub-skill can claim cleanly, then reclaims after
  auto-review-pr returns to cover the summary-comment + cleanup window.
- Step 13 releases the in-progress label inside the same trap/finally that
  cleans up worktrees so a crash still frees the PR.
- Rules section gains a dedicated bullet describing the claim/release
  lifecycle so future readers see the contract at a glance.

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

* docs(runs): mark ai-framework-unification steps 1.2 and 1.3 complete

Flip Phase 1 Steps 1.2 (timestamp fix, 4a782bbd1) and 1.3 (in-progress lock
discipline, 98ec6abb2) to [x] in PLAN.md, rewrite HANDOFF.md for the Phase 1
exit state, and append NOTIFY entries covering the timestamp repair, the
in-progress-label user directive, the PR #1593 dogfood claim, and the
Phase 1 completion.

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

* docs(skills): flatten run-folder verification layout to step-<X.Y>-checks.md + optional artifacts

Replace the nested .ai/runs/<date>-<slug>/proofs/<step-id>/ layout with a flat
layout that keeps verification files next to PLAN.md:

- step-<X.Y>-checks.md — required per Step; records typecheck / unit tests /
  i18n / Playwright / diff-re-read outcomes (or explicit N/A with reason).
- step-<X.Y>-artifacts/ — optional per Step; created only when the Step
  actually produced artifacts worth keeping (Playwright transcripts,
  screenshots, captured command output). Never empty.
- final-gate-checks.md + optional final-gate-artifacts/ replace the former
  proofs/_final-gate/ location.
- Review-fix follow-ups use step-<X.Y-review-fix>-checks.md + optional
  step-<X.Y-review-fix>-artifacts/.

Updated contracts: .ai/runs/README.md, auto-create-pr SKILL, auto-continue-pr
SKILL, and auto-sec-report SKILL (auto-qa-scenarios inherits by reference and
needed no edit). Migrated the three existing proofs/<id>/notes.md files in
this run folder to step-<X.Y>-checks.md and removed the proofs/ directory.

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

* docs(runs): mark ai-framework-unification step 1.4 complete

Flip Phase 1 Step 1.4 (verification layout flatten, 6a1afab69) to [x] in
PLAN.md, backfill the commit SHA into step-1.4-checks.md, rewrite HANDOFF.md
for the post-Phase-1 state, and append NOTIFY entries covering the user
directive, the dogfood in-progress reclaim on PR #1593, and the Step 1.4
commit.

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

* docs(skills): make PLAN.md's top-of-file Tasks table the authoritative status source

Replace the bottom-of-file ## Progress checkbox section with a ## Tasks
markdown table at the top of PLAN.md. The table has a fixed column set
(Phase | Step | Title | Status | Commit) and uses only two Status values
(todo, done) so it is trivially parseable and human-scannable at a glance.

- .ai/runs/README.md now documents the Tasks table as the required top-of-
  file element, including the parseable row shape and the todo/done rule.
- auto-create-pr SKILL: frontmatter description, step 3 (draft the plan)
  template, step 6 (post-commit tracking update), step 9 (PR body anchor),
  step 11 (review-fix follow-ups), completion gate, and Rules section all
  reference the Tasks table and stop demanding the legacy ## Progress
  checklist.
- auto-continue-pr SKILL: frontmatter description, step 3 (orient +
  parse), step 4 (post-commit update), step 7 (review-fix follow-ups),
  step 9 (completion gate), and Rules section now treat the Tasks table as
  the authoritative source. A legacy ## Progress fallback is retained so
  PRs opened before the migration still resume; the skill migrates the
  fallback to a Tasks table on the first resume commit.
- This run's PLAN.md now opens with the Tasks table (rows 1.1-1.5 + 2.1)
  and the old ## Progress section is removed.

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

* docs(runs): mark ai-framework-unification step 1.5 complete

Flip Phase 1 Step 1.5 (Tasks table migration, 93440ec79) to done in
PLAN.md's Tasks table, backfill the commit SHA into step-1.5-checks.md,
rewrite HANDOFF.md for the post-Phase-1 state, and append NOTIFY entries
covering the user directive, the dogfood in-progress reclaim, the Step 1.5
commit, and the note about the unrelated working-tree spec edit left
unstaged on purpose.

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

* docs(runs): compact Phase 1 plan to single step and rename PR to main goal

Roll up the five historical Phase 1 Steps (1.1–1.5) into one row in
PLAN.md's Tasks table and one bullet in the Implementation Plan section,
with the five commit SHAs kept inline as a breadcrumb so the audit trail
is still discoverable. Add a Step 1.2 row for this compaction itself.

The per-Step step-1.<N>-checks.md files remain on disk as the Phase 1
verification audit trail — this is a readability change to PLAN.md, not a
history rewrite.

Rename PR #1593 to "feat(ai-framework): AI framework unification — Phase 1
skill harness foundation" so the title names the real goal (skill-harness
docs were only the delivery mechanism of Step 1.1). PR body rewritten to
match the compacted plan.

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

* docs(runs): mark ai-framework-unification step 1.2 complete

Flip the Phase 1 compaction Step 1.2 (61b655eac) to done in PLAN.md's
Tasks table, backfill step-1.2-checks.md with the commit SHA, rewrite
HANDOFF.md to reflect the fully-complete Phase 1 state, and append NOTIFY
entries for the user directive, the dogfood in-progress reclaim, the PR
rename, and the compaction commit.

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

* feat: verification phase defined

* docs(runs): rephase PLAN.md to cover full ai-tooling spec

Rewrites the `ai-framework-unification` execution plan so Phases 2-5 map
one-to-one to the `2026-04-11-unified-ai-tooling-and-subagents` spec's
Phase 0-3 with Workstream A/B/C/D grouping preserved. Tasks table grows
from 3 rows to 46 rows (1 rollup + 1 rephasing + 44 commit-sized Steps
covering the D16 mutation approval gate and the D18 catalog merchandising
demo). Implementation Plan section rewritten to mirror the table so every
Step cites a numbered deliverable in the source spec.

Docs-only. No code, no migrations, no user-facing surface.

Verification: `step-1.2-checks.md` (N/A typecheck/unit/Playwright/i18n;
Tasks-table schema + spec cross-reference + PR metadata confirmed).

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

* docs(runs): mark ai-framework-unification step 1.2 complete

Flips Step 1.2 `Status` to `done` with commit SHA 80b335707, rewrites
HANDOFF.md to name Step 2.1 (AiAgentDefinition type + defineAiTool helper
in packages/ai-assistant) as the next concrete action, and appends NOTIFY
entries for the auto-continue-pr resume, the decision to broaden Step 1.2
scope, and the Step 1.2 commit.

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

* feat(ai-assistant): add AiAgentDefinition type and defineAiTool() helper

Phase 2 / Step 2.1 of the ai-framework-unification plan. Spec §2 §1.

- New AiAgentDefinition type with every optional field from spec §2
  (executionMode, mutationPolicy, resolvePageContext, maxSteps,
  output, keywords, domain, dataCapabilities, acceptedMediaTypes,
  readOnly, uiParts, requiredFeatures, defaultModel).
- Identity defineAiAgent() helper for author-site type inference.
- Identity defineAiTool() helper returning AiToolDefinition (thin
  additive builder over the existing MCP-compatible shape).
- AiToolDefinition extended with optional focused-agent metadata
  (displayName, tags, isMutation, maxCallsPerTurn, supportsAttachments).
  McpToolDefinition unchanged; every existing aiTools: AiToolDefinition[]
  export remains structurally compatible (BC verified by a dedicated
  test).
- Public re-exports from @open-mercato/ai-assistant.
- Unit tests in ai-agent-definition.test.ts (7 cases) cover builder
  identity, BC assignability to AiToolDefinition + McpToolDefinition,
  plain-object BC, and every optional AiAgentDefinition field.

No runtime/loader changes yet — Step 2.2 adds the generator for
ai-agents.ts and Step 2.3 restores ai-tools.generated.ts loading.

* docs(runs): mark ai-framework-unification step 2.1 complete

* docs(runs): finalize handoff for ai-framework-unification step 2.1 resume

* docs(runs): coordinator claim

* feat(cli): add ai-agents.generated.ts generator extension

Scan every module for ai-agents.ts and emit an additive
apps/mercato/.mercato/generated/ai-agents.generated.ts aggregate next to
the existing ai-tools.generated.ts. Output mirrors the ai-tools shape:
aiAgentConfigEntries (filtered) + allAiAgents (flattened). No change to
ai-tools.generated.ts. Covers spec Phase 0 deliverable 2.

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

* docs(runs): mark ai-framework-unification step 2.2 complete

Flip PLAN.md Tasks table Step 2.2 -> done (89cbbe56a). Rewrite
HANDOFF.md to point next action at Step 2.3 (runtime tool-loader
restore). Add step-2.2-checks.md + NOTIFY entries for the landing
and the direct-executor decision.

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

* docs(skills): codify main-session executor-dispatch pattern

Add a "Multi-Step runs: executor-dispatch pattern" subsection to both
auto-continue-pr and auto-create-pr SKILL.md. Captures the constraint
learned by dogfooding on #1593: subagents do not have the Agent tool,
so coordinator-subagents cannot spawn executors. Dispatch must live in
the main session. Includes an executor prompt template, post-executor
verification checklist, and safety stops.

Single-Step resumes unchanged. Pattern is opt-in for multi-Step runs.

* feat(ai-assistant): load module ai-tools.generated.ts in runtime tool-loader

Restores module-contributed AI tools at runtime: the tool-loader now
imports aiToolConfigEntries from ai-tools.generated.ts and registers
each tool through the existing registerMcpTool / mcp-tool-adapter.ts
path. Code Mode tools stay untouched; this is strictly additive.
Spec Phase 0 deliverable (2026-04-11-unified-ai-tooling-and-subagents).

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

* docs(runs): mark ai-framework-unification step 2.3 complete

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

* docs(skills): fork auto-create-pr/auto-continue-pr on simple vs spec-impl

Add an early "Classify the run" section to both skills. Simple runs
(bug fix, CR follow-up, dep bump, typo, small refactor, linter/i18n/
test-only) skip the run-folder ceremony entirely: no PLAN.md, no Tasks
table, no HANDOFF.md, no NOTIFY.md, no step-<X.Y>-checks.md. Just
commit, test, push, summary. Spec-implementation runs (linked spec,
multi-phase, ≥3 commits, new contract surface) keep the full contract.
Classification heuristic + promotion path documented. Multi-Step
executor-dispatch subsection scoped to spec-impl runs only.

* feat(ai-assistant): add attachment-bridge and prompt-composition contract types

Lands spec Phase 0 (§8, §10) implementation-ready type primitives for the
unified AI tooling surface: AiResolvedAttachmentPart + AiUiPart +
AiChatRequestContext under attachment-bridge-types, and PromptSection /
PromptTemplate / definePromptTemplate under prompt-composition-types.
Package index re-exports every new symbol alongside the existing
defineAiAgent / defineAiTool surface — purely additive. Types are not
yet consumed by the runtime; that lands in Phase 3 along with the prompt
composer and attachment fetcher.

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

* docs(runs): mark ai-framework-unification step 2.4 complete

Flips the PLAN.md Tasks table row for Step 2.4 to done (commit b3ea44b0c),
rewrites HANDOFF.md for the next executor, records the Step verification
log under step-2.4-checks.md, and appends the timestamped Step 2.4 entry
to NOTIFY.md.

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

* test(ai-assistant): add phase 0 additive-contract regression suite

Phase 2 closeout (spec Phase 0 Alignment Prerequisite). Adds a cross-cutting
regression + additivity suite covering the four Phase 0 deliverables:
restored module-tool loading, defineAiTool/plain-object compatibility,
additive ai-agents.generated.ts discovery, and generator output stability.
Tests only — no production-code edits.

* docs(runs): mark ai-framework-unification step 2.5 complete

* feat(ai-assistant): add agent-registry loader for ai-agents.generated.ts

Loads the generated `allAiAgents` array behind a typed, cached
read API (`getAgent`, `listAgents`, `listAgentsByModule`). Missing
generated file is non-fatal (empty registry); duplicate agent ids
throw at load time; malformed entries are skipped with a warning.
Registry is read-side only — runtime policy and dispatch land in
Step 3.2 and Step 3.3.

* docs(runs): mark ai-framework-unification step 3.1 complete

Flip PLAN.md Tasks row 3.1 to done (a87bd19f6), rewrite HANDOFF.md to
point to Step 3.2 (policy gate), append NOTIFY.md entry with
verification outcomes and decisions.

* feat(ai-assistant): add runtime policy gate for agent + tool + attachment checks

Introduces checkAgentPolicy() as a pure decision helper covering every deny
branch the spec's Phase 1 runtime needs: agent resolution, requiredFeatures,
allowedTools whitelist, tool-level features, readOnly (default-true in v1),
mutationPolicy, executionMode chat/object consistency, and acceptedMediaTypes
attachment gating. No HTTP, no AI SDK wiring, no attachment fetching — Steps
3.3 / 3.4 / 3.7 will consume this gate.

* docs(runs): mark ai-framework-unification step 3.2 complete

Flip PLAN.md row 3.2 to done with SHA 4f3b8b737, rewrite HANDOFF.md to
point Step 3.3 at the POST /api/ai/chat dispatcher route, and append a
NOTIFY entry covering the runtime policy gate landing plus the
isMutation BC gotcha carried forward from Step 2.5.

* feat(ai-assistant): add POST /api/ai/chat?agent=<id> dispatcher route

Wires auth, zod payload validation, and the Step 3.2 policy gate onto
the new focused-agent chat dispatcher. The streaming body is a
TODO(step-3.4) placeholder that only proves the HTTP contract and error
model; Step 3.4 replaces it with createAiAgentTransport / runAiAgentText.
Attachment media-type resolution stays deferred to Step 3.7.

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

* docs(runs): mark ai-framework-unification step 3.3 complete

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

* feat(ai-assistant): add AI SDK helpers — runAiAgentText, resolveAiAgentTools, createAiAgentTransport

Phase 1 WS-B deliverable. `resolveAiAgentTools` adapts whitelisted tools
through `mcp-tool-adapter.ts` under the same policy gate as the HTTP route;
`runAiAgentText` composes the system prompt (+ opportunistic
`resolvePageContext` hydration), resolves a model via the llm-provider
registry, and streams via AI SDK `streamText`; `createAiAgentTransport`
binds the agent-id query to `DefaultChatTransport`. The dispatcher
`POST /api/ai_assistant/ai/chat?agent=<id>` now delegates to
`runAiAgentText` and maps `AgentPolicyError` to the canonical deny status.
`runAiAgentObject` / `executionMode: 'object'` remain Step 3.5.

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

* docs(runs): mark ai-framework-unification step 3.4 complete

Flip PLAN row 3.4 → done (e20c80c1e), rewrite HANDOFF to hand off to
Step 3.5 (`runAiAgentObject` / `executionMode: 'object'`), append a
NOTIFY entry covering the `authContext` Phase-1 shim, attachment-id
pass-through until Step 3.7, and the opportunistic `resolvePageContext`
hydration that Step 5.2 will later backfill.

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

* feat(ai-assistant): add runAiAgentObject structured-output helper

Phase 1 WS-B (Step 3.5). Adds runAiAgentObject alongside runAiAgentText,
wiring AI SDK generateObject/streamObject through the same policy gate,
tool resolution, system-prompt composition, and model resolution path.
resolveAiAgentTools now accepts requestedExecutionMode so chat-only
agents are rejected at the shared agent-level check. 8 new unit tests.

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

* docs(runs): mark ai-framework-unification step 3.5 complete

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

* test(ai-assistant): add chat/object runtime parity contract tests

Adds agent-runtime-parity.test.ts asserting that runAiAgentText and
runAiAgentObject share the exact same policy gate, tool filtering,
prompt composition, resolvePageContext pathway, attachment pass-through,
and execution-mode gate. Uses describe.each for 11 paired invariants
plus the execution-mode inverse pair. No production changes; both
helpers already observe every parity rule (+1 suite, +26 tests).

* docs(runs): mark ai-framework-unification step 3.6 complete

* docs(runs): finalize handoff for ai-framework-unification WS-B pause

* feat(ai-assistant): add attachment-to-model conversion bridge

Introduces `attachment-parts.ts` — the Phase 1 WS-C bridge that turns
`attachmentIds` into contract-typed `AiResolvedAttachmentPart[]` and
threads them through both `runAiAgentText` and `runAiAgentObject` via a
single shared code path.

Behavior:
- classifies each attachment by media type (`image` / `pdf` / `file`)
- emits `bytes` (provider-native inline) for images/PDFs under the 4 MB
  threshold; `signed-url` when a container-registered
  `attachmentSigner` can mint a short-lived URL; `text` for text-like
  files using the attachments `content` column (OCR/text-extraction);
  `metadata-only` otherwise
- enforces tenant/org scope via `findOneWithDecryption` — cross-tenant
  records are dropped with `console.warn`, super-admin bypass retained
- respects `agent.acceptedMediaTypes` whitelist; undefined means "no
  filter"; excluded types drop with `console.warn`
- gracefully skips with `console.warn` when no DI container is
  available (preserves the Step 3.6 parity invariant #7 pass-through
  behavior; `attachmentIds` still flow into `resolveAiAgentTools`
  untouched)
- materializes bytes/signed-url parts as AI SDK v6 `FileUIPart` on the
  last user `UIMessage.parts`, and text/metadata-only surfaces as an
  `[ATTACHMENTS]` block appended to the system prompt — identical in
  chat and object modes

Unit tests cover all four source kinds, the whitelist filter, the
cross-tenant drop, and the unavailable-service graceful skip.

Refs: spec §77, §421, §983–§1001, §1454
Closes: Step 3.7

* docs(runs): mark ai-framework-unification step 3.7 complete

* feat(ai-assistant): add general-purpose tool packs (search, attachments, meta)

Adds the three Phase 1 WS-C general-purpose AI tool packs consumable by any
agent via `allowedTools`:

- `search.hybrid_search`, `search.get_record_context` (wraps `searchService`).
- `attachments.list_record_attachments`, `attachments.read_attachment`,
  `attachments.transfer_record_attachments` (uses `findWithDecryption` /
  `findOneWithDecryption` with tenant + organization scope; transfer tool
  carries `isMutation: true` so the Step 3.2 policy gate blocks read-only
  agents).
- `meta.list_agents`, `meta.describe_agent` (RBAC-filtered; empty registry
  is a first-class case — never crashes the runtime; output schemas are
  emitted as JSON-Schema via `z.toJSONSchema` with a `non-serializable`
  fallback marker).

All three packs live at `packages/ai-assistant/src/modules/ai_assistant/
ai-tools/` and are re-exported through a module-root `ai-tools.ts` so the
existing generator discovery (Step 2.3) picks them up without any new
generator plumbing. `yarn generate` emits the `ai_assistant` entry in
`ai-tools.generated.ts` alongside `search` and `inbox_ops`.

Unit tests live under `ai-tools/__tests__/`: 3 new suites, 31 new tests
covering happy path, empty, missing-tenant, RBAC filtering, super-admin
bypass, cross-entity transfer rejection, mutation-flag propagation, and
the `z.toJSONSchema` fallback for non-serializable output schemas.

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

* docs(runs): mark ai-framework-unification step 3.8 complete

Flip PLAN row 3.8 to done (commit 11c5a87b8), rewrite HANDOFF to point
at Step 3.9 (customers tool pack), append NOTIFY entry summarizing the
design decisions (dotted tool names, zero new feature IDs, dynamic
imports for cross-package attachments deps, empty-registry safety on
`meta.*`, JSON-Schema fallback for `meta.describe_agent`, recordId
scan strategy for `search.get_record_context`), and add
step-3.8-checks.md with the 25/316 test delta and typecheck verdict.

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

* feat(customers): add customers ai-tool pack (read-only Phase 1)

Implements Step 3.9 of the ai-framework-unification plan: the Phase 1
WS-C customers tool pack exposing eleven read-only tools
(list_people, get_person, list_companies, get_company, list_deals,
get_deal, list_activities, list_tasks, list_addresses, list_tags,
get_settings) through the module-root `ai-tools.ts` contribution so
the existing generator pipeline (Step 2.3 loader) surfaces them at
runtime.

Every tool is tenant + organization scoped via `findWithDecryption` /
`findOneWithDecryption`; no raw `em.find` / `em.findOne` in production
files. No mutation tools — deferred to Phase 5 under the
pending-action contract. `requiredFeatures` reuse existing ids from
`acl.ts`; the aggregator test enforces this.

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

* docs(runs): mark ai-framework-unification step 3.9 complete

- Flip PLAN.md row 3.9 to done (commit c2f2e21cb).
- Rewrite HANDOFF.md for the next executor (Step 3.10 catalog base pack).
- Append step-3.9 landing note to NOTIFY.md.
- Add step-3.9-checks.md (verification log, mocking strategy,
  BC audit, follow-up candidates).

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

* feat(catalog): add catalog ai-tool pack (read-only Phase 1 base coverage)

Ships the catalog module-root AI tool contribution for Phase 1 WS-C, Step
3.10. Twelve read-only tools across six packs — products, categories,
variants, prices + offers, media + tags, product configuration — all
tenant+organization scoped through the existing encryption helpers.

Tool surface:
  - catalog.list_products / catalog.get_product
  - catalog.list_categories / catalog.get_category
  - catalog.list_variants
  - catalog.list_prices / catalog.list_price_kinds_base / catalog.list_offers
  - catalog.list_product_media / catalog.list_product_tags
  - catalog.list_option_schemas / catalog.list_unit_conversions

The base price-kinds enumerator uses the `_base` suffix to reserve the
`catalog.list_price_kinds` name for the Step 3.11 D18 merchandising tool.
An aggregator test pins that reservation so a future drive-by cannot
collapse the two.

Every tool whitelists existing feature IDs from catalog/acl.ts (no new
IDs invented — verified by aggregator.test.ts). Every query routes
through findWithDecryption / findOneWithDecryption with tenantId +
organizationId in both the where map and the scope tuple, plus a
defense-in-depth post-filter on row.tenantId. Raw em.find/em.findOne
appear nowhere in the new production files.

Mutation tools are deferred to Phase 5 Step 5.14 under the
pending-action contract.

Unit tests (7 suites / 36 tests) cover:
  - tenant isolation and cross-tenant row filtering
  - { found: false } on missing / cross-tenant detail fetches
  - 100-row limit cap enforcement via zod
  - aggregator completeness + RBAC-in-acl audit
  - D18 name-collision reservation
  - includeRelated output shape for get_product
  - filter translation for list_prices / list_offers / list_variants

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

* docs(runs): mark ai-framework-unification step 3.10 complete

Flip PLAN row 3.10 to done (0a5395ff2), rewrite HANDOFF.md pointing to
Step 3.11 (D18 catalog merchandising read tools) as the next action,
and append a NOTIFY entry summarizing the catalog base tool pack
delivery.

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

* docs(runs): add step-3.10-checks.md (Step 3.10 verification log)

Follow-up to 112e74dad — the docs-flip commit for Step 3.10 omitted the
step-3.10-checks.md verification log. Adding it here as a minimal
follow-up so the Step contract artifact is complete. The checks file
captures the test counts (7 suites / 36 tests in catalog ai-tools; full
packages/core 331/2992 with +7/+36 delta; ai-assistant 25/316
preserved), typecheck verdict, yarn generate outcome, BC audit, and
design decisions for the twelve new read-only catalog AI tools.

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

* feat(catalog): add D18 merchandising read tools (search_products, get_product_bundle, list_selected_products, get_product_media, get_attribute_schema, get_category_brief, list_price_kinds)

Spec §7 (D18) — seven canonical catalog merchandising read tools the
`catalog.merchandising_assistant` agent (Step 4.9) will whitelist
verbatim. All tools are read-only (no `isMutation: true`); mutation
tooling lands in Step 5.14 under the pending-action contract.

- `catalog.search_products`: fulltext + filter search. Routes through
  the search service when `q` is non-empty (entityType
  `catalog:catalog_product`, then tenant-scoped hydration); falls back
  to the query-engine path with filters (category / tags / price /
  active) when `q` is empty or the search service is absent. Output
  carries `source: 'search_service' | 'query_engine'` so callers can
  tell which path ran.
- `catalog.get_product_bundle`: aggregate bundle with core fields +
  categories + tags + variants + prices (all rows plus `best` via
  `catalogPricingService.resolvePrice`) + media metadata (no bytes) +
  custom-field values + merged attribute schema. `translations: null`
  is surfaced explicitly because no `translations.ts` exists for
  catalog yet.
- `catalog.list_selected_products`: bulk bundle resolver (1..50 ids,
  deduplicated). Cross-tenant / missing ids drop into `missingIds`
  (with a `console.warn`) instead of surfacing as errors.
- `catalog.get_product_media`: attachment metadata + `attachmentId`
  strings only. Does NOT invoke the Step 3.7 bridge — the runtime
  bridge converts ids into model file parts when the chat/object
  helper dispatches the tool in-context.
- `catalog.get_attribute_schema`: merged module + category + product
  schema via the shared `loadCustomFieldDefinitionIndex` resolver.
- `catalog.get_category_brief`: category snapshot reusing the same
  resolver; `{ found: false }` on miss / cross-tenant.
- `catalog.list_price_kinds`: D18 spec-named enumerator. Coexists with
  Step 3.10's `catalog.list_price_kinds_base`; both route through the
  new shared `listPriceKindsCore` helper in `_shared.ts` so they
  cannot drift.

All queries go through `findWithDecryption` / `findOneWithDecryption`
with tenant + organization scoping (plus a defensive post-filter).
RBAC: every tool whitelists existing IDs from `catalog/acl.ts`
(`catalog.products.view`, `catalog.categories.view`,
`catalog.settings.manage`). No new feature IDs introduced.

Aggregator test expanded to assert 19-tool coverage, the
base/D18 price-kinds coexistence, and spec-name fidelity.

Tests: `packages/core` 332 / 3013 (baseline 331 / 2992). `ai-assistant`
unchanged at 25 / 316. Catalog ai-tools scope 8 suites / 57 tests.

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

* docs(runs): mark ai-framework-unification step 3.11 complete

- PLAN.md: flip row 3.11 to `done` with short SHA 6e0beccb8.
- HANDOFF.md: rewrite Last-commit / What-just-happened / Next-action
  for Step 3.11 (D18 catalog merchandising read tools). Next action
  is Step 3.12 (D18 catalog AI-authoring tools via `runAiAgentObject`).
- NOTIFY.md: append a UTC-timestamped entry at 2026-04-18T23:05:00Z
  recording the commit, files, design decisions, and test deltas.
- step-3.11-checks.md (new): full verification log — files touched,
  seven tools matrix + feature-ID mapping, unit test outcomes (catalog
  ai-tools scope 8 / 57, core 332 / 3013, ai-assistant 25 / 316),
  typecheck verdict (core passes; app carries only pre-existing
  Steps 3.1/3.8 diagnostics), `yarn generate` outcome, notable design
  decisions, BC impact, and follow-up candidates.

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

* feat(catalog): add D18 authoring tools (draft/extract/suggest) as structured-output helpers

Spec §7 Step 3.12. Adds five catalog AI-authoring tools as structured-output
helpers in packages/core/src/modules/catalog/ai-tools/authoring-pack.ts:

- catalog.draft_description_from_attributes
- catalog.extract_attributes_from_description
- catalog.draft_description_from_media
- catalog.suggest_title_variants
- catalog.suggest_price_adjustment

Every tool sets isMutation: false explicitly (per spec §7 line 536 callout for
suggest_price_adjustment; whole pack mirrors the flag for consistency). Tools
NEVER write to the database and NEVER open a fresh model call from inside
the handler. Each returns a { proposal, context, outputSchemaDescriptor }
contract: the handler assembles tenant-scoped context and the JSON-Schema
output descriptor; the surrounding agent turn uses runAiAgentObject
(landed in Step 3.5) to populate proposal via structured output.

Refactor: promotes buildProductBundle, toProductSummary, resolveAttributeSchema,
toPriceNumeric, and bundle types from merchandising-pack.ts to _shared.ts so
both packs consume the same loader. Behavior-preserving. Adds `description`
to the product summary (additive) so authoring tools can seed
extract-attributes-from-description.

RBAC: describe/extract/title tools gate on catalog.products.view; price
adjustment gates on catalog.pricing.manage (existing feature IDs, verified
against catalog/acl.ts). Tenant scoping routes through findWithDecryption
/ findOneWithDecryption; cross-tenant userUploadedAttachmentIds drop with
console.warn in draft_description_from_media.

Tests: packages/core/src/modules/catalog/__tests__/ai-tools/authoring-pack.test.ts
(1 suite / 20 tests) plus an aggregator test update to pin all five D18
authoring tool names.

Totals: catalog ai-tools scope 9 suites / 77 tests (was 8 / 57, +1 / +20
matches the new suite). Full packages/core jest: 333 / 3033 (was 332 / 3013).
packages/ai-assistant jest: 25 / 316 unchanged. Typecheck: @open-mercato/core
clean; @open-mercato/app carries the pre-existing Step 3.1 + 3.8 diagnostics
only; zero new diagnostics on the new files.

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

* docs(runs): mark ai-framework-unification step 3.12 complete

Flip PLAN.md row 3.12 to done (14249bc68), rewrite HANDOFF.md for
Step 3.13 (Phase 1 WS-C integration tests via Playwright TS under
.ai/qa/ — unknown agent / forbidden agent / invalid attachment /
allowed-tool filtering / tool-pack coverage), append the NOTIFY
entry covering the five D18 structured-output authoring tools
(draft_description_from_attributes, extract_attributes_from_description,
draft_description_from_media, suggest_title_variants,
suggest_price_adjustment — all isMutation: false explicitly; handlers
never call the model, they emit { proposal, context, outputSchemaDescriptor }
for runAiAgentObject), and add the verification log at
step-3.12-checks.md.

Regression deltas: packages/core 333/3033 (was 332/3013, +1/+20
matches the new authoring-pack suite), packages/ai-assistant 25/316
unchanged, catalog ai-tools scope 9/77 (was 8/57, +1/+20). Typecheck:
@open-mercato/core clean; @open-mercato/app carries only the
pre-existing Step 3.1 + Step 3.8 diagnostics (zero new).

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

* fix(ai-assistant): resolve ai-tools handler-variance typecheck blocker

The `{search,attachments,meta}-pack` aggregators declared each tool as
`AiToolDefinition` (default `<unknown, unknown>`) while `defineAiTool()`
infers a specific `<TInput, TOutput>` pair from the literal — TS rejected
the downcast because function parameters are contravariant. Flagged in
step-3.8 and carried through step-3.10 as "tolerated carryover"; the
first end-of-workstream checkpoint surfaced it as a real blocker on
\`yarn typecheck\`.

Fix drops the per-tool annotation (let inference keep the specific shape
for author-side checking) and widens the exported arrays + module-root
aggregator to `AiToolDefinition<any, any>[]`, which is the idiomatic
answer for heterogeneous tool arrays. Also narrows the search-pack
\`strategiesUsed\` type predicate to `SearchStrategyId` so TS stops
complaining about the wider \`string\` widening.

Checkpoint artifacts under .ai/runs/2026-04-18-ai-framework-unification/
checkpoint-phase3-wsc-artifacts/ capture the full validation gate
(build:packages, generate, typecheck, unit tests 4194 passing across
all packages, i18n sync + usage, build:app) that this fix unblocks.

* docs(runs): record end-of-Phase-3-WS-C checkpoint (app/build/unit/browser smoke + integration-test seeds)

First coordinator-level checkpoint for this run. Captures:
- Full validation gate outcome (build:packages, generate, typecheck,
  test, i18n sync+usage, build:app) before Step 3.13.
- Browser smoke evidence (3 screenshots) exercising login, catalog
  products list (4 demo SKUs), customers people list (6 demo people).
- Integration-test seed scenarios harvested from the browser session,
  feeding the Step 3.13 executor so the WS-C exit tests trace to real
  verified UI surfaces.

This is the checkpoint that surfaced the ai-tools handler-variance
typecheck blocker fixed in b8817229b; the checkpoint log documents
the pre/post runs so the fix provenance is preserved.

* test(ai-framework): add WS-C integration tests (runtime policy, attachment bridge, tool-pack coverage)

Closes Step 3.13 of the ai-framework-unification plan (Phase 1 WS-C).
Adds two Playwright HTTP e2e specs under .ai/qa/tests/ai-framework/ covering
superadmin auth sanity (login + wrong-password) and the chat dispatcher
policy gate (unknown agent -> 404 agent_unknown; malformed / missing query
-> 400 validation_error; unauthenticated -> 401). Adds three Jest
integration suites under packages/ai-assistant/.../__tests__/integration/
that exercise the full runtime pipeline with the AI SDK mocked at the module
boundary (mirroring agent-runtime-parity.test.ts): policy gate + tool
resolution (agent_unknown, agent_features_denied, super-admin bypass,
allowedTools filtering, tool-level requiredFeatures skip-with-warn,
mutation_blocked_by_readonly, and a full runAiAgentText pass-through that
asserts the SDK tool map contains only whitelisted tools); attachment
bridge cross-tenant drop (no foreign tenant/org leak in warn), oversized
image without signer -> source=metadata-only, oversized image with signer
-> source=signed-url, missing DI container graceful return; tool-pack
coverage for the search/attachments/meta packs shipped in ai-assistant
itself (requiredFeatures on every read tool, tenant context enforcement,
meta.list_agents empty-registry + RBAC + super-admin bypass, and end-to-end
tool-map composition across the three packs).

Additive only -- zero production code touched. Customer + catalog tool-pack
scenarios from the Step brief remain covered by the per-pack unit tests
already under packages/core/src/modules/{customers,catalog}/__tests__/ai-tools/;
re-testing them from the ai-assistant harness is out of reach until a future
cross-package Jest plumbing lands.

* docs(runs): mark ai-framework-unification step 3.13 complete

- PLAN.md row 3.13 -> done, commit f1cc6be3d.
- HANDOFF.md rewritten; next concrete action = Phase 4 Step 4.1 `<AiChat>` component.
- NOTIFY.md appended with the Step 3.13 outcome and scope-deferral notes.
- step-3.13-checks.md recorded (3 new Jest integration suites / 22 tests, 2 Playwright specs; typecheck + both regression suites preserved).

* feat(ui): add AiChat component + client-side UI-part registry (Phase 2 WS-A)

Phase 2 WS-A opener (Step 4.1). Introduces the canonical embeddable
`<AiChat>` React component under `packages/ui/src/ai/` bound to the Step 3.3
chat dispatcher via the Step 3.4 `createAiAgentTransport` URL convention.
The component:

- Speaks `POST /api/ai_assistant/ai/chat?agent=<module>.<agent>` and
  consumes the dispatcher's plain-text streaming response, surfacing tokens
  into the transcript as they arrive.
- Accepts `agent`, `pageContext`, `attachmentIds`, `initialMessages`,
  `debug`, `onMutationRequested`, and `onError` per the spec's Phase 2 WS-A
  contract. No props beyond the sanctioned set.
- Handles the dispatcher's error envelopes (`agent_unknown`,
  `agent_features_denied`, `execution_mode_not_supported`,
  `tool_not_whitelisted`, etc.) through an inline `Alert` plus the
  `onError` callback. Warning-class denies (tool filter, attachment media
  type) surface as `Alert variant="warning"`; hard denies surface as
  `destructive`.
- Keyboard: `Cmd/Ctrl+Enter` submits, `Escape` aborts an in-flight stream
  (or blurs the composer when idle) — matches the `packages/ui/AGENTS.md`
  dialog convention and the legacy Command Palette pattern.
- Accessible: transcript region has `role="log"` + `aria-live="polite"`;
  composer is a labelled `<Textarea>`; icon-only controls carry
  `aria-label`.
- Reserves client-side UI-part slots for the four Phase 3 mutation cards
  (`mutation-preview-card`, `field-diff-card`, `confirmation-card`,
  `mutation-result-card`) via a global-scoped registry — unknown ids
  render a neutral placeholder chip + `console.warn` instead of throwing.
- Debug panel stub shows the last request/response summary when
  `debug: true`.

Design-system compliant: no hardcoded status colors (`Alert` already emits
`border-status-*` semantic tokens); no arbitrary text sizes; icons via
`lucide-react`; every user-facing string via `useT()`.

AI SDK coupling: the `ai` package is pinned to `6.0.44` but
`@ai-sdk/react` (the source of `useChat`) is not a workspace dependency,
and the dispatcher currently returns a plain-text stream
(`toTextStreamResponse`), not `UIMessageChunk` format — so this Step takes
the brief's sanctioned fallback: a hand-rolled `useAiChat` hook that
reuses `createAiAgentTransport`'s URL convention (single source for the
dispatcher path) but consumes the streaming body through `apiFetch` so
the scoped-headers + auth-redirect contract is honored. When the
dispatcher migrates to `toUIMessageStreamResponse` the hook can collapse
onto `useChat({ transport })` without changing `<AiChat>`'s public
contract.

i18n: 14 additive keys under `ai_assistant.chat.*` in all four locales
(en/pl/es/de); `yarn i18n:check-sync` stays green.

BC: additive-only. New files under `packages/ui/src/ai/`, one new
`export * from './ai'` line in `packages/ui/src/index.ts`, and additive
i18n keys. No existing export renamed or removed.

Tests:

- `packages/ui/src/ai/__tests__/AiChat.test.tsx` — composer renders with
  the i18n placeholder; Cmd+Enter submits and streams assistant text into
  the transcript; dispatcher 404 + `agent_unknown` envelope surfaces as
  `Alert` + `onError`; `Escape` aborts an in-flight stream cleanly.
- `packages/ui/src/ai/__tests__/ui-part-registry.test.ts` — register +
  resolve round-trip; unknown id returns null; unregister works;
  re-registration overwrites; reserved Phase 3 slot ids exposed.

Validation:

- `packages/ui` full regression: 53 suites / 279 tests (baseline +2 / +10).
- `packages/ai-assistant` regression: 28 / 338 preserved exactly.
- `packages/core` regression: 333 / 3033 preserved exactly.
- Typecheck (`yarn turbo run typecheck` across ui/core/app): 3/3 ok.
- `yarn generate`: no generator drift.
- `yarn i18n:check-sync`: green.
- `yarn build:packages`: 18/18 ok.

Next Step: 4.2 — upload adapter that reuses the attachments API and
returns `attachmentIds` for the component's existing prop.

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

* docs(runs): mark ai-framework-unification step 4.1 complete

- Flip Step 4.1 status to done (aae5bdac8) in PLAN.md's Tasks table.
- Rewrite HANDOFF.md: next action is Step 4.2 (upload adapter that reuses
  the attachments API and returns `attachmentIds` that thread into the
  new `<AiChat attachmentIds>` prop). Capture the decision to skip
  Playwright for 4.1 (jsdom RTL coverage is sufficient; 4.4 is the
  natural browser-proof re-entry point).
- Append Step 4.1 entry to NOTIFY.md: files created, files touched
  (additive), full validation-gate numbers (ui 53/279 baseline +2/+10,
  ai-assistant 28/338 exact, core 333/3033 exact, typecheck 3/3 OK,
  generate clean, i18n green, build:packages 18/18 OK), and the four
  recorded decisions: (a) hand-rolled `useAiChat` fallback because
  `@ai-sdk/react` is not in the workspace and the dispatcher returns
  plain-text streams; (b) transport factory is client-safe from the
  ai-assistant barrel; (c) Escape wiring during streaming; (d) i18n
  namespace stayed on `ai_assistant.chat.*` with 14 additive keys across
  all four locales.
- Add `step-4.1-checks.md` with the full verification evidence
  (validation-gate command outputs, DS compliance audit, BC audit).

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

* refactor(skills): split verbose variant into -sophisticated, restore originals

Per user ask: keep the original auto-create-pr / auto-continue-pr /
auto-qa-scenarios / auto-sec-report / auto-update-changelog skills 100%
identical to their pre-PR behavior (lightweight flat .ai/runs/<date>-<slug>.md
file + Progress checklist). The verbose run-folder + HANDOFF + NOTIFY +
per-step-checks + executor-dispatch variant introduced in this PR now ships
under distinct names:

  .ai/skills/auto-create-pr-sophisticated/
  .ai/skills/auto-continue-pr-sophisticated/

Both variants coexist. The default skills are best for one-off bug fixes
and small features; the -sophisticated variants are best for long multi-step
spec implementations that need resumability and contract tracking.

Referrers updated:
- Root AGENTS.md Task Router now lists both variants with a clear trigger.
- .ai/skills/README.md lists both folders + describes the use-case split.
- .ai/runs/README.md (restored to pre-PR) gets a short "two layouts coexist"
  footnote naming which layout each skill emits.

Sibling skills (auto-qa-scenarios / auto-sec-report / auto-update-changelog)
restored to pre-PR state — they continue to delegate to the original
auto-create-pr / auto-continue-pr. No callers of the default flow break.

* feat(ui): add AiChat upload adapter + useAiChatUpload hook (Phase 2 WS-A)

Adds the framework-agnostic `uploadAttachmentsForChat` adapter plus a
React `useAiChatUpload` hook that Step 4.1's `<AiChat>` composer will
call when the user drags files into the chat. The adapter wraps
`POST /api/attachments` (the canonical attachments endpoint used by the
rest of the admin UI), caps in-flight uploads at 3 via a hand-rolled
semaphore, preserves input order, and captures network, server, MIME,
size, and abort outcomes as structured `failed[]` entries so the
composer can render chips and error badges without re-implementing the
adapter everywhere. The hook exposes `{ files, overallProgress, busy,
upload, reset }` state and keeps all user-facing copy out of the hook so
consumers translate via `useT()` at render time.

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

* docs(runs): mark ai-framework-unification step 4.2 complete

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

* refactor(qa): move TC-AI-* integration specs under per-module __integration__/ folders

Step 3.13 placed both TC-AI-001 and TC-AI-002 under .ai/qa/tests/ai-framework/.
Per AGENTS.md and the NOTIFY feedback on
.ai/runs/2026-04-18-ai-framework-unification/, per-module Playwright specs must
live beside the module they exercise; .ai/qa/tests/ is reserved for cross-cutting
QA artifacts and shared helpers only.

- TC-AI-001-auth-sanity moved to packages/core/src/modules/auth/__integration__/
  (it exercises login + /backend redirect, i.e. the auth surface).
- TC-AI-002-agent-policy moved to
  packages/ai-assistant/src/modules/ai_assistant/__integration__/ (it exercises
  the AI chat dispatcher policy gate).
- Helper imports rewritten from the old .ai/qa/tests/helpers/ relative path to
  @open-mercato/core/modules/core/__integration__/helpers/* package imports,
  matching the canonical TC-AUTH-021 pattern.
- Added a short README.md in the newly created ai_assistant __integration__/
  folder pointing at the shared helpers location.

Playwright config is unchanged: .ai/qa/tests/playwright.config.ts already
discovers module-level __integration__/ folders across packages via
discoverIntegrationSpecFiles, so both specs are picked up automatically.

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

* feat(ui): formalize AiChat UI-part registry with Phase 3 slot reservations (Phase 2 WS-A)

Step 4.3 expands the minimal Step-4.1 registry into a first-class
reusable surface:

- `createAiUiPartRegistry` returns an isolated `AiUiPartRegistry`
  instance. The default global registry is seeded with the four Phase 3
  approval-card slot ids (`mutation-preview-card`, `field-diff-card`,
  `confirmation-card`, `mutation-result-card`) pointing at a shared
  `PendingPhase3Placeholder` DS-compliant info alert.
- `registerAiUiPart` / `resolveAiUiPart` top-level helpers are kept as
  thin shims over the default registry for Step-4.1 backward compat.
- `<AiChat>` gains an optional `registry` prop so embedded pages and
  unit tests can inject a scoped registry without leaking state.
- Reserved slot ids live in `ui-part-slots.ts` as a `const`-array +
  string-literal type, so Step 5.10's approval cards can plug in
  without changing call sites.
- i18n keys added under `ai_assistant.chat.pending_phase3.*` (4 locales).
- Unit tests: registry isolation + replace-semantics; slot-id constants
  lock the four reserved ids; `<AiChat>` registry override path covered.
- Integration spec under `packages/ui/__integration__/` mounts the
  component against a mocked stream to verify the resolver path.

* docs(runs): mark ai-framework-unification step 4.3 complete

* feat(ai-assistant): add backend AI playground page + run-object route (Phase 2 WS-B)

Lands the first real user-facing embedding of <AiChat> inside the
backoffice — an operator-only playground at
/backend/config/ai-assistant/playground guarded by
ai_assistant.settings.manage. Picks any registered agent (the caller
can invoke), toggles a debug panel, and previews structured output
through a new scoped POST /api/ai_assistant/ai/run-object route that
reuses the chat dispatcher's auth + policy gate with
requestedExecutionMode='object'. A second additive GET
/api/ai_assistant/ai/agents endpoint mirrors meta.list_agents so the
picker populates without going through the MCP tool transport.

Includes:
- New backend page + meta with sidebar nav injection under Module
  Configs.
- New one-shot run-object HTTP route (JSON response, not streaming).
- New agents-list HTTP route (ACL-filtered summaries).
- 32 new ai_assistant.playground.* i18n keys, synced across en/pl/es/de.
- TC-AI-PLAYGROUND-004 Playwright spec (network-stubbed SSE +
  run-object responses so CI never hits a live LLM provider).
- 8 new unit tests for the run-object route (401/400/404/403/422
  happy+sad paths, AgentPolicyError mapping, stream-mode rejection).
- Internal import narrowing in packages/ui/src/ai/useAiChat.ts to a
  scoped subpath so the Turbopack client bundle no longer pulls the
  @open-mercato/ai-assistant barrel (which transitively imports
  opencode-handlers / server-only DI container). Three AiChat test
  mock paths updated to match; no behavior change.

Tests:
- packages/ai-assistant jest: 29 suites / 346 tests (was 28 / 338).
- packages/ui jest: 58 / 317 preserved.
- packages/core jest: 333 / 3033 preserved.
- yarn build:app green after the narrow-import fix.
- yarn i18n:check-sync green across 46 modules × 4 locales.

BC: additive only (new URLs, new page, new i18n keys, new explicit
./ai subpath export in packages/ui/package.json). Existing
<AiChat> / useAiChat consumers see no contract change.

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

* docs(runs): mark ai-framework-unification step 4.4 complete

- PLAN.md: row 4.4 → `done` + short SHA `f62aead47`.
- HANDOFF.md: rewritten; next concrete action = Step 4.5 (backend
  agent settings page with prompt overrides + tool toggles +
  attachment policy).
- NOTIFY.md: appended a 2026-04-18T17:05 entry with files / decisions /
  verification / BC posture for the run-object route + playground page.
- step-4.4-checks.md + step-4.4-artifacts/dev-app.log: the usual
  per-step proof bundle (29/346 ai-assistant suite, 58/317 ui, 333/3033
  core, yarn build:app 51.9s, i18n / generate green). Documents the
  pre-session next-server saturation that required the Playwright
  integration spec to carry the browser-smoke coverage.

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

* docs(runs): add Step 4.4 browser smoke screenshot (playground empty state)

Captures the real-browser verification the Step-4.4 executor couldn't
run due to a stale next-server process. The /backend/config/ai-assistant/
playground page renders correctly with the expected "No AI agents
registered" empty-state alert (agents ship with Steps 4.7/4.8), sidebar
navigates to it via AI Assistant > AI Playground, and the page guards
off `ai_assistant.settings.manage` as designed.

This closes the step-4.4-artifacts/ evidence set; the Playwright
integration spec (TC-AI-PLAYGROUND-004) remains the automated gate.

* fix: simplify skill metadata descriptions

* fix(tests): stabilize TC-AI-001/002 flakes surfaced by 5-step checkpoint

TC-AI-001 wrong-password — two bugs fixed:
- The .catch-swallowing of `form[data-auth-ready="1"]` waitForSelector
  let the click land on a still-disabled submit button; made the hydration
  wait mandatory with a 30s timeout.
- Cookie/demo-env banners overlay the submit button and intercepted
  pointer events. Replaced `button.click()` with form.requestSubmit()
  so the form's native submit path fires regardless of overlay.
- Increased the per-test timeout to 60s for dev cold-compile.

TC-AI-002 unauthenticated-caller — two bugs fixed:
- The shared `request` fixture carries session cookies from earlier
  getAuthToken calls; switched to a dedicated `playwrightRequest.newContext()`
  for the unauth probe.
- Unauth requests short-circuit at the framework's `requireAuth` page
  metadata guard which returns `{error:"Unauthorized"}` (no `code`),
  not the route's own `jsonError(..., 'unauthenticated')`. Assertion
  now accepts either envelope — whichever the framework hands back.

Checkpoint artifacts captured under
.ai/runs/2026-04-18-ai-framework-unification/checkpoint-5step-after-4.4-artifacts/.
All 7 TC-AI specs pass in 1.7m.

* docs(runs): summarize 5-step full-gate checkpoint after Step 4.4

* feat(ai-assistant): add backend AI agent settings page + prompt-override placeholder route (Phase 2 WS-B)

Ships the operator-facing agent configuration page at
`/backend/config/ai-assistant/agents` with an agent picker, per-agent metadata panel,
additive prompt-section override editor (eight spec §8 section ids), read-only
allowed-tools list with Phase-3-deferred tooltip, and attachment media-type
policy. Prompt-override persistence is explicitly deferred: the placeholder
route `POST /api/ai_assistant/ai/agents/:agentId/prompt-override` validates the
agent and feature gate, then returns `{ pending: true, message: 'Persistence
lands in Phase 3 Step 5.3.' }` so Step 5.3 can wire versioned storage without
churning the UI contract. The existing agents-list endpoint gains additive
`systemPrompt`, `readOnly`, `maxSteps`, and `tools[]` fields so the client can
display the read-only tool details inline.

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

* docs(runs): mark ai-framework-unification step 4.5 complete

Flip PLAN row 4.5 to done (ce011a9e5), rewrite HANDOFF around Step 4.6
(i18n keys + keyboard shortcuts + debug support polish — closes Phase 2
WS-B), append the NOTIFY entry, and add the per-step checks + artifacts.

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

* feat(ai-assistant): polish Phase 2 WS-B (i18n audit, shared keyboard shortcuts, debug panel)

Close Phase 2 WS-B polish (Step 4.6). Every Phase-2 UI surface that accepts
user input now shares a single keyboard-shortcut hook, the <AiChat> debug
panel renders resolved tool map / prompt sections / last request + response
in collapsible sections, and the i18n audit proves no user-facing literal
slipped past useT().

- New packages/ui/src/ai/useAiShortcuts.ts owns Cmd/Ctrl+Enter + Escape
  for every AI surface; exported from @open-mercato/ui/ai and consumed by
  <AiChat>, AiPlaygroundPageClient, and AiAgentSettingsPageClient.
- <AiChat> grows optional `debugTools` and `debugPromptSections` props and
  renders four collapsible <details> sections plus a status footer;
  playground page wires both from the agents API payload.
- Agent picker stays inline (~15 lines per surface — under the 50-line
  threshold named in the brief); the former `TODO(step 4.6)` comment is
  rewritten to document the decision instead of pointing at an open task.
- 19 new i18n keys under `ai_assistant.chat.debug.*` and
  `ai_assistant.chat.shortcuts.*`, synced across en/pl/es/de
  (`yarn i18n:check-sync` green, 46 modules × 4 locales).
- Unit tests: 7 for `useAiShortcuts`, 4 for the AiChat debug-panel
  expansion (RTL). Baseline preserved: ai-assistant 30/353, ui now
  60/328 (+2 suites, +11 tests), core 333/3033.
- Integration tests: TC-AI-PLAYGROUND-004 toggles the debug panel and
  asserts the three new sections render; TC-AI-AGENT-SETTINGS-005 adds a
  Cmd/Ctrl+Enter test that triggers the placeholder save endpoint.

BC: additive only — new optional props on <AiChat>, new package exports,
new i18n keys, no removed or renamed surfaces.

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

* docs(runs): mark ai-framework-unification step 4.6 complete

Flip PLAN row 4.6 to done + ee68a0030. Rewrite HANDOFF for Step 4.7
(next = first customers read-only agent, opens Phase 2 WS-C). Append
NOTIFY entry. Add step-4.6-checks.md with the i18n audit table and
artifacts (playground + agents browser-smoke screenshots).

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

* feat(customers): add customers.account_assistant read-only AI agent (Phase 2 WS-C)

First production `ai-agents.ts` in the repo. Declares the
customers.account_assistant agent with `readOnly: true`, a 16-tool
whitelist covering the customers read pack (Step 3.9) plus the
general-purpose `search.*` / `attachments.*` / `meta.describe_agent`
tools, and a structured PromptTemplate with the seven §8 sections
(ROLE / SCOPE / DATA / TOOLS / ATTACHMENTS / MUTATION POLICY /
RESPONSE STYLE). Compiled to the agent's systemPrompt string while the
structured template is additionally exported so Phase 5.3 overrides can
address sections by name. resolvePageContext is an async identity stub
that Step 5.2 will replace. Types are redeclared locally to keep
@open-mercato/core off the @open-mercato/ai-assistant import graph,
mirroring customers/ai-tools/types.ts. Unit tests (9) under
packages/core/src/modules/customers/__tests__/ai-agents.test.ts and a
new integration spec TC-AI-CUSTOMERS-006 assert the HTTP agents list,
the meta.describe_agent tool call, and the playground picker option.

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

* docs(runs): mark ai-framework-unification step 4.7 complete

- Flip PLAN.md row 4.7 to done with short SHA c4cba55ad.
- Rewrite HANDOFF.md: next action is Step 4.8 (first catalog agent with
  prompt template, read-only).
- Append NOTIFY.md entry with verification summary and decisions.
- Add step-4.7-checks.md with verification table and decisions.
- Add step-4.7-artifacts/playground-customers-agent.png showing the
  playground picker populated with the new agent.

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

* docs(skills): batch sophisticated auto-create/continue PR verification into checkpoints

Shift the sophisticated auto-create-pr and auto-continue-pr skills from
per-Step verification chatter to checkpoint-based verification so long
spec runs stay lean and focused:

- Per-Step: one code commit only, Tasks-table row flipped in the same
  commit. No step-<X.Y>-checks.md, no per-Step artifacts folder, no
  per-Step HANDOFF rewrite, no routine NOTIFY appends.
- Checkpoint (every 5 Steps or when a Phase with >=3 Steps closes):
  targeted typecheck/tests/i18n/generate/build for the touched packages,
  focused integration tests + screenshots when any Step in the window
  touched UI, write checkpoint-<N>-checks.md, rewrite HANDOFF.md, append
  one NOTIFY entry, commit as docs(runs): checkpoint N.
- Spec completion: full validation gate, yarn test:integration and
  yarn test:create-app:integration (mandatory for code changes), plus a
  ds-guardian pass that lands auto-fixes as X.Y-ds-fix Steps.

Executor templates and Rules sections updated in lockstep for both
skills.

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

* feat(catalog): add catalog.catalog_assistant read-only AI agent (Phase 2 WS-C)

Mirrors the Step 4.7 customers.account_assistant pattern under the
catalog module. Declares `catalog.catalog_assistant` with:
- readOnly: true, mutationPolicy: 'read-only', executionMode: 'chat'
- 17-tool whitelist: twelve base catalog read tools (Step 3.10) plus
  five general-purpose tools (search.*, attachments.*, meta.*)
- requiredFeatures: catalog.products.view + catalog.categories.view
- acceptedMediaTypes: image/pdf/file
- Structured PromptTemplate with the seven spec §8 sections (ROLE,
  SCOPE, DATA, TOOLS, ATTACHMENTS, MUTATION POLICY, RESPONSE STYLE)
  compiled into systemPrompt for the runtime, exported as
  promptTemplate for future prompt-override work
- resolvePageContext async stub (Step 5.2 replaces the body)

Keeps the Step 4.9 D18 merchandising tools
(catalog.search_products, catalog.get_product_bundle,
catalog.list_selected_products, catalog.get_product_media,
catalog.get_attribute_schema, catalog.get_category_brief,
catalog.list_price_kinds) and every authoring tool
(catalog.draft_*, catalog.extract_*, catalog.suggest_*) OUT of the
whitelist so the generic catalog agent cannot shadow the D18 demo
agent's entry point.

Types redeclared locally (same pattern as Step 4.7 and the existing
catalog ai-tools/types.ts) because @open-mercato/core does not
depend on @open-mercato/ai-assistant.

Includes 11 unit tests (read-only flag, execution metadata,
whitelist membership, mutation deny-list, D18 deny-list, authoring
deny-list, ACL features, seven §8 sections, prompt compilation,
resolvePageContext stub) and integration spec TC-AI-CATALOG-007
covering /api/ai_assistant/ai/agents, meta.describe_agent, and
the playground picker (asserting both agents visible).

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

* docs(runs): mark ai-framework-unification step 4.8 complete

* feat(catalog): add catalog.merchandising_assistant agent + products-list AiChat sheet (Phase 2 WS-C, spec §10 D18)

Phase 2 exit for the D18 merchandising demo. Ships:

- `catalog.merchandising_assistant` agent (read-only, chat mode) next
  to `catalog.catalog_assistant` in `ai-agents.ts`. Whitelists the
  exact D18 tool set: 7 read tools (Step 3.11), 5 authoring tools
  (Step 3.12), 5 general-purpose tools (Step 3.8). Deny-list tests
  enforce no mutation tools and no overlap with the generic catalog
  assistant's list/get surface.
- Prompt template follows spec §10.5 verbatim (seven structured
  sections: ROLE, SCOPE, DATA, TOOLS, ATTACHMENTS, MUTATION POLICY,
  RESPONSE STYLE) and compiles into `systemPrompt`.
- `resolvePageContext` async stub — Phase 5.2 wires server-side
  record hydration; this Step lets the client page form the context.
- New `MerchandisingAssistantSheet` drawer on
  `/backend/catalog/catalog/products` gated behind
  `catalog.products.view` + `ai_assistant.view`. Feeds `<AiChat>` a
  spec §10.1-shaped `pageContext` (view / recordType / recordId /
  extra.filter / extra.totalMatching / extra.selectedCount) that
  updates live when the operator selects or filters rows. "Acting on
  N products" pill surfaces the selection size.
- Products DataTable wired to the sheet via a small listener so the
  current selection and filter state propagate into `pageContext`
  without coupling the table to the sheet's internals.
- 4 locale files extended under `catalog.merchandising_assistant.*`
  (en / pl / es / de, 6 keys each).
- Integration spec under
  `packages/core/src/modules/catalog/__integration__/` asserts the
  button renders for superadmin, the sheet opens with the composer,
  the selection pill updates, and the playground picker shows all
  three agents.
- Unit tests extended (23/23 green) for the second agent's read-only
  flag, 17-tool whitelist, deny-lists, ACL features, seven-section
  prompt, compilation, and pageContext stub.

Browser evidence: step-4.9-artifacts/{products-list-with-ai-trigger,
merchandising-sheet-open,playground-three-agents}.png.

* docs(runs): mark ai-framework-unification step 4.9 complete

* feat(ai-assistant-examples): backend + portal AiChat injecti…
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
* feat(staff): move assignable-staff route to staff module with 308 redirect (Phase 1.A)

Owns the staff team-member listing under its canonical URL and breaks the
customers -> staff entity import chain.

- New route at /api/staff/team-members/assignable preserves the original
  Zod schema, response shape, RBAC (customers.roles.view page guard +
  customers.roles.manage OR customers.activities.manage handler check),
  pageSize=100 cap, and tenant-scoped findWithDecryption usage.
- Legacy /api/customers/assignable-staff returns 308 Permanent Redirect to
  the new URL preserving the query string; openApi marks the GET method
  as deprecated.
- In-tree UI fetcher (customers detail assignableStaff helper) migrated
  to the new URL. The dialog unit test and TC-CRM-038 integration test
  also point at the new URL.
- New unit tests assert the redirect status, Location header, and
  query-string preservation. New TC-STAFF-005 integration test exercises
  the new URL plus the legacy redirect end-to-end (maxRedirects: 0).

Refs spec .ai/specs/2026-05-08-staff-decouple-from-core.md

* refactor(planner): consume staff availability access via DI resolver (Phase 1.B)

Removes the direct StaffTeamMember import from planner so the module no
longer hard-depends on staff for self-availability access checks.

- New staff/lib/availabilityAccess.ts owns resolveAvailabilityWriteAccess
  plus the planner.manage_availability / staff.my_availability.* feature
  constants. Adds an optional unregistered? sentinel field to the
  AvailabilityWriteAccess shape (additive, BC surface #2 STABLE).
- New staff/di.ts registers the resolver under the public
  availabilityAccessResolver DI key (Awilix asValue).
- planner/api/access.ts becomes a thin DI wrapper that resolves the key
  with { allowUnregistered: true } and synthesizes an unregistered shape
  + console.warn when staff is absent. assertAvailabilityWriteAccess
  throws CrudHttpError(403, { error: 'staff_module_not_loaded' }) on
  exactly that branch; all other branches stay byte-identical.
- Unit tests cover the fail-soft branch, normal delegation, and the
  per-member self-scope assertion. Staff DI smoke test asserts
  hasRegistration('availabilityAccessResolver') === true and that a
  bare container with allowUnregistered: true returns undefined.

Refs spec .ai/specs/2026-05-08-staff-decouple-from-core.md

* docs(staff): document public DI contract surfaces and deprecation notice (Phase 1.C)

- Adds packages/core/src/modules/staff/AGENTS.md declaring staff as
  optional and listing availabilityAccessResolver (DI surface open-mercato#9 STABLE),
  the new /api/staff/team-members/assignable route (API surface #7
  STABLE), and the staff.my_availability.* feature IDs (ACL surface open-mercato#10
  FROZEN) as the public contract. Entity classes (StaffTeam,
  StaffTeamMember, etc.) are explicitly module-internal — cross-module
  consumers must go through a DI service or an API route.
- Adds an UPGRADE_NOTES.md entry under a new 0.6.0 -> 0.7.0 (unreleased)
  section deprecating GET /api/customers/assignable-staff in favour of
  GET /api/staff/team-members/assignable. The 308 redirect stays for at
  least one minor; removal scheduled no earlier than the next major.

Decouple proof: `grep -rn "@open-mercato/core/modules/staff"
packages/core/src/modules/ | grep -v "/staff/" | grep -vE
"__tests__|__integration__"` returns zero matches.

Refs spec .ai/specs/2026-05-08-staff-decouple-from-core.md

* docs(spec): record implementation status and pre-implementation analysis for staff decouple

- Adds the structured pre-implementation analysis at
  .ai/specs/analysis/ANALYSIS-2026-05-08-staff-decouple-from-core.md
  covering the 13 BC contract surfaces, AGENTS.md compliance matrix,
  risk assessment, and remediation plan that drove the implementation.
- Annotates the spec with three doc fixes applied before implementation
  (test fixture path, Awilix version verification step, pre-populated
  consumer inventory) and the deviations observed during execution:
  UPGRADE_NOTES.md instead of the non-existent RELEASE_NOTES.md;
  method-level openApi.deprecated placement (the field lives on
  OpenApiMethodDoc, not OpenApiRouteDoc); Awilix 12.0.5 supports
  allowUnregistered natively so no try/catch fallback is needed.
- Logs the final verification gate results (yarn generate, build,
  typecheck, lint, full jest suite — 446/446 suites, 3736/3736 tests).

* fix(staff): assert assignable redirect via URLSearchParams (TC-STAFF-005)

Comparing target.search as a raw string is fragile because servers may
encode spaces as `+` (RFC 3986 / application/x-www-form-urlencoded)
instead of `%20`. CI exposed this when the legacy redirect returned
`?search=QA+Staff+Assignable+...` while the test built
`?search=QA%20Staff%20Assignable%20...`. Both are valid; compare the
decoded values via URLSearchParams.get() instead.
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
…09, bulk validation, i18n (open-mercato#2303, open-mercato#2304, open-mercato#2305) (open-mercato#2309)

* feat(staff): add timesheets backend — SPEC-069 Phase 1 (Steps 1-5)

Implements the backend foundation for timesheets inside the staff module:

- ACL: 7 new feature flags (view, manage_own, manage_all, projects.view/manage, approve, lock)
- Data: 4 entities (TimeEntry, TimeEntrySegment, TimeProject, TimeProjectMember) with indexes
- Validators: 9 Zod schemas including bulk save (max 200 entries)
- Commands: 8 undoable commands (CRUD entries, CRUD projects, assign/unassign members)
- Events: 8 new events (CRUD + timer_started/stopped)
- API: 8 route files (entries CRUD, bulk save, timer start/stop, segments, projects CRUD, employee assignments)
- Search: TimeProject indexed (name, code, description, type, cost_center)
- Interceptor: self-scope enforcement on dashboard widget data for non-admin users
- Utility: shared staffMemberResolver (userId → staffMemberId)

Note: TimeEntry.date uses native date type (not text) per pre-implementation analysis finding.
UI pages, dashboard widgets, and i18n keys to follow in subsequent commits.

* feat(staff): add timesheets UI — SPEC-069 Phase 1 (Steps 6-7)

- My Timesheets monthly grid with 3-query N+1 mitigation (my-projects endpoint)
- Projects admin: list, create, detail with collapsible employee cards + Add Employee modal
- Feature gating: employee sees read-only views, admin/superadmin gets full management
- Page metadata for RBAC: detail requires projects.view, create requires projects.manage
- Bug fixes: bulk route auth.sub, segment route imports, employees route URL parsing
- Fix sticky column transparency in grid (bg-muted/50 → bg-muted)

* fix(staff): fix timer widget and route params — SPEC-069 Phase 1 (Step 8)

Fix timer detection (check startedAt/endedAt instead of non-existent
timer_running field), fix URL param extraction for segments routes,
fix create response field name, add dashboard widgets (Hours by Project
+ Time Reporting), analytics config, self-scope enforcement, i18n keys,
and project page metadata.

* test(staff): add timesheets integration tests — SPEC-069 Phase 1 (Step 9)

8 Playwright tests covering time entry CRUD, timer start/stop,
segments, projects, bulk save, dashboard widget data with self-scope
enforcement, and UI smoke tests for grid, projects list, and widgets.

* refactor(staff): update analytics date type and improve segment creation

- Changed the date field type in analytics configuration from 'date' to 'timestamp'.
- Refactored segment creation in time entries and timer start routes to use a segmentData object for better readability and maintainability.
- Updated error handling in the timesheets page to correctly reference the response object.
- Enhanced time entry and project commands to ensure proper type handling for source and status fields.
- Added optional fields for startedAt and endedAt in the time entry validation schema.
- Expanded i18n files with new keys for timesheet functionality in multiple languages.

* feat(staff): enhance time entry ownership validation and improve timesheet routes

- Added ownership validation for time entries in POST and PATCH routes to ensure users can only manage their own entries.
- Integrated `getStaffMemberByUserId` function to retrieve staff member details based on user ID.
- Updated error handling to return appropriate responses for unauthorized access and entry not found scenarios.
- Modified bulk route to include `staffMemberId` in the query for better data integrity.
- Refactored timesheets page state management to improve handling of entry data and dirty state tracking.

* fix(tapestry): update response field names and enhance timesheet integration tests

- Changed response field names in time project and employee assignment routes for consistency.
- Enhanced timesheet integration tests to include setup and teardown for project and employee assignments.
- Added assignedStartDate to employee assignment fixture for better tracking of assignment dates.

* test(staff): skip dashboard widget visibility test due to missing DB entries

- Updated the TC-STAFF-022 integration test to skip the widget visibility check for Time Reporting and Hours by Project due to the absence of required dashboard_role_widgets DB entries in setup.ts.
- Noted that the test has been manually verified and will be re-enabled once the setup includes the necessary widget IDs in the default role configuration.

* fix(staff): address PR review — CrudForm, DataTable filters, UX polish

- Use CrudForm for project create/edit with shared projectFormConfig
- Add separate edit page with version history and delete support
- Replace inline editing with link to edit page
- Status filter via DataTable filters (not custom buttons)
- Empty state with "+ Add first project" action button
- Profile link points to self-service /staff/profile/create
- Fix response field names (timeProjectId, timeProjectMemberId)

* feat(staff): enhance MyTimesheetsPage with project management features

- Added state management for project management access.
- Implemented API call to check if the user can manage projects.
- Updated empty state messaging to provide context for users without assigned projects, differentiating between admin and employee roles.
- Enhanced UI with buttons for creating and viewing projects based on user permissions.

* feat(i18n): update translations for timesheet project management

- Added new keys for project management features in German, English, Spanish, and Polish.
- Enhanced messaging for users without assigned projects, including admin-specific instructions.
- Updated UI labels and error messages to improve clarity and user experience.

* feat(staff): add timesheets UX enhancements spec

* feat(staff): add weekly view, calendar picker, list view and UX improvements

- Weekly grid as default view with Mon-Sun columns
- Toggle between Weekly/Monthly view modes
- Calendar week picker dropdown with "This week"/"Last week" shortcuts
- List view showing entries grouped by day
- View switcher component (Timesheet/List view)
- Compact grid cells with numeric-only input validation
- Fix cross-month week/monthly toggle bug
- Auto-assign project creator on creation
- Fix 403 redirect for employee on project detail
- No full page reload on navigation (opacity fade instead)

* feat(staff): timesheets UX fixes for hackathon

- Timer: add missing staffMemberId to create payload (fixes "Failed to start timer")
- AddRowDropdown: rewrite with createPortal so dropdown overlays without layout shift or inner scroll
- Move Add row into table tbody (above Daily Total row)
- CreateProjectDialog: add embedded prop to CrudForm to hide duplicate FormHeader (single Create button now)
- i18n: remove leading "+" from addRow.trigger and addRow.createProject (Plus icon already renders the symbol) across en/de/es/pl
- Reorganize timesheet UI components from backend/staff/timesheets/components to lib/timesheets-ui

* feat(staff): persist grid membership with show_in_grid + remove row in My Timesheets

Adds show_in_grid column on staff_time_project_members (+ backfill), new
self-service PATCH endpoint, and X remove button with confirm dialog.
Closes a gap in the original UX spec where "+ Add row" was only local state.

* feat(staff): add project colors, sidebar timer indicator, and inline list descriptions

- Phase 3: color field (varchar 20) on staff_time_projects with 12-color palette
- ColorPicker component, ProjectColorDot rendered in grid/AddRow/Timer/ListView
- Auto-fallback color from project name hash (djb2)
- Sidebar timer indicator widget with pulsing dot, persists via sessionStorage
- Inline editable descriptions in ListView (click to edit, Enter/blur to save)
- show_in_grid column on staff_time_project_members with backfill migration
- Self-service PATCH /api/staff/timesheets/my-projects/{projectId} endpoint
- X button to remove rows from grid with confirm dialog
- Unit tests for colors.ts (13 passing)
- i18n keys for 4 languages (en/pl/es/de)

* fix(staff): show add row button when user has assignments but empty grid

Switch the empty state condition from projects.length to allAssignedProjects.length
so users who haven't opted any project into their grid still see the "+ Add row"
control instead of being stuck on the "create a project" screen.

* fix(staff): resolve DELETE employee from time project returning 400

The mapInput for the employees DELETE action read `raw` directly, but the
CRUD factory passes `raw = { body, query }` — so the id coming in the query
string never reached the zod parser. Align with the customers/people pattern:
read id from parsed.body.id ?? parsed.id ?? parsed.query.id ?? URL search params.

* feat(staff): add timesheets projects portfolio view (Phase A + B)

Redesign /backend/staff/timesheets/projects into a role-aware portfolio
with table and cards view modes. PM sees team-wide aggregates; Collaborator
sees personal hours scoped via mine=1 filter.

Backend
- New aggregate helpers: computeProjectsKpis, computeProjectHoursTrend,
  listProjectMembersPreview + 26 unit tests
- New GET /api/staff/timesheets/projects/kpis endpoint with role-aware
  PM/Collab response shapes (openApi + zod schemas)
- New response enricher staff.timesheets-projects-portfolio targeting
  staff:staff_time_project — adds _staff.{hoursWeek, hoursTrend,
  members, memberCount, myRole} via batched SQL (no N+1)
- Extend /api/staff/timesheets/time-projects with mine=1 filter and
  include query param

UI
- ProjectsKpiStrip (5 PM cards / 3 Collab cards with delta badges)
- SavedViewTabs (status + Mine, URL-synced)
- ViewModeToggle + useProjectsViewMode (localStorage persisted)
- ProjectCard, ProjectsCards grid (3 col)
- HoursSparkline (SVG, 7 weeks, theme-aware)
- ProjectMembersAvatarStack (max 4 + N overflow, dark mode palette)
- Enriched table columns: color dot, status badge, team/role, sparkline,
  relative updated-at
- Inline refresh (no skeleton flash on filter/search)
- Dark mode tokens across all new components + fix project name in
  My Timesheets grid

i18n: 31+ new staff.timesheets.projects.portfolio.* keys in
en/de/pl/es (DE/PL/ES placeholders pending translation)

Spec: .ai/specs/2026-04-24-timesheets-projects-portfolio-view.md
Pre-impl analysis: .ai/specs/analysis/

Lessons: QueryEngine doesn't support $or top-level filters; CRUD
factory's enricher feature gating expects rbac.getGrantedFeatures()
which RbacService doesn't expose — so enrichers with features array
are silently skipped. Routed ACL via route metadata + inline manage
check instead.

* fix(staff): prevent cross-employee time-entry leak on GET endpoint

Add self-scope interceptor for GET /api/staff/timesheets/time-entries that forces staffMemberId to the caller's own staff member when the user lacks staff.timesheets.manage_all (or staff.* wildcard).

Mirrors the existing self-scope pattern used by the dashboard widget endpoint. Extracts the wildcard ACL check into a shared helper to avoid duplication between both interceptors.

Fixes review finding #1 on PR open-mercato#1111.

* fix(staff): enforce unique constraint on project code and member assignment

MikroORM v6 silently drops the unique flag when the @Index options bag also carries a where clause. The original migration emitted plain 'create index' statements instead of 'create unique index', allowing duplicate project codes within an (org, tenant) and duplicate active assignments of the same staff member to one project.

Add a fix-up migration that drops the affected indexes and recreates them as partial unique indexes (where deleted_at is null) so reuse of codes after soft-delete keeps working.

Affected indexes: staff_time_projects_code_unique_idx, staff_time_project_members_unique_idx.

Fixes review finding #2 on PR open-mercato#1111.

* fix(dashboards): remove cross-module staff coupling from widgets/data route

The route was importing StaffTeamMember from the staff module and inlining self-scope enforcement for staff:staff_time_entries entityType, violating the architectural rule against cross-module ORM coupling.

Replace the inline check with a proper invocation of runApiInterceptorsBefore. The interceptor 'staff.timesheets.self-scope-widget-data' already declared in staff/api/interceptors.ts now runs effectively (until now it was registered but never invoked because custom routes do not auto-run interceptors).

Side effects: the route is now open to interceptor injection from any module, not just staff. The staff interceptor itself was not changed.

Fixes review finding #3 on PR open-mercato#1111.

* fix(staff): make bulk time-entries save atomic

Wrap the create/update/soft-delete loop in em.transactional so the whole batch is committed or rolled back as a unit. A mid-loop failure no longer leaves the database in a partial state.

Also move the existingEntries lookup inside the transaction to avoid a read-modify-write race between fetching current rows and applying mutations.

Fixes review finding #4 on PR open-mercato#1111.

* fix(staff): emit CRUD side effects from bulk time-entries save

The bulk endpoint mutated entities directly inside a single em.flush(), so the highest-traffic write path was silently skipping the staff.timesheets.time_entry.created/updated/deleted events, query index updates, and cache invalidation that the per-row commands provide.

Collect a per-row action log inside the transaction (created/updated/deleted), then after the transaction commits dispatch emitCrudSideEffects for each entity and flushCrudSideEffects once at the end. Events fire only after the DB changes are durable, per the side-effects guideline in core/AGENTS.md.

Fixes review finding #5 on PR open-mercato#1111.

* fix(staff): wire mutation guards into custom write routes

AGENTS.md requires every non-makeCrudRoute write to call validateCrudMutationGuard before mutating and runCrudMutationGuardAfterSuccess after success so record locks, conflict detection, and ACL-driven mutation policies actually fire. The six timesheets custom write routes shipped without it.

Add a thin staff/api/guards.ts helper around runMutationGuards + bridgeLegacyGuard (mirrors integrations/api/guards.ts) and wire it into:

- time-entries/bulk (POST) - update on staff.timesheets.time_entry

- time-entries/[id]/timer-start (POST) - update on staff.timesheets.time_entry

- time-entries/[id]/timer-stop (POST) - update on staff.timesheets.time_entry

- time-entries/[id]/segments (POST) - create on staff.timesheets.time_entry_segment

- time-entries/[id]/segments/[segmentId] (PATCH) - update on staff.timesheets.time_entry_segment

- my-projects/[projectId] (PATCH) - update on staff.timesheets.time_project_member

Each route now blocks on guard rejection (422 with the guard body) and dispatches afterSuccess callbacks after the flush succeeds.

Fixes review finding #6 on PR open-mercato#1111.

* fix(staff): drop vitest import from colors test

The colors test imported describe/it/expect from vitest, which is not a dependency, so the test could not run. Drop the import and rely on jest globals like the rest of the module.

Fixes review finding #7 on PR open-mercato#1111.

* fix(staff): extract pure helpers so unit tests can run

computeProjectsKpis and listProjectMembersPreview both imported MikroORM entities at module top-level, so the unit tests added for SPEC-069 Step 9 transitively loaded @mikro-orm/core ESM and exploded under the jest preset. The tests never executed.

Move the pure helpers into their own files that don't import entities:

- timesheets-projects/kpiMath.ts: deltaPct, minutesToHours

- timesheets-projects/initials.ts: computeInitials

Update the helper test files to import from the new pure modules. computeProjectsKpis and listProjectMembersPreview now import (and re-export computeInitials) from the new files so callers keep working.

Result: 26/26 helper tests now pass.

Fixes review finding open-mercato#8 on PR open-mercato#1111.

* fix(staff): wire timesheets page writes through useGuardedMutation

The My Timesheets page is a custom backend page (not a CrudForm), so its four write call sites (bulk time-entries POST and three my-projects PATCH calls) bypassed the global mutation injection hooks. Record-lock conflict handling, scoped request headers, and the standard onBeforeSave/onAfterSave hooks never fired.

Wrap each write in runMutation({ operation, context, mutationPayload }) so global injection modules can run their before/after hooks and consume mutation errors consistently. Each call site passes a stable resourceKind plus the project or staff member id as resourceId.

Fixes review finding open-mercato#9 on PR open-mercato#1111.

* fix(staff): use readJsonSafe consistently in timesheets routes

Three timesheets write routes still read JSON via req.json().catch(...) while the rest of the module already adopted readJsonSafe (see my-projects/[projectId]). Pick the conventional helper everywhere.

Affected:

- time-entries/bulk (POST)

- time-entries/[id]/segments (POST)

- time-entries/[id]/segments/[segmentId] (PATCH)

Fixes review finding open-mercato#10 on PR open-mercato#1111.

* fix(staff): use resolveOrganizationScopeForRequest in segment PATCH

Every peer timesheets route resolves the active scope via resolveOrganizationScopeForRequest so org switching in multi-org tenants works. The segment PATCH was reading auth.tenantId/auth.orgId directly, so requests sent from a non-default organization landed on the wrong scope.

Reorder the handler to create the container before validating scope, then derive tenantId/organizationId from the resolver with the auth values as fallback, mirroring the bulk and timer routes.

Fixes review finding open-mercato#11 on PR open-mercato#1111.

* fix(staff): tighten projectId UUID validation in my-projects PATCH

The old /^[0-9a-f-]{36}$/i regex accepts any 36-character mix of hex and dashes (e.g. 36 dashes), so junk ids slipped through to the DB query. Replace it with z.string().uuid() so only well-formed UUIDs are accepted.

Fixes review finding open-mercato#12 on PR open-mercato#1111.

* fix(staff): use apiCallOrThrow for timesheets writes

Mixed patterns inside the same files: some writes used apiCallOrThrow / readApiResultOrThrow while others called apiCall and checked res.ok manually. Pick the convention so server error bodies propagate uniformly through raiseCrudError instead of a hand-rolled 'throw new Error(await res.response.text())'.

Updated:

- backend/staff/timesheets/page.tsx — 4 writes inside runMutation (bulk save + 3 my-projects PATCH)

- lib/timesheets-ui/TimerBar.tsx — 3 writes (time-entries POST, timer-start, timer-stop) consolidated into try/catch so the flash message paths are unchanged

Read-only GETs that use a fallback on failure stay on apiCall.

Fixes review finding open-mercato#13 on PR open-mercato#1111.

* fix(staff): sort i18n keys to satisfy i18n:check-sync

yarn i18n:check-sync was failing on the four staff locale files (en/pl/es/de) with 'unsorted keys'. Run --fix to reorder; no key or value content changes.

Fixes review finding open-mercato#14 on PR open-mercato#1111.

* test(staff): regression guard for time-entries self-scope leak

Spec §Security calls out the self-scope rule but no integration test covered it. Add TC-STAFF-023 so the GET cross-employee leak fixed in a7704babd cannot regress unnoticed.

The test logs in as admin to create a time entry owned by the admin's own staff member, then logs in as employee (manage_own only, no manage_all) and issues GET /api/staff/timesheets/time-entries?staffMemberId=<admin's id>. It asserts the admin entry never appears and that every returned row belongs to the employee — proof the staff/api interceptor rewrote the filter to the caller's own staff member id.

Self-contained: creates project + assignment + entry in setup, cleans up in finally.

Fixes review finding open-mercato#15 on PR open-mercato#1111.

* fix(staff): seed timesheets dashboard widgets into role defaults

The Time Reporting and Hours by Project widgets ship with defaultEnabled:false so the global dashboard seed never associated them with any role. Existing tenants ended up with the widgets installed but invisible — and TC-STAFF-022 had to skip in CI because nothing would render.

Wire staff/setup.ts seedDefaults to call appendWidgetsToRoles for superadmin, admin, and employee with both timesheets widget ids. appendWidgetsToRoles is idempotent and only adds missing ids, so re-running setup on existing tenants is safe. The dashboards module sits before staff in modules.ts, so the DashboardRoleWidgets rows it creates already exist when staff's seed runs.

Update TC-STAFF-022's skip comment to explain the seed is now in place; remove the skip once CI runs against a freshly-seeded tenant.

Fixes review finding open-mercato#16 on PR open-mercato#1111.

* docs(spec): clarify TimeProject.customer_id is optional

The Data Models table and Projects API contract both marked customer_id as required, but the actual entity and validator have always treated it as nullable — internal projects have no customer. The spec was the inconsistent side.

Update line 259 (Data Models) and line 421 (Create/Update fields) to call out the column as optional, and log the doc fix in the changelog.

Fixes review finding open-mercato#18 on PR open-mercato#1111.

* docs(spec): rename SPEC-069 file to date+slug convention

.ai/specs/AGENTS.md mandates {date}-{title}.md filenames and forbids new SPEC- prefixes; the timesheets spec was the lone outlier in this PR's surface. git mv preserves history, the README link is updated, and a changelog entry is added inside the spec. Textual references to 'SPEC-069' stay as a human identifier.

Fixes review finding open-mercato#19 on PR open-mercato#1111.

* docs(spec): log SPEC-069 filename normalization in changelog

* chore(staff): regenerate snapshot and lucide registry after develop rebase

* fix(staff): handle duplicate project code with 409 and validate timeProjectId in bulk save (closes open-mercato#2304)

* fix(staff): i18n timesheets relative time and aria-labels, add seed-timesheets-widgets CLI command (closes open-mercato#2305)

* fix(staff): use wildcard-aware hasFeature for manage_all ACL check (closes open-mercato#2303)

* fix(staff): load ACL via rbacService when JWT lacks features, sort i18n keys alphabetically

* fix(staff): enforce time-entry ownership on writes and emit timer lifecycle events (H-1, H-2)

* fix(staff): timesheets follow-up — M-1..M-4 from review (assign side-effects, indexer plumbing, ref validation, bulk stale-id 422)

---------

Co-authored-by: migsilva89 <migdrum@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant