feat(observability): PostHog + Langfuse + Sentry integration#7
Draft
twn-lloyd wants to merge 32 commits into
Draft
feat(observability): PostHog + Langfuse + Sentry integration#7twn-lloyd wants to merge 32 commits into
twn-lloyd wants to merge 32 commits into
Conversation
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>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@open-mercato/observabilitypackage registering PostHog, Langfuse, and Sentry as Integration Marketplace providers.host/dsnfields — no code changes required to point at a self-hosted deployment.llmTracerDI token with a noop default.OM_INTEGRATION_*), per-tenant PII scrubbing (keys + 8KB truncation), per-tenant config cache with event-driven invalidation.Spec:
.ai/specs/2026-04-18-observability-integration-posthog-langfuse-sentry.mdPlan:
.ai/plans/2026-04-18-observability-integration-posthog-langfuse-sentry.mdTest plan
@open-mercato/observabilityyarn test:integration— lifecycle + PostHog wiring specs added, need CI run against an ephemeral env)/api/observability/client-configreturns posthog/sentry enabled shape, omits Langfuse secret🤖 Generated with Claude Code