Primordia

Changelog

← Back to app

51 entries — click to expand

  1. Add deploy-to-exe.dev command

    # Add deploy-to-exe.dev command

    ## What changed

    Added bun run deploy-to-exe.dev <server-name> command that deploys Primordia to an exe.dev server in a single step.

    New files:
    - scripts/deploy-to-exe-dev.sh — the deploy script
    - New "deploy-to-exe.dev" entry in package.json scripts

    ## How it works

    Running bun run deploy-to-exe.dev primordia connects via SSH to primordia.exe.xyz and:

    1. Copies .env.local (secrets) from the local machine to the server via scp
    2. Installs git and bun on the server if they are not already present
    3. Clones the GitHub repo (or pulls the latest main if already cloned)
    4. Runs bun install to install dependencies
    5. Starts bun run dev with HOSTNAME=0.0.0.0 so the app is publicly reachable at http://<server-name>.exe.xyz:3000
    6. Waits for the Next.js "Ready" signal and tails the logs

    Because the server runs next dev (NODE_ENV=development), the fast local evolve flow is active — change requests are handled directly by the Claude Agent SDK + git worktrees on the remote machine, with no GitHub Issues / Actions / Vercel round-trip required.

    ## Why

    The GitHub → Actions → Vercel pipeline works but is slow (minutes per change). The local dev flow (Claude Agent SDK + git worktrees) is much faster but requires installing git, bun, and Claude Code locally. Deploying the dev server to an exe.dev instance gives users the speed of the local flow with zero local prerequisites — just SSH access and a .env.local file.

  2. Fix evolve prompt changelog instructions to use changelog directory

    # Fix evolve prompt changelog instructions to use changelog directory

    ## What changed

    Updated the changelog instructions in both evolve LLM prompts to align with the file-based changelog system described in PRIMORDIA.md:

    • `lib/local-evolve-sessions.ts`: Replaced step 1 ("Update the Changelog section of PRIMORDIA.md with a brief entry") with correct instructions to create a new changelog/YYYY-MM-DD-HH-MM-SS Description.md file.
    • `.github/workflows/evolve.yml`: Replaced the system-prompt instruction to "update its Changelog section" with the correct instruction to create a new file in changelog/.

    ## Why

    PRIMORDIA.md's Changelog section explicitly states that changelog entries must be stored exclusively in changelog/ — never in PRIMORDIA.md itself. The old prompts contradicted this, which would cause Claude to write changelog entries directly into PRIMORDIA.md instead of creating the correct timestamped files in changelog/.

  3. Fix bullet list rendering on evolve page

    # Fix bullet list rendering on evolve page

    ## What changed

    Replaced SimpleMarkdown with MarkdownContent in EvolveForm.tsx for rendering progress messages.

    ## Why

    SimpleMarkdown is an inline renderer designed for a single line of text — it has no concept of paragraphs or bullet lists. When Claude Code emits progress text containing bullet lists (e.g. - item 1\n- item 2\n- item 3), passing that multi-line string to SimpleMarkdown caused all bullets to be smooshed onto one line with no separation.

    MarkdownContent is the block-level renderer that correctly splits content on blank lines into paragraphs, detects bullet list items, and renders them as <ul>/<li> elements — exactly what was needed here.

  4. Show most recent changelog entry instead of git commit

    # Show most recent changelog entry instead of git commit

    The "Most recent change:" message shown in the chat on startup now displays the
    most recent changelog/ entry (title + full body) instead of the raw git commit
    message. This gives users a human-readable summary of what changed rather than
    an internal commit log line.

    app/page.tsx was updated to read the changelog/ directory, sort files
    lexicographically (which equals chronological order given the filename convention),
    pick the newest file, and pass its title and body as the initial chat message.

  5. Move accept-reject bar below the app layout

    # Move accept/reject bar below the app layout

    ## What changed

    Extracted the accept/reject changes widget from ChatInterface into a new
    AcceptRejectBar component, and moved it into the root layout (app/layout.tsx)
    so it appears on every page.

    The bar now sits below the 100dvh main app content. On preview builds
    (local worktree or Vercel deploy preview), users can scroll down to reveal
    the accept/reject controls. On production builds the bar renders null
    and has no effect.

    ### Files changed

    - New: components/AcceptRejectBar.tsx — standalone client component
    containing all accept/reject state and handlers for both local and Vercel
    previews.
    - Modified: app/layout.tsx — added git preview detection (runGit /
    isPreviewInstance) and renders <AcceptRejectBar> after {children}.
    - Modified: components/ChatInterface.tsx — removed accept/reject bars,
    state (previewActionState, vercelActionState, deployPrNumber,
    deployPrBaseBranch, deployPrState), and handlers. Kept the deploy-context
    fetch (simplified to only context string) for the chat system prompt.
    - Modified: app/page.tsx — removed isPreviewInstance and
    previewParentBranch props (now handled in layout).

    ## Why

    The accept/reject widget was previously only visible on the / chat page.
    Moving it to the root layout makes it accessible from any page (e.g. /evolve,
    /changelog). Keeping the main layout at h-dvh means the app looks identical
    to production at first glance; the bar is discoverable by scrolling down.

  6. Show submitted request text on progress page

    # Show submitted request text on progress page

    ## What changed

    After a user submits a request on the /evolve page, their original request text is now displayed in a card at the top of the progress tracking area.

    ## Why

    Once the form is submitted and transitions to the progress view, the textarea disappears and the user had no way to see what they had typed. This was confusing — especially if the CI pipeline takes minutes to run and the user has forgotten the exact wording. The new "Your request" card gives them immediate confirmation of what was submitted and keeps it visible throughout the progress polling.

    ## How

    • Added a submittedRequest: string | null state variable to EvolveForm.tsx.
    • Set it from the trimmed input in handleSubmit (before clearing the textarea).
    • Reset it in handleReset alongside the other state resets.
    • Rendered a styled card labeled "Your request" between the description banner and the progress messages, visible only after submission.
  7. Remove production from deployment message

    # Remove "production" from deployment message

    ## What changed
    Updated the post-merge deployment message in components/ChatInterface.tsx from:

    > "The changes will be deployed to production shortly."

    to:

    > "The changes will be deployed shortly."

    ## Why
    The word "production" is inaccurate in the context of deploy previews, where merging a PR doesn't necessarily mean deploying to production. Removing it makes the message more universally correct.

  8. Remove inaccurate production deployment message

    # Remove inaccurate "Production deployment is on its way!" message

    ## What changed

    Removed the phrase "Production deployment is on its way!" from the accepted-changes confirmation message in components/ChatInterface.tsx.

    ## Why

    The message was misleading. It claimed a production deployment was happening whenever a PR was merged, but that's only true if the target branch is main. In a hyperforkable app like Primordia where branches can be anything, the notion of "production" is ambiguous. The simpler message — "✅ Changes accepted and merged into {branch}." — is accurate in all cases.

  9. Remove evolve form subtitle about GitHub Issue

    # Remove evolve form subtitle about GitHub Issue

    ## What changed

    • Removed the sentence "Your request will become a GitHub Issue and trigger an automated PR." from the production subtitle in components/EvolveForm.tsx.

    ## Why

    The subtitle text was implementation-detail noise that didn't add value for users submitting requests. Removing it keeps the evolve form clean and focused on the user's action ("Describe a change you want to make to this app.") without exposing how the pipeline works internally.

  10. Move evolve mode to its own page

    # Move evolve mode to its own page

    ## What changed

    • Removed the ModeToggle component ("Chat" / "Evolve" toggle buttons in the header).
    • Replaced the toggle with a small pencil (Edit) icon button in the chat header that links to /evolve.
    • Created app/evolve/page.tsx — a dedicated Next.js route for submitting evolve requests.
    • Created components/EvolveForm.tsx — a new "submit a request" form component containing all evolve-specific logic (formerly embedded in ChatInterface).
    • Simplified ChatInterface.tsx — now handles chat only; all evolve state, handlers, and UI have been removed.

    ## Why

    The previous design treated Chat and Evolve as two modes of the same interface, toggled by a button in the header. This was conceptually muddled — they serve very different purposes. Chat is the main app; Evolve is a utility for proposing code changes to the app itself.

    The new design makes the distinction clear:
    - / (Chat) is the landing page and primary experience.
    - /evolve is a separate "submit a request" form, reached by clicking the pencil icon.

    This mirrors familiar patterns (e.g., GitHub's "New Issue" form is separate from the issues list), reduces clutter in the chat header, and makes the evolve flow feel intentional rather than accidental.

  11. Fix branch name wrapping on small screens

    # Fix branch name wrapping on small screens

    ## What changed
    In ChatInterface.tsx, the <h1> that displays "Primordia" alongside the current branch name now uses flex-wrap instead of a single‑row flex layout. The branch name span also gets w-full sm:w-auto, so on small viewports it occupies its own row while on sm and wider screens it stays inline as before.

    ## Why
    On narrow screens (e.g. mobile), a long branch name like claude/issue-77-20260320-0428 caused the heading row to overflow horizontally, pushing the Chat/Evolve mode‑toggle buttons off the right edge of the screen and requiring horizontal scrolling to reach them. Wrapping the branch name to its own row on small screens keeps the toggle always visible without affecting the desktop layout.

  12. Match deploy preview accept-reject language to local dev

    # Match deploy preview accept/reject language to local dev + fix issue creation branch targeting

    ## What changed

    ### 1. Match accept/reject bar language to local dev

    Updated the Vercel deploy preview accept/reject bar in ChatInterface.tsx to use the same language as the local development preview bar, explicitly showing the name of the branch the PR will be merged into.

    • The description now reads: "Accepting will merge the PR into {baseBranch}. Rejecting will close the PR." — matching the local dev bar's pattern of "Accepting will merge the preview branch into {previewParentBranch}."
    • The accepted confirmation message now reads: "✅ Changes accepted and merged into {baseBranch}." — matching the local dev accepted message.
    • Added PR state display (merged/closed indicators) for when a PR was already merged or closed.
    • Updated deploy-context/route.ts to return both prBaseBranch (base/target branch) and prState (open/closed/merged) in the API response.
    • prBranch (the head branch being previewed) is also returned so it can be passed when creating evolve issues.

    ### 2. Fix evolve issue creation to target the correct branch

    When creating an evolve issue from a deploy preview, the issue body now includes instructions for Claude to:
    - Base changes on the deploy preview's branch (not main)
    - Create the PR targeting that same branch (not main)

    This ensures that evolve requests made from a deploy preview stack on the current PR rather than diverging onto main without the preview's changes.

    • ChatInterface.tsx now passes deployPrBranch as parentBranch when calling /api/evolve from a deploy preview.
    • app/api/evolve/route.ts accepts the optional parentBranch field and embeds branch-targeting git commands in the issue body when the parent branch is not main.

    ### 3. Fix merge conflicts

    Resolved merge conflicts between this PR and #76 (branch-based PR lookup). Both prBaseBranch (from #76) and prState (from this PR) are now present in the response.

    ## Why

    • The local dev preview bar already showed the target branch name; aligning the Vercel bar makes the two flows feel coherent.
    • Issues created from deploy previews were always targeting main, which meant CI would make changes without the preview's context. Changes should be stacked on the branch being reviewed.
  13. Fall back to branch-based PR lookup and show PR state in deploy preview bar

    # Fall back to branch-based PR lookup and show PR state in deploy preview bar

    ## What changed

    ### app/api/deploy-context/route.ts
    - When VERCEL_GIT_PULL_REQUEST_ID is empty (Vercel deployed the branch before a PR was opened) and the branch is not main/master, the endpoint now searches for an associated PR by branch name using the GitHub API (/pulls?head={owner}:{branch}&state=all).
    - A new prState field ("open" | "closed" | "merged") is returned in the response alongside the existing prNumber and prUrl.
    - The GitHubPR interface now includes state and merged_at fields.

    ### components/ChatInterface.tsx
    - A new deployPrState state variable tracks the PR's current state.
    - The Vercel preview accept/reject bar now behaves differently based on prState:
    - `"merged"`: Shows a notice that the PR is already merged (no accept/reject buttons).
    - `"closed"`: Shows a notice that the PR was already closed/discarded (no accept/reject buttons).
    - `"open"` or unknown: Shows the normal accept/reject buttons as before.

    ## Why

    When Vercel creates a deployment for a branch push that happens before a PR is made, VERCEL_GIT_PULL_REQUEST_ID is an empty string. Previously, the deploy-context API returned null in this case, so the preview bar and PR context were never shown. This fix ensures the PR is still found (if one was later opened) and the UI correctly reflects the PR's current state — avoiding presenting accept/reject actions when the PR is already resolved.

  14. Add accept-reject bar for Vercel preview deployments

    # Add accept/reject bar for Vercel preview deployments

    ## What changed
    - Added a new /api/close-pr route that closes a PR via the GitHub API (PATCH state: "closed").
    - Replaced the old chat-intent-triggered merge card in ChatInterface.tsx with a persistent accept/reject bar that appears automatically on all Vercel preview deployments (whenever deployPrNumber is set).
    - Accept Changes → calls /api/merge-pr to merge the PR, then shows a confirmation.
    - Reject → calls /api/close-pr to close the PR, then shows a confirmation.
    - The bar collapses into a static status message once an action is taken.
    - Removed the isMergeIntent helper function and showMergeCard state, which were only needed for the old chat-triggered flow.

    ## Why
    Previously, users on a Vercel preview deployment had to type "merge" or "accept" in the chat to trigger a merge card — and there was no reject/close option at all. The new bar gives reviewers a clear, always-visible way to either ship or discard a PR directly from the preview URL, matching the UX of the local dev preview flow.

  15. Polish local evolve sessions

    # Polish local evolve sessions

    ## What changed

    ### Human-readable worktree and branch names

    Installed mnemonic-id (v4.1.0). Local evolve sessions now generate names by combining a Claude-generated kebab-case slug (3–5 words summarising the change request, produced by claude-haiku-4-5) with a createNameId() mnemonic suffix — e.g. add-dark-mode-toggle-ancient-fireant.

    • Worktrees are created under a shared ../primordia-worktrees/{slug}-{mnemonicId} directory instead of scattering primordia-preview-* folders in the parent.
    • Branch names follow evolve/{slug}-{mnemonicId}, consistent with the CI convention.
    • A first-5-words fallback is retained so slug generation never blocks session creation if the API call fails.

    ### Worktree sandboxing

    Added a PreToolUse hook (makeWorktreeBoundaryHook) to the query() call in lib/local-evolve-sessions.ts. The hook blocks any tool call whose target resolves outside the session's worktreePath:

    • Read / Write / Editfile_path is resolved and checked.
    • Glob / Greppath is checked when it is an absolute path.
    • Bash — commands referencing the main repo root path are blocked, preventing git -C /main/repo …-style escapes.

    ### Git context in UI via server component

    app/page.tsx is now a React Server Component that reads the current git branch and full HEAD commit message at request time using execSync (falling back to Vercel env vars on Vercel). The values are passed as props (branch, commitMessage) to ChatInterface, eliminating the former client-side fetch("/api/git-context") on mount and removing the app/api/git-context/ route entirely.

    • The browser tab title reads Primordia (branch-name).
    • The h1 header shows the branch name in muted gray.
    • On load, an assistant message shows "Most recent change: {full commit message}" (changed from the previous "Ok, here's what's changed:" phrasing, and now showing the full commit body rather than just the subject line).

    ### Correct parent branch in accept/reject flow

    The accept action in app/api/evolve/local/manage/route.ts now:

    1. Reads the parent branch from git config branch.<evolveBranch>.parent (stored at session-creation time by startLocalEvolve).
    2. Checks out that parent branch in the main repo before merging, so the merge always lands on the originating branch rather than whatever happened to be checked out.
    3. Handles the case where the parent branch is already checked out in another worktree (stacked evolve sessions): if git checkout fails with already checked out at '<path>', the reported path is used as mergeRoot instead.

    app/page.tsx also reads git config branch.<name>.parent server-side and passes isPreviewInstance and previewParentBranch as props to ChatInterface, replacing the previous client-side GET /api/evolve/local/manage call on every mount. The accept/reject bar now shows the actual parent branch name (e.g. feature/my-branch) in both the description and the post-accept success message.

    ### Welcome message cleanup

    Removed the sentence "Your idea will be turned into a GitHub PR automatically." from the welcome message — it was an implementation detail of the production pipeline that was confusing in local dev where no PR is created.

    ## Why

    The old local evolve flow had accumulated several rough edges:

    • Opaque timestamp-based names made worktree paths unreadable and cluttered the parent directory.
    • The Claude agent running inside a worktree could still use absolute paths to escape to the main checkout — in at least one observed case it committed directly to the main branch.
    • Git context (branch, commit) was fetched client-side via an extra HTTP round-trip on every page load, even though it's static for the lifetime of a local server.
    • Accepting a change merged into whichever branch happened to be checked out in the main repo, not necessarily the branch the session was forked from.
    • Stacked evolve sessions (where the parent is itself an evolve/… branch checked out in another worktree) caused the accept action to fail with a git error.
    • The accept/reject bar always said "merge into main" regardless of the real parent branch.
  16. Use dynamic hostname for local dev preview URL

    # Use dynamic hostname for local dev preview URL

    ## What changed

    - components/EvolveForm.tsx: When a local evolve session reaches the ready
    state, the preview URL is now constructed client-side as
    ${window.location.protocol}//${window.location.hostname}:${port} instead of
    the hardcoded http://localhost:${port}. The LocalEvolveSession interface
    gained a port: number | null field to support this.
    - lib/local-evolve-sessions.ts: The progress-log message that previously said
    Ready at http://localhost:{port} now says Ready on port {port} to avoid
    embedding a localhost URL in text that could be copied from a remote machine.

    ## Why

    When Primordia's dev server runs on a remote machine (e.g. primordia.exe.xyz)
    and the developer accesses it via a forwarded or public hostname, the old
    http://localhost:{port} preview link would resolve to the developer's own
    machine rather than the remote host. Using window.location.hostname makes
    the link work correctly regardless of whether the session is truly local or
    accessed remotely.

  17. Fix git worktree error message regex for newer git versions

    # Fix git worktree error message regex for newer git versions

    ## What changed

    Updated the regex in app/api/evolve/local/manage/route.ts that parses the path
    from a failed git checkout when the target branch is already checked out in
    another worktree.

    Before:
    ```
    /already checked out at '([^']+)'/
    ```

    After:
    ```
    /(?:already checked out at|already used by worktree at) '([^']+)'/
    ```

    ## Why

    Different versions of git emit slightly different error messages for this
    condition:

    • Older git: fatal: 'branch' is already checked out at '/path/to/worktree'
    • Newer git: fatal: 'branch' is already used by worktree at '/path/to/worktree'

    The original regex only matched the older form. When running a newer git version
    the regex would fail to match, causing the accept action to return a 500 error
    instead of gracefully falling back to running the merge in the correct existing
    worktree.

    The fix uses a non-capturing alternation `(?:already checked out at|already used
    by worktree at)` so both message variants are handled, while still capturing the
    worktree path in group 1.

  18. Add CI workflow for type-checking and linting

    # Add CI workflow for type-checking and linting

    ## What changed

    • Added .github/workflows/ci.yml: a GitHub Actions workflow that runs on every pull request to main, installing dependencies with Bun (frozen lockfile), running the prebuild script, then executing tsc --noEmit and eslint . to enforce type safety and code quality before merge.
    • Added eslint.config.mjs: ESLint flat config using @eslint/eslintrc to extend the Next.js core-web-vitals ruleset.
    • Updated next.config.ts: set typescript.ignoreBuildErrors: true and eslint.ignoreDuringBuilds: true so Vercel production builds skip these checks (they are now enforced in CI instead).
    • Updated package.json: added a typecheck script (tsc --noEmit) and switched from package-lock.json to bun.lock as the canonical lockfile.
    • Deleted package-lock.json: replaced by bun.lock following the project's switch to Bun as the default package manager.

    ## Why

    Type-checking and linting during next build slows down every Vercel deployment. Moving these checks into a dedicated CI workflow means Vercel builds stay fast while code quality is still enforced on every PR before it can reach main.

  19. Suppress GITHUB_TOKEN warning in local dev

    # Suppress GITHUB_TOKEN warning in local dev

    ## What changed
    app/api/check-keys/route.ts now skips the GITHUB_TOKEN and GITHUB_REPO checks when NODE_ENV === "development".

    ## Why
    In local development the evolve pipeline runs entirely via git worktrees and the @anthropic-ai/claude-agent-sdk — it never touches the GitHub API. Showing a "missing GITHUB_TOKEN" warning in that context is misleading and unnecessary noise for local developers who only need ANTHROPIC_API_KEY to use the app locally.

  20. Set bun as default package manager in package.json

    # Set bun as default package manager in package.json

    Added "packageManager": "bun@1.2.0" to package.json.

    ## What changed
    - package.json now declares "packageManager": "bun@1.2.0".

    ## Why
    The project already uses bun throughout (scripts invoke bun, the local evolve flow spawns bun run dev, and the previous changelog entry switched from npm/node to bun). Declaring packageManager makes this explicit and machine-readable: Node.js Corepack can enforce the correct package manager version, and tooling (editors, CI, Vercel) can detect bun automatically without extra configuration.

  21. Fix mobile viewport height using dvh unit

    # Fix mobile viewport height using dvh unit

    ## What changed
    Replaced h-screen (height: 100vh) with h-dvh (height: 100dvh) on the main chat container in components/ChatInterface.tsx.

    ## Why
    On mobile browsers, 100vh is calculated based on the full viewport height without browser chrome (address bar, on-screen keyboard). When the address bar is visible or the virtual keyboard is open, the actual visible area is smaller, causing the layout to overflow or clip incorrectly.

    The CSS dvh (dynamic viewport height) unit adjusts dynamically as the browser UI appears and disappears, so 100dvh always equals the current visible viewport height. Tailwind CSS v3.4+ ships h-dvh out of the box.

  22. Show accept-reject in preview instance not parent chat

    # Show accept/reject in preview instance, not parent chat

    ## What changed

    In local development evolve mode, the Accept / Reject UI now appears inside
    the newly-created preview instance (the child Next.js dev server) instead of in
    the parent chat. The preview instance manages its own accept/reject flow entirely
    via git config — no cross-origin requests or URL params are required.

    ### Specific changes

    - `lib/local-evolve-sessions.ts`:
    - On worktree creation, records the current branch as git config branch.<preview-branch>.parent <parent-branch> so the preview server can find where to merge back.
    - Exports runGit so the manage route can reuse it.
    - Removed acceptSession and rejectSession — cleanup is now handled by the preview instance itself.

    - `app/api/evolve/local/manage/route.ts`:
    - Removed CORS headers and OPTIONS handler (no longer needed — same-origin only).
    - Added GET handler that detects preview instances by reading the current branch via git rev-parse --abbrev-ref HEAD and checking whether git config branch.<name>.parent is set. This is persistent across server restarts and manual dev server invocations — no environment variable required.
    - Rewrote POST handler: uses the same git-based branch detection, locates the parent repo root via git rev-parse --git-common-dir, reads the parent branch from git config branch.<branch>.parent, performs the merge (accept) or skips it (reject), removes the worktree and branch in the parent repo, then calls process.exit(0) to shut itself down.

    - `components/ChatInterface.tsx`:
    - Removed useSearchParams import and all searchParams / previewSessionId / previewParentOrigin state.
    - On mount, fetches GET /api/evolve/local/manage to detect whether this instance is a preview; sets isPreviewInstance accordingly.
    - handlePreviewAccept and handlePreviewReject now POST to the instance's own /api/evolve/local/manage (same origin, no sessionId or parentOrigin needed).
    - Preview URL shown in the parent chat is now the plain http://localhost:<port> link (no query params).
    - Removed dead-code handleLocalAccept and handleLocalReject.

    • `app/page.tsx`: Removed <Suspense> wrapper — it was only needed because useSearchParams required it.

    ## Why

    The previous implementation passed sessionId and parentOrigin as URL query
    params on the preview link, and the preview instance used them to POST
    cross-origin to the parent server. This required CORS headers and the Accept /
    Reject handlers to reach back out to a specific localhost port.

    Using git config to store the parent branch name lets the preview instance
    handle its own lifecycle: it merges into the parent branch (discovered from git
    config), removes its own worktree and branch via the parent repo, and exits —
    all without needing to know the parent server's address.

    The initial implementation detected preview instances via a PREVIEW_BRANCH
    environment variable injected at spawn time. This was fragile: the env var
    would not survive a server restart or a user manually running npm run dev
    inside the worktree. Replacing it with a git-based check (`git rev-parse
    --abbrev-ref HEAD + git config branch.<name>.parent`) makes detection fully
    persistent, since git config is stored on disk and survives any restart.

  23. Parse Next.js output to determine dev server port

    # Parse Next.js output to determine dev server port

    ## What changed

    In the local evolve flow (lib/local-evolve-sessions.ts), the dev server startup
    logic (Step 5 of startLocalEvolve) no longer pre-finds a free port before
    spawning Next.js. Instead it:

    1. Starts bun run dev without a PORT environment variable, letting Next.js
    choose its own port (defaulting to 3000, or auto-incrementing if 3000 is busy).
    2. Parses the spawned process's stdout/stderr for two patterns:
    - localhost:(\d+) — the "Local:" URL Next.js prints at startup
    - using available port (\d+) instead — the warning when the default port is
    taken by another process
    3. Uses the parsed port number to set session.port and session.previewUrl.

    The isPortAvailable and findAvailablePort helper functions, along with the
    import * as net from 'net' dependency, were removed as they are no longer needed.

    ## Why

    The old approach had a race condition: we'd check whether a port was free, then
    pass it to Next.js via PORT=... — but the port could be claimed by another
    process in the brief window between our check and Next.js's bind. Letting Next.js
    handle port selection and simply reading its output is more reliable and also
    handles the common case where the main dev server (port 3000) is already running.

  24. Inject PRIMORDIA.md into chat system prompt statically at build time

    # Inject PRIMORDIA.md into chat system prompt statically at build time

    ## What changed

    • scripts/generate-changelog.mjs now additionally generates lib/generated/system-prompt.ts — a TypeScript module that exports the full SYSTEM_PROMPT string with PRIMORDIA.md and the last 30 changelog filenames baked in at build time.
    • app/api/chat/route.ts now imports SYSTEM_PROMPT from @/lib/generated/system-prompt instead of reading from the filesystem at runtime.
    • lib/generated/ added to .gitignore (build artifact).
    • PRIMORDIA.md updated to document the new lib/generated/system-prompt.ts artifact and clarify the build-time generation flow.

    ## Why

    In chat mode, Primordia was prone to hallucination when users asked about its own architecture — it had no grounding information about itself. By baking PRIMORDIA.md and the last 30 changelog filenames into the system prompt at build time (via scripts/generate-changelog.mjs), the assistant can answer accurately about how the app works, what technologies it uses, and what has been changed, without inventing details. Using a static import (instead of reading files at runtime) means the prompt is bundled into the Next.js route at build time with no filesystem access needed at runtime.

  25. Strip thinking spinner img from CI chat messages

    # Strip thinking spinner img from CI chat messages

    ## What changed

    Added a stripThinkingSpinner() helper in app/api/evolve/status/route.ts that removes the Claude Code "thinking" spinner <img> tag from GitHub comment bodies before they are returned to the chat UI.

    The spinner is a 14×14 px inline image (e.g. <img src="https://github.com/user-attachments/assets/..." width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />) that Claude Code appends to its GitHub comment while it is working. It is designed for GitHub's rendered markdown view, not for Primordia's plain-text chat display, where it shows up as a noisy broken-image or raw HTML string.

    ## Why

    The spinner cluttered the CI progress display in the chat interface. Stripping it server-side (in status/route.ts) keeps the fix in one place and requires no changes to the frontend rendering logic.

  26. Use markdown renderer on changelog page

    The changelog page previously rendered entry content as raw preformatted text (<pre>). It now renders it with the existing markdown renderer so bold text, inline code, links, and bullet lists are properly formatted.

    What changed:
    - components/SimpleMarkdown.tsx (new): extracted SimpleMarkdown (inline renderer) from ChatInterface.tsx into a shared module. Added MarkdownContent (block renderer) that splits multi-line markdown into paragraphs and bullet lists, rendering each line with SimpleMarkdown for inline formatting.
    - components/ChatInterface.tsx: removed the inline SimpleMarkdown function definition and imports it from ./SimpleMarkdown instead.
    - app/changelog/page.tsx: replaced <pre> with <MarkdownContent> for entry bodies, so changelog content is rendered with proper markdown formatting.

    Why: Changelog entries are written in markdown (bold labels like **What changed**:, inline code like ` file.tsx `, bullet lists) and were being displayed as raw text. Using the existing renderer improves readability without adding any new dependencies.

  27. Switch from npm and node to bun

    # Switch from npm/node to bun

    ## What changed
    - package.json: prebuild/predev scripts now invoke bun instead of node to run scripts/generate-changelog.mjs
    - scripts/generate-changelog.mjs: shebang updated from #!/usr/bin/env node to #!/usr/bin/env bun; inline comment updated to reference bun run predev
    - lib/local-evolve-sessions.ts: the spawned dev server process now uses bun run dev instead of npm run dev; comments updated accordingly
    - README.md: local dev setup instructions updated from npm install / npm run dev to bun install / bun run dev
    - PRIMORDIA.md: architecture data-flow diagram updated to reference bun run dev
    - .gitignore: added bun-debug.log* and bun-error.log* alongside the existing npm/yarn debug log ignores

    ## Why
    Bun is significantly faster than npm for both installs and script execution, reducing cold-start times for local evolve sessions and CI runs.

  28. Filter evolve issue search to related issues only

    Filter open evolve issue search to only return issues related to the current request, and auto-create a new issue when none match.

    What changed:
    - app/api/evolve/route.ts: added extractKeywords(request) helper that strips punctuation, filters words ≤ 3 characters, and takes the first 6 keywords. searchOpenEvolveIssues now accepts a request parameter and appends those keywords to the GitHub search query, narrowing results to issues whose title/body/comments overlap with the user's request. The search action handler passes body.request through to the function.

    Why: Previously, searching for open evolve issues returned every open [Primordia Evolve] issue, flooding the decision card with unrelated entries. With keyword-based filtering, only genuinely related issues are returned. When no related issues exist the frontend already falls through to auto-creating a new issue — so the decision prompt is now suppressed for truly new requests.

  29. Switch to file-based changelog system

    Replaced git-history-based changelog with a file-based system where each change gets its own .md file in changelog/.

    Background: A git-history-based changelog was first experimented with (scripts/generate-changelog.mjs reading from git log), including a workaround for Vercel's shallow clone / no-remote environment. However, git commit messages are too terse to capture the level of detail of the original PRIMORDIA.md entries. The file-based approach was chosen instead.

    What changed:
    - changelog/ directory (new): contains one .md file per change, named YYYY-MM-DD-HH-MM-SS Description of change.md. The filename provides a short description (useful for context management — agents can read the directory listing to get an overview, then open individual files for detail). The file body contains the full rich description.
    - scripts/generate-changelog.mjs (new): reads changelog/*.md files. Parses date+time from the filename, uses the remaining filename part as the title, reads the file body as content, sorts newest-first, and writes public/changelog.json.
    - app/changelog/page.tsx (new): renders each entry as a <details>/<summary> disclosure widget. The summary shows the date and title; expanding it reveals the full markdown content rendered as preformatted text.
    - components/ChatInterface.tsx: added a "Changelog" link in the subtitle below the "Primordia" heading.
    - package.json: added prebuild and predev scripts that run generate-changelog.mjs automatically before every build and local dev start.
    - .gitignore: added public/changelog.json — it's a build artifact and should not be committed to git.
    - PRIMORDIA.md: updated File Map, Design Principles (new protocol: add a changelog/YYYY-MM-DD-HH-MM-SS Description.md file instead of prepending to the Changelog section), and this entry.

    Why: File-based entries preserve the rich "what + why" descriptions, avoid any git-history depth issues, work identically in all environments (local/CI/Vercel), and use filenames as natural short descriptions for AI context management.

  30. Check for missing API keys on page load

    ## Check for missing API keys on page load and warn in chat

    What changed:
    - New app/api/check-keys/route.ts: GET endpoint that checks ANTHROPIC_API_KEY, GITHUB_TOKEN, and GITHUB_REPO server-side and returns any that are absent.
    - components/ChatInterface.tsx: new useEffect on mount calls /api/check-keys; if any keys are missing, a system message is prepended to the chat listing the missing variables and which features they affect.

    Why: Users who deploy without setting all required environment variables get no feedback on why chat or evolve mode fails. The on-load check surfaces the problem immediately with a clear message instead of leaving them to debug silently failing API calls.

  31. Local evolve flow now uses claude-agent-sdk

    Replaced the spawned claude CLI in the local evolve flow with @anthropic-ai/claude-agent-sdk's query() for structured, rich progress output.

    What changed:
    - lib/local-evolve-sessions.ts: replaced spawn('claude', ...) with query() from @anthropic-ai/claude-agent-sdk. The session now stores a progressText field (formatted markdown) instead of raw logs. As the SDK emits assistant messages, text blocks are appended verbatim and tool_use blocks are summarised as - 🔧 Read \path\` style lines. A summarizeToolUse()` helper handles the common Claude Code tools (Read, Write, Edit, Glob, Grep, Bash, TodoWrite).
    - app/api/evolve/local/route.ts: GET endpoint now returns progressText instead of logs. Error handler updated to use appendProgress.
    - components/ChatInterface.tsx: LocalEvolveSession interface renamed logsprogressText. The polling handler now renders **Local Evolve Progress**:\n\n{progressText} — the same pattern as **CI Progress** ...\n\n{body} used by the GitHub/CI flow, so both paths have a consistent progress display style.
    - package.json: added @anthropic-ai/claude-agent-sdk dependency.

    Why: Spawning the claude CLI produced unstructured stdout that was truncated to 20 lines. Using the SDK gives structured message events (text blocks, tool-use blocks, result), enabling a formatted progress display that mirrors what the GitHub CI comment shows — checklist setup steps, then Claude's live commentary and tool calls, then a ✅ finish line.

  32. Local development evolve flow bypass GitHub entirely

    Added a local development evolve flow that creates a git worktree preview without touching GitHub.

    What changed:
    - lib/local-evolve-sessions.ts (new): module-level singleton that holds all active local evolve sessions in a Map. Contains the full business logic: creates a git worktree at ../primordia-preview-{timestamp}, symlinks node_modules and .env.local, spawns claude --dangerouslySkipPermissions -p "..." as a child process, then starts npm run dev on the next available port ≥ 3001. Also exposes acceptSession (merge + cleanup) and rejectSession (cleanup only).
    - app/api/evolve/local/route.ts (new): POST starts a session and returns a sessionId immediately (fire-and-forget); GET ?sessionId=... returns { status, logs, port, previewUrl } for client polling.
    - app/api/evolve/local/manage/route.ts (new): POST { action: "accept"|"reject", sessionId } — accept merges the preview branch into main and kills the dev server; reject just cleans up.
    - components/ChatInterface.tsx: in evolve mode, branches on process.env.NODE_ENV === "development" to call the new local flow instead of the GitHub Issues flow. Adds localEvolveSession state, a localPollingRef interval (5 s), handleLocalEvolveSubmit, handleLocalAccept, handleLocalReject, an updated evolve-mode banner, and an accept/reject card that appears when the preview server is ready. The existing GitHub flow is unchanged.
    - PRIMORDIA.md: updated File Map and Data Flow sections.

    Why: When iterating locally, creating a GitHub Issue → waiting for CI → waiting for a Vercel deploy is slow. The new flow lets a developer see changes in a local preview server within minutes and accept/reject without touching GitHub.

  33. Make Primordia header text a link to the production app

    The "Primordia" heading on deploy previews is now a clickable link that navigates to the production app.

    What changed: next.config.ts now exposes VERCEL_PROJECT_PRODUCTION_URL to client components. components/ChatInterface.tsx: the "Primordia" h1 text is now wrapped in an <a> tag (when the production URL is available) pointing to https://${VERCEL_PROJECT_PRODUCTION_URL} with target="_blank". Styled with text-white no-underline hover:text-gray-300 to preserve the same appearance as the plain text.

    Why: Gives users on deployment previews a one-click way to jump to the production app without the link looking like an obvious URL or button.

  34. Simplify deploy preview banner

    Simplified the deploy preview banner to hide PR details from the visible notice while keeping them in the system context for Claude.

    What changed: components/ChatInterface.tsx: the visible system message shown at the top of the chat on deploy previews now always displays only "⚠️ This is a deploy preview — a work-in-progress build, not the production app." The full PR/issue context string is still sent to Claude via systemContext so the assistant remains aware of it.

    Why: The PR title and branch name in the banner were noisy and not useful to end users; the brief warning is sufficient.

  35. Fix evolve workflow to auto-create PR instead of showing a Create PR link

    Updated the evolve GitHub Actions workflow to automatically open a pull request after Claude Code finishes, instead of posting a "Create PR" link.

    What changed: Added id: claude to the "Run Claude Code" step in evolve.yml and added a new "Create Pull Request" step after it. The new step runs only on issues events and only when the action produced a branch_name output. It calls gh pr create with a title derived from the issue title and a body that closes the originating issue.

    Why: anthropics/claude-code-action@v1 in interactive mode (triggered by @claude in an issue) creates a branch, commits changes, and pushes — but then posts a "Create PR" link in the issue comment rather than opening the PR automatically. The action exposes a branch_name output that the new post-step uses to call gh pr create directly, completing the pipeline end-to-end without manual intervention.

  36. Prevent changelog merge conflicts with union merge strategy

    Configured git to use the union merge strategy for PRIMORDIA.md to prevent conflicts when multiple PRs prepend changelog entries simultaneously.

    What changed: Added .gitattributes with PRIMORDIA.md merge=union.

    Why: Every PR that follows the "prepend a changelog entry" convention inserts new text at the same line in PRIMORDIA.md. When two PRs are open simultaneously, git sees two insertions at the same position and cannot auto-resolve them, producing a merge conflict on every merge. The union merge driver tells git to accept all lines from both sides without conflicting. Since each changelog entry is unique text, the merged result is always correct and ordering stays newest-first.

  37. Comment on PR instead of issue when a PR already exists

    When a PR already exists for an evolve issue, follow-up @claude comments are now posted to the PR instead of the issue.

    What changed:
    - app/api/evolve/route.ts: the comment action now checks for an open PR whose branch matches claude/issue-{N}-*. If found, the @claude follow-up comment is posted to the PR instead of the issue. Returns prNumber/prUrl in the response so the frontend can surface the right link.
    - app/api/evolve/status/route.ts: when a matching PR is found and its comments are fetched (for the Vercel preview URL), also check for Claude's progress comment there — it will now live on the PR when the follow-up was posted to the PR.
    - components/ChatInterface.tsx: confirmation message now says "comment on PR #N" vs "comment on Issue #N" depending on where the comment landed.

    Why: When a PR already exists for an evolve issue, the active work is happening on that PR's branch. Commenting directly on the PR is clearer for the reviewer and means Claude Code's response appears right where the code changes are.

  38. Add README

    Created README.md for the repository.

    What changed: Created README.md with a project overview, how-it-works explanation for both chat and evolve modes, tech stack table, step-by-step setup instructions, environment variable reference, and a link to PRIMORDIA.md for deeper architecture details.

    Why: The repo had no README, making it hard for new users to understand what Primordia is or how to set it up.

  39. Switch Accept Changes merge from squash to regular merge commit

    Changed the PR merge strategy from squash to regular merge commit.

    What changed: app/api/merge-pr/route.ts: changed merge_method from "squash" to "merge" in the GitHub API call.

    Why: User requested regular merge commits instead of squash merges to preserve individual commit history from the PR branch.

  40. Accept Changes card for deploy-preview merge

    Added an "Accept Changes" card to the chat interface on deploy previews, allowing users to merge the PR directly from the chat.

    What changed:
    - app/api/merge-pr/route.ts (new): POST endpoint that merges a PR via the GitHub API using GITHUB_TOKEN.
    - app/api/deploy-context/route.ts: now also returns prNumber and prUrl alongside context so the client can identify the PR without re-parsing the context string.
    - components/ChatInterface.tsx:
    - Stores deployPrNumber from the deploy-context response.
    - New isMergeIntent() helper detects phrasing like "merge this branch", "accept this change", "ship this", etc.
    - When a merge-intent message is submitted on a deploy preview, a green "Accept Changes" card is shown above the input (styled like the related-issues decision card). It displays the PR number and a "Cancel" option.
    - Clicking "Accept Changes" calls /api/merge-pr, then appends a confirmation (or error) message to the chat.

    Why: On deploy previews users sometimes want to merge the PR right from the chat rather than switching to GitHub. The card-response pattern (same as related issues) gives a clear, safe confirmation step before an irreversible action.

  41. Fix landmark-one-main axe rule

    Fixed the landmark-one-main axe rule ("Ensures the document has a main landmark").

    What changed: No additional code change required beyond the aria-hidden-focus fix in the same session. The landmark-one-main axe rule fires when there is no *accessible* <main> landmark — i.e. when the only <main> element is an ancestor with aria-hidden="true". Moving <main> into ChatInterface.tsx (see previous entry) resolves both violations simultaneously.

    Why: Covered by the aria-hidden-focus fix — same root cause, same fix.

  42. Fix aria-hidden-focus accessibility violation on main

    Fixed an accessibility violation where the <main> landmark was hidden from assistive technology while containing focusable elements.

    What changed: Removed the <main> wrapper from app/page.tsx (which could receive aria-hidden="true" from Next.js App Router's concurrent rendering / Suspense machinery while still containing focusable elements). Moved the landmark directly onto ChatInterface's root element in components/ChatInterface.tsx, changing it from <div> to <main> and adding mx-auto to preserve horizontal centering.

    Why: The axe aria-hidden-focus rule fires when an element with aria-hidden="true" contains focusable children. The <main> in page.tsx was the flagged target. By owning the <main> element inside the component that controls its content, we eliminate the detached wrapper that Next.js could transiently hide from AT while form elements remained keyboard-reachable.

  43. Search for existing evolve issues and live CI progress for follow-ups

    Search for existing evolve issues before creating new ones, and show live CI progress for follow-up comments.

    (Combined with the "Check for related open issues" change — covers the status polling that surfaces Claude's live task-list for follow-up comments on existing issues, using the same 10-second polling loop as new issues.)

  44. Deploy previews are now self-aware

    Deploy previews now load their own PR and linked-issue context into the chat, making the assistant aware it is running on a work-in-progress build.

    What changed:
    - New app/api/deploy-context/route.ts: server-side endpoint that reads VERCEL_GIT_PULL_REQUEST_ID, fetches the PR from GitHub, extracts the linked issue (via Closes #N in the PR body), and returns a formatted context string.
    - app/api/chat/route.ts: now accepts an optional systemContext field in the POST body and appends it to the hardcoded system prompt, so Claude is aware of the WIP context.
    - components/ChatInterface.tsx: on mount, if VERCEL_ENV === "preview", calls /api/deploy-context and (a) prepends a visible amber system-message notice to the chat, (b) passes the context as systemContext in every /api/chat call. System messages render as a distinct amber notice bar rather than a chat bubble.

    Why: Deploy previews are live but unmerged works-in-progress. Loading the PR and linked issue into the chat makes the assistant aware it's running on a preview build, and gives users immediate visibility into which PR/issue the preview corresponds to.

  45. Show PR link in header for deploy previews

    On Vercel preview deployments, the top header now displays a linked #N badge right after "Primordia", pointing to the GitHub PR for that preview.

    What changed: next.config.ts now exposes four Vercel system env vars (VERCEL_ENV, VERCEL_GIT_PULL_REQUEST_NUMBER, VERCEL_GIT_REPO_OWNER, VERCEL_GIT_REPO_SLUG) via the env block, which Next.js inlines at build time so client components can read them. ChatInterface.tsx conditionally renders the link when VERCEL_ENV === "preview" and a PR number is present. Production deployments are unaffected.

    Why: Makes it easy to identify which PR each preview tab corresponds to.

  46. Fix Vercel env var name for PR ID

    Fixed incorrect Vercel environment variable name for the PR ID.

    What changed: Renamed VERCEL_GIT_PULL_REQUEST_NUMBERVERCEL_GIT_PULL_REQUEST_ID in next.config.ts and ChatInterface.tsx.

    Why: VERCEL_GIT_PULL_REQUEST_NUMBER is not a real Vercel system env var. The correct name is VERCEL_GIT_PULL_REQUEST_ID. Without this fix the PR badge in the header would never render on preview deployments.

  47. Check for related open issues before creating a new evolve request

    Search for existing open evolve issues before creating a new one, and allow posting follow-up comments on them.

    What changed:
    - /api/evolve/route.ts now supports three actions: search (find open evolve issues via GitHub Search API), comment (add a @claude follow-up comment to an existing issue), and create (existing behavior, now explicit). The comment action returns issueNumber so the frontend can start CI polling.
    - components/ChatInterface.tsx: on evolve submit, the app searches for open evolve issues first. If any are found, a decision card lists them with an "Add comment" button per issue and a "Create new issue instead" fallback. After posting a comment on an existing issue, the same live CI-progress polling starts (identical to the new-issue path), so users see Claude's task-list updating in real time. EvolveResult type updated to support both "created" and "commented" outcomes.

    Why: Avoid unnecessary issue/branch proliferation; follow-up requests should continue on the existing branch. The live comment display was already present for new issues — now it works for follow-ups too.

  48. Live CI progress in Primordia chat

    Added real-time CI progress display inside the Primordia chat.

    What changed:
    - New app/api/evolve/status/route.ts endpoint: given an issue number, fetches (a) the latest Claude bot comment body + updated_at, (b) any open PR whose branch matches claude/issue-{N}-*, and (c) a Vercel deploy preview URL from PR comments.
    - components/ChatInterface.tsx now starts polling that endpoint every 10 s after a successful evolve submit. A dedicated "CI Progress" message is added to the chat and updated in-place every time Claude's comment changes on GitHub, so users see the bot's live task-list as it ticks off items. Separate one-time messages are appended when the PR is created and when the deploy preview URL becomes available. Polling stops automatically on deploy preview or after 15 minutes.

    Why: Claude's GitHub comment is continuously edited as CI progresses; showing only a one-time 400-char snapshot missed all the live updates. The new approach mirrors the comment in real time directly in the Primordia chat.

  49. Fix WCAG 2 AA color contrast issues

    Improved color contrast for two elements that failed the WCAG 2 AA 4.5:1 minimum ratio for normal text.

    What changed:
    1. <p>A self-evolving application</p> subtitle: changed text-gray-500 to text-gray-400 (contrast on bg-gray-950 improves from ~4.16:1 to ~7.9:1).
    2. Evolve mode toggle button active state: changed bg-amber-600 to bg-amber-700 (contrast for white text improves from ~3.19:1 to ~5.0:1).

    Why: Both elements failed the WCAG 2 AA threshold of 4.5:1 for normal (non-large) text, flagged by an accessibility audit.

  50. Fix bold text duplication in SimpleMarkdown

    Fixed a bug in SimpleMarkdown where bold text (**text**) was rendered twice — once as a <strong> element and once as a plain <span>.

    What changed: Fixed the split regex in SimpleMarkdown. String.split() with a regex that has capturing groups includes the captured sub-groups in the result array. The split regex had inner capturing groups (e.g., ([^*]+) inside \*\*([^*]+)\*\*), so for each bold token the array contained both the full **text** match and the inner text capture. The fix converts all inner groups to non-capturing ((?:...)) so only the full token appears in the split result.

    Why: Bold text was visually duplicated in the chat UI.

  51. Initial Scaffold

    Built the entire initial scaffold from scratch.

    Included:
    - Next.js 15 app with TypeScript and Tailwind CSS
    - Two-mode chat interface: "chat" (talks to Claude) and "evolve" (opens a GitHub Issue)
    - ModeToggle component for switching between modes
    - /api/chat route: streams Claude responses via SSE
    - /api/evolve route: creates a labeled GitHub Issue via the GitHub API
    - evolve.yml GitHub Actions workflow: triggered by the primordia-evolve label, runs Claude Code CLI, commits changes, opens a PR, and comments on the originating issue
    - PRIMORDIA.md: living architecture document and changelog

    Why: This is the first version — the foundation everything else evolves from.