Skip to content

feat: sync personalization from user-level AGENTS.md/CLAUDE.md#3286

Merged
pauldambra merged 19 commits into
mainfrom
posthog-code/sync-personalization-from-agents-md
Jul 10, 2026
Merged

feat: sync personalization from user-level AGENTS.md/CLAUDE.md#3286
pauldambra merged 19 commits into
mainfrom
posthog-code/sync-personalization-from-agents-md

Conversation

@pauldambra

@pauldambra pauldambra commented Jul 8, 2026

Copy link
Copy Markdown
Member
CleanShot 2026-07-08 at 23 12 02@2x

Problem

If you keep personal agent instructions in a user-level AGENTS.md or CLAUDE.md, you have to copy the same content into PostHog Code's Personalization tab and keep the two in sync by hand.

Refs #3251

Changes

Adds a Sync from AGENTS.md / CLAUDE.md toggle above the custom-instructions text box on the Personalization tab.

  • When on, PostHog Code reads the user-level instructions file on start (and whenever the toggle flips): the first non-empty of ~/.agents/AGENTS.md, ~/.codex/AGENTS.md, ~/.claude/AGENTS.md, then ~/.claude/CLAUDE.md — AGENTS.md wins when both exist.
  • Strictly either/or: while sync is on, the synced file is the personalization for every session (local system prompt and cloud task first message) and the hand-typed instructions are ignored — the text box is disabled and greyed out, with a caption naming the file in use. The typed draft is preserved and takes effect again when sync is turned off.
  • If no file is found while sync is on, nothing is synced and a warning says so.
  • The file read lives in workspace-server (os.getUserAgentInstructions, capped at 20k chars), forwarded by the host router, and driven by a settings boot contribution; session schemas now accept the larger synced payload.
  • The tab is split into a pure PersonalizationSettingsView with Storybook stories for each state (Settings/PersonalizationSettings). The Storybook preview theme was also fixed to mirror the app's ThemeWrapper (radius="medium", yellow accent in dark) — it previously rendered square toggles in the wrong accent.

Screenshots (from the Storybook stories)

Sync off (unchanged) Sync on — file found
sync off synced
Sync on — file truncated at 20k Sync on — no file found
truncated no file

How did you test this?

  • New unit tests: OsService.getUserAgentInstructions (precedence, fallback, empty-file skip, truncation) and getEffectiveCustomInstructions (sync on/off, missing/whitespace file).
  • Ran pnpm --filter typecheck for workspace-server, host-router, ui, and the Electron app; Biome check on touched files; node scripts/check-host-boundaries.mjs.
  • Ran existing suites touching the changed seams: sessionServiceHost (143 tests), core sessions + task-detail (319 tests), os service (19), settings store (27) — all green.
  • Rendered every Storybook story in a headless browser and reviewed screenshots of each state (embedded above).

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

@trunk-io

trunk-io Bot commented Jul 8, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

React Doctor found 1 issue in 1 file · 1 warning.

1 warning

src/features/settings/sections/PersonalizationSettings.tsx

Reviewed by React Doctor for commit 502cb41.

@pauldambra pauldambra added the Stamphog This will request an autostamp by stamphog on small changes label Jul 8, 2026
@pauldambra pauldambra marked this pull request as ready for review July 9, 2026 05:33

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zero reviews on a substantive feature that changes the session instruction-passing path, relaxes a server-side schema limit from 2 000 to 20 000 chars, and adds a new persisted settings field — all higher-risk changes that need at least one human approval.

@stamphog stamphog Bot removed the Stamphog This will request an autostamp by stamphog on small changes label Jul 9, 2026
@pauldambra pauldambra requested a review from a team July 9, 2026 05:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 139b406594

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pauldambra pauldambra left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated comment by QA Swarm — not written by a human

QA Swarm review complete. See inline comments.

Comment thread packages/ui/src/features/task-detail/hooks/useTaskCreation.ts
});
}

private async reconcile(enabled: boolean): Promise<void> {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated comment by QA Swarm — not written by a human

[paul] 🟢 LOW

how do we find out when this quietly does nothing? the toggle fires SETTING_CHANGED, nice — but the states that actually matter are invisible: "sync on, no file found" and "truncated a big file". since this changes what gets injected into every session, i wonder if reconcile should emit a small analytics ping on the outcome (found / not-found / truncated, maybe which of the four candidates won). that's what tells us whether people land on AGENTS.md or silently get nothing. the log.warn on read failure keeping the last snapshot is exactly right though — no notes there. not blocking, just a "log before you limit" itch.

Comment thread packages/workspace-server/src/services/os/os.ts
Comment thread packages/workspace-server/src/services/os/os.ts
Comment thread packages/ui/src/features/settings/settingsStore.test.ts
Comment thread packages/workspace-server/src/services/os/os.ts Outdated
Comment thread packages/workspace-server/src/services/os/os.test.ts Outdated
Comment thread packages/ui/src/features/settings/settingsStore.ts

Copy link
Copy Markdown
Member Author

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review: paul-reviewer, xp-reviewer, security-audit (qa-team unavailable in this environment — skipped)

Verdict: 💬 APPROVE WITH NITS

No correctness or security blockers found; two MEDIUM design/observability questions plus a handful of nits, all non-blocking.

Key findings

MEDIUM

  • useTaskCreation.ts — cloud task path sends the new 20k-capable instructions with no client-side length guard; backend serializer cap unconfirmed (paul)
  • os.ts — the 20,000-char cap is asserted in three places (USER_AGENT_INSTRUCTIONS_MAX_LENGTH + two .max(20_000) Zod schemas) with no shared constant (xp)

LOW

  • customInstructionsSync.contribution.ts — the interesting sync outcomes (no file found / truncated) are invisible outside the UI; consider an analytics ping on reconcile outcome (paul)
  • os.test.ts — "prefers an AGENTS.md over the user CLAUDE.md" never exercises the first candidate (~/.agents), so the .agents-before-.codex ordering isn't pinned (xp)
  • settingsStore.ts — the whitespace guard in getEffectiveCustomInstructions re-checks an invariant os.ts already guarantees (xp)

NIT — Windows displayPath slash drift; slice(0, 20_000) can split a surrogate pair; celebration of the parameterised test tables (paul)

Convergence

No two reviewers flagged the same line independently. Adjacent concerns: paul's cloud-cap question and xp's cap-constant triplication both orbit the 20k limit — highest-confidence area to tighten.

Reviewer summaries

Reviewer Assessment
👤 paul approve with caveats — architecture right, tests lovely; confirm the cloud/backend cap and add outcome observability
📐 xp well-factored, respects the layering; dedupe the 20k constant before merge, two small test/guard nits
🛡 security-audit clean — zero-input read of hardcoded home-dir paths over same-principal IPC, cap enforced at read + both session schemas, opt-in gating sound; no findings

Automated by QA Swarm — not a human review

@pauldambra pauldambra left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated comment by QA Swarm — not written by a human

QA Swarm round 2 (delta 139b406..3349cb6): round-1 fixes verified landed; security-audit clean. See inline comments — all LOW/NIT, verdict ✅ APPROVE.

Comment thread packages/ui/src/features/settings/customInstructionsSync.contribution.ts Outdated
Comment thread packages/workspace-server/src/services/os/os.test.ts Outdated

@pauldambra pauldambra left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated comment by QA Swarm — not written by a human

QA Swarm round 3 (delta 3349cb6..1022011): clean — the generation guard, rename, and test folds all verified by all three reviewers; security-audit traced the guard and found no new flow. Only NITs remain. Verdict ✅ APPROVE.

@pauldambra pauldambra added the Stamphog This will request an autostamp by stamphog on small changes label Jul 9, 2026
pauldambra added 17 commits July 9, 2026 08:22
Adds a toggle on the Personalization settings tab that mirrors the user's
user-level agent instructions file into custom instructions, so the same
guidance doesn't have to be maintained in two places.

- workspace-server: os.getUserAgentInstructions() probes ~/.agents/AGENTS.md,
  ~/.codex/AGENTS.md, ~/.claude/AGENTS.md, then ~/.claude/CLAUDE.md and
  returns the first non-empty file (truncated at 20k chars).
- ui: persisted syncCustomInstructionsFromFile setting, runtime synced
  snapshot, and a boot contribution that re-reads the file once settings
  hydrate and whenever the toggle flips.
- Sessions (local system prompt) and cloud task creation both use the synced
  content when available, falling back to the hand-typed instructions.
- Agent session schemas accept the larger synced payload (20k cap, up from
  2k).

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Make the sync semantics strictly either/or: while the toggle is on, only the
synced AGENTS.md/CLAUDE.md content applies (nothing when no file is found)
and the hand-typed instructions are ignored. The personalization textarea is
now disabled and greyed out whenever sync is on, keeping the typed draft
visible but clearly inactive, with a caption naming the file in use or a
warning when none was found.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Split PersonalizationSettings into a pure PersonalizationSettingsView (props
only) and the store-wired container, per the storybook-stories convention, and
add stories for each visual state: editable box (empty and filled), synced
from CLAUDE.md/AGENTS.md with the box disabled, truncated file, and sync on
with no file found. Also drop the disabled box to 50% opacity — Radix's
disabled state alone is too subtle in dark mode to read as inactive.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Temporary assets so the PR description can embed the story states; removed
again in the next commit — the raw URLs stay pinned to this commit's SHA.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
The PR description references them via the previous commit's pinned raw URLs;
the working tree stays clean.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
The preview's Theme used radius="none" and swapped accent colors (orange in
dark, yellow in light), so stories rendered square toggles in the wrong
accent. Mirror ThemeWrapper: radius="medium", yellow in dark, orange in
light.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Temporary assets for the PR description; removed in the next commit — the
raw URLs stay pinned to this commit's SHA.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
React Doctor flagged the store-value-copied-into-state + mirror-effect
pattern (no-derived-state). Replace it with a nullable draft that renders
over the store value only while edits are pending: typing sets the draft,
the debounce (or blur) persists it and releases it back to the store value,
so external store changes render without an effect and can no longer clobber
in-progress typing.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
…oggle re-enable

Re-enabling "sync custom instructions from file" left the previous
snapshot in the store while the new file read was in flight. A session
created in that window (e.g. right after turning sync off, editing the
file, then back on) would inject the stale prior snapshot instead of
nothing. Clear the snapshot synchronously before the async read so the
gap resolves to empty personalization rather than stale content.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
…nstant

The 20,000-char cap was asserted three times with a copy-pasted comment:
OsService's truncation plus the .max(20_000) on both
startSessionInput and reconnectSessionInput. Bumping one without the
others would either truncate silently below the intended length or
reject a synced instructions file that was truncated to fit. Export
USER_AGENT_INSTRUCTIONS_MAX_LENGTH from os/schemas.ts, alongside the
UserAgentInstructions type it bounds, and reference it from all three
sites.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
…tInstructions

The existing "prefers an AGENTS.md over the user CLAUDE.md" case seeded
.codex/AGENTS.md + .claude/CLAUDE.md, so it never exercised the
first candidate (.agents/AGENTS.md) or pinned the .agents-before-.codex
ordering — a reorder of USER_AGENT_INSTRUCTIONS_CANDIDATES swapping
those two entries would still pass. Add a case seeding both so the
ordering is covered.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Dedupe the customInstructions Zod field shared by startSessionInput and
reconnectSessionInput into one const, and flatten the hydrate/toggle
detection in the sync contribution's store subscription.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
After the simplify pass moved the state._hasHydrated guard out of the
expression, `becameHydrated = !prev._hasHydrated` standing alone is
true for any change while un-hydrated — the name only told the truth
inside the combined condition. `wasUnhydrated` is self-contained.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Two reads in flight at once (rapid toggle off/on/off/on) could resolve
out of order, letting the stale read land last and win while sync is
on. A late read resolving after toggle-off could likewise deposit a
latent snapshot. Capture a generation before the await and discard any
read a newer reconcile has superseded. The read is a host round-trip,
so on slower transports than local IPC the window is real.

Covers the contribution with tests: happy path, clear-on-disable, and
both race orderings (verified the race tests fail without the guard).

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
The three getUserAgentInstructions preference tests shared one shape —
givenFiles in, expected winner out — matching the repo convention of
parameterising same-logic cases (as the getEffectiveCustomInstructions
table already does). Coverage is unchanged.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
Correct the flush helper's comment (setTimeout(0) waits a macrotask turn,
not microtasks) and replace hand-rolled promise-capture boilerplate with the
repo's deferred<T>() test idiom. Test-only; the generation-guard pins were
mutation-checked before and after.

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
The "clear first, so a rejected read leaves personalization empty
rather than reviving stale content" decision lived only in a comment.
Seed a leftover snapshot, reject the read, and assert the store ends
null — a refactor reverting to keep-last-snapshot now fails this test
(verified by removing the pre-clear).

Generated-By: PostHog Code
Task-Id: b6359217-b9b8-40bb-a748-308fa450a732
@pauldambra pauldambra force-pushed the posthog-code/sync-personalization-from-agents-md branch from 68cf812 to f1965a5 Compare July 9, 2026 07:22

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This T1d-complex feature touches session instruction passing, raises schema limits 10×, and adds persisted settings — but every review is from an automated bot with no human sign-off. There is also an unresolved MEDIUM concern about the cloud task creation path: synced instructions up to 20,000 chars flow into the cloud task API with no client-side length guard, while the Django backend may still enforce the old 2,000-char limit, producing failures for users with large AGENTS.md files who create cloud tasks.

@stamphog stamphog Bot removed the Stamphog This will request an autostamp by stamphog on small changes label Jul 9, 2026
…s-md

Generated-By: PostHog Code
Task-Id: 7bc77008-3d23-4db7-bcf3-3320960dd522

@tatoalo tatoalo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet

pauldambra commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Merge activity

  • Jul 10, 9:59 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 10, 9:59 AM UTC: @pauldambra merged this pull request with Graphite.

@pauldambra pauldambra merged commit 1743b24 into main Jul 10, 2026
26 checks passed
@pauldambra pauldambra deleted the posthog-code/sync-personalization-from-agents-md branch July 10, 2026 09:59

@richardsolomou richardsolomou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pauldambra roborich got to reviewing this a bit after it merged, but there might be some good follow-ups here

// unsubscribes, so one shared instance serves every test.
let started = false;

beforeEach(() => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could use a test for the boot path here, its the headline behaviour and the suite never runs it. beforeEach pre-sets _hasHydrated: true, so the false -> true transition (the actual production boot order, persist rehydration flips it async after start()) never happens here. delete the if (initial._hasHydrated) block and shrink the subscribe guard to just toggleFlipped and all 5 tests stay green - but anyone with sync persisted-on gets no file read at startup until they re-toggle. a test that starts the contribution unhydrated with sync on, flips hydration true, and asserts the read fires would pin it.

const state = useSettingsStore.getState();
return {
...state,
customInstructions: getEffectiveCustomInstructions(state),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing asserts synced content actually reaches session start - both sessionServiceHost test files mock the store without the new fields, so getEffectiveCustomInstructions only ever takes its pass-through branch here (same for the cloud path at packages/ui/src/features/task-detail/hooks/useTaskCreation.ts:333). reverting this line to state.customInstructions kills the feature for every local session and passes green. worth an assert that sync on + a snapshot lands the file content in the startSession payload?

);
});

describe("getEffectiveCustomInstructions", () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be worth pinning that syncedCustomInstructions stays out of partialize (settingsStore.ts:478 has the toggle, correctly not the snapshot)? nothing asserts it today, so someone adding it later passes green - and then a stale snapshot rehydrates at boot and can hit a session created before the contribution's clear-and-re-read finishes, exactly the stale-snapshot case the contribution documents. one test covering toggle-persisted + snapshot-not-persisted would close it.

pauldambra added a commit that referenced this pull request Jul 10, 2026
Follow-up to #3286. Close three test-coverage gaps where the AGENTS.md/CLAUDE.md
sync feature could silently break while the suite stayed green:

- customInstructionsSync.contribution: assert the file read fires on the
  unhydrated -> hydrated boot transition when sync is persisted-on (the headline
  boot path the suite never exercised).
- sessionServiceHost: give the store mock the sync fields and assert the synced
  file content (not the hand-typed instructions) reaches the agent.start payload.
- settingsStore: assert the sync toggle persists but the runtime snapshot stays
  out of partialize, so a stale snapshot can't rehydrate.

Each test was mutation-checked to confirm it fails on the corresponding regression.

Generated-By: PostHog Code
Task-Id: 989ab6ef-4d6c-4e95-912c-105f9e842b31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants