Primordia
Changelog
51 entries — click to expand
▶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 inpackage.jsonscripts## How it works
Running
bun run deploy-to-exe.dev primordiaconnects via SSH toprimordia.exe.xyzand:1. Copies
.env.local(secrets) from the local machine to the server viascp
2. Installsgitandbunon the server if they are not already present
3. Clones the GitHub repo (or pulls the latestmainif already cloned)
4. Runsbun installto install dependencies
5. Startsbun run devwithHOSTNAME=0.0.0.0so the app is publicly reachable athttp://<server-name>.exe.xyz:3000
6. Waits for the Next.js "Ready" signal and tails the logsBecause 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.localfile.▶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.mdfile. - `.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 inchangelog/.- `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
▶Fix bullet list rendering on evolve page
# Fix bullet list rendering on evolve page
## What changed
Replaced
SimpleMarkdownwithMarkdownContentinEvolveForm.tsxfor rendering progress messages.## Why
SimpleMarkdownis 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 toSimpleMarkdowncaused all bullets to be smooshed onto one line with no separation.MarkdownContentis 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.▶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 recentchangelog/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.tsxwas updated to read thechangelog/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.▶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
ChatInterfaceinto a newAcceptRejectBarcomponent, 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 rendersnull
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 onlycontextstring) for the chat system prompt.
- Modified:app/page.tsx— removedisPreviewInstanceand
previewParentBranchprops (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 ath-dvhmeans the app looks identical
to production at first glance; the bar is discoverable by scrolling down.▶Show submitted request text on progress page
# Show submitted request text on progress page
## What changed
After a user submits a request on the
/evolvepage, 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 | nullstate variable toEvolveForm.tsx. - Set it from the trimmed input in
handleSubmit(before clearing the textarea). - Reset it in
handleResetalongside the other state resets. - Rendered a styled card labeled "Your request" between the description banner and the progress messages, visible only after submission.
- Added a
▶Remove production from deployment message
# Remove "production" from deployment message
## What changed
Updated the post-merge deployment message incomponents/ChatInterface.tsxfrom:> "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.▶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.▶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.
- Removed the sentence "Your request will become a GitHub Issue and trigger an automated PR." from the production subtitle in
▶Move evolve mode to its own page
# Move evolve mode to its own page
## What changed
- Removed the
ModeTogglecomponent ("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 inChatInterface). - 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.
-/evolveis 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.
- Removed the
▶Fix branch name wrapping on small screens
# Fix branch name wrapping on small screens
## What changed
InChatInterface.tsx, the<h1>that displays "Primordia" alongside the current branch name now usesflex-wrapinstead of a single‑row flex layout. The branch name span also getsw-full sm:w-auto, so on small viewports it occupies its own row while onsmand wider screens it stays inline as before.## Why
On narrow screens (e.g. mobile), a long branch name likeclaude/issue-77-20260320-0428caused 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.▶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.tsxto 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.tsto return bothprBaseBranch(base/target branch) andprState(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 (notmain)
- Create the PR targeting that same branch (notmain)This ensures that evolve requests made from a deploy preview stack on the current PR rather than diverging onto
mainwithout the preview's changes.ChatInterface.tsxnow passesdeployPrBranchasparentBranchwhen calling/api/evolvefrom a deploy preview.app/api/evolve/route.tsaccepts the optionalparentBranchfield and embeds branch-targeting git commands in the issue body when the parent branch is notmain.
### 3. Fix merge conflicts
Resolved merge conflicts between this PR and #76 (branch-based PR lookup). Both
prBaseBranch(from #76) andprState(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.
- The description now reads: "Accepting will merge the PR into
▶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
- WhenVERCEL_GIT_PULL_REQUEST_IDis empty (Vercel deployed the branch before a PR was opened) and the branch is notmain/master, the endpoint now searches for an associated PR by branch name using the GitHub API (/pulls?head={owner}:{branch}&state=all).
- A newprStatefield ("open" | "closed" | "merged") is returned in the response alongside the existingprNumberandprUrl.
- TheGitHubPRinterface now includesstateandmerged_atfields.###
components/ChatInterface.tsx
- A newdeployPrStatestate variable tracks the PR's current state.
- The Vercel preview accept/reject bar now behaves differently based onprState:
- `"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_IDis an empty string. Previously, the deploy-context API returnednullin 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.▶Add accept-reject bar for Vercel preview deployments
# Add accept/reject bar for Vercel preview deployments
## What changed
- Added a new/api/close-prroute that closes a PR via the GitHub API (PATCHstate: "closed").
- Replaced the old chat-intent-triggered merge card inChatInterface.tsxwith a persistent accept/reject bar that appears automatically on all Vercel preview deployments (wheneverdeployPrNumberis set).
- Accept Changes → calls/api/merge-prto merge the PR, then shows a confirmation.
- Reject → calls/api/close-prto close the PR, then shows a confirmation.
- The bar collapses into a static status message once an action is taken.
- Removed theisMergeIntenthelper function andshowMergeCardstate, 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.▶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 byclaude-haiku-4-5) with acreateNameId()mnemonic suffix — e.g.add-dark-mode-toggle-ancient-fireant.- Worktrees are created under a shared
../primordia-worktrees/{slug}-{mnemonicId}directory instead of scatteringprimordia-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
PreToolUsehook (makeWorktreeBoundaryHook) to thequery()call inlib/local-evolve-sessions.ts. The hook blocks any tool call whose target resolves outside the session'sworktreePath:- Read / Write / Edit —
file_pathis resolved and checked. - Glob / Grep —
pathis 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.tsxis now a React Server Component that reads the current git branch and full HEAD commit message at request time usingexecSync(falling back to Vercel env vars on Vercel). The values are passed as props (branch,commitMessage) toChatInterface, eliminating the former client-sidefetch("/api/git-context")on mount and removing theapp/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.tsnow:1. Reads the parent branch from
git config branch.<evolveBranch>.parent(stored at session-creation time bystartLocalEvolve).
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): ifgit checkoutfails withalready checked out at '<path>', the reported path is used asmergeRootinstead.app/page.tsxalso readsgit config branch.<name>.parentserver-side and passesisPreviewInstanceandpreviewParentBranchas props toChatInterface, replacing the previous client-sideGET /api/evolve/local/managecall 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.
- Worktrees are created under a shared
▶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 theready
state, the preview URL is now constructed client-side as
${window.location.protocol}//${window.location.hostname}:${port}instead of
the hardcodedhttp://localhost:${port}. TheLocalEvolveSessioninterface
gained aport: number | nullfield to support this.
-lib/local-evolve-sessions.ts: The progress-log message that previously said
Ready at http://localhost:{port}now saysReady 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 oldhttp://localhost:{port}preview link would resolve to the developer's own
machine rather than the remote host. Usingwindow.location.hostnamemakes
the link work correctly regardless of whether the session is truly local or
accessed remotely.▶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.tsthat parses the path
from a failedgit checkoutwhen 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.- Older git:
▶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 tomain, installing dependencies with Bun (frozen lockfile), running the prebuild script, then executingtsc --noEmitandeslint .to enforce type safety and code quality before merge. - Added
eslint.config.mjs: ESLint flat config using@eslint/eslintrcto extend the Next.js core-web-vitals ruleset. - Updated
next.config.ts: settypescript.ignoreBuildErrors: trueandeslint.ignoreDuringBuilds: trueso Vercel production builds skip these checks (they are now enforced in CI instead). - Updated
package.json: added atypecheckscript (tsc --noEmit) and switched frompackage-lock.jsontobun.lockas the canonical lockfile. - Deleted
package-lock.json: replaced bybun.lockfollowing the project's switch to Bun as the default package manager.
## Why
Type-checking and linting during
next buildslows 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 reachmain.- Added
▶Suppress GITHUB_TOKEN warning in local dev
# Suppress GITHUB_TOKEN warning in local dev
## What changed
app/api/check-keys/route.tsnow skips theGITHUB_TOKENandGITHUB_REPOchecks whenNODE_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 needANTHROPIC_API_KEYto use the app locally.▶Set bun as default package manager in package.json
# Set bun as default package manager in package.json
Added
"packageManager": "bun@1.2.0"topackage.json.## What changed
-package.jsonnow declares"packageManager": "bun@1.2.0".## Why
The project already uses bun throughout (scripts invokebun, the local evolve flow spawnsbun run dev, and the previous changelog entry switched from npm/node to bun). DeclaringpackageManagermakes 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.▶Fix mobile viewport height using dvh unit
# Fix mobile viewport height using dvh unit
## What changed
Replacedh-screen(height: 100vh) withh-dvh(height: 100dvh) on the main chat container incomponents/ChatInterface.tsx.## Why
On mobile browsers,100vhis 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, so100dvhalways equals the current visible viewport height. Tailwind CSS v3.4+ shipsh-dvhout of the box.▶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 asgit config branch.<preview-branch>.parent <parent-branch>so the preview server can find where to merge back.
- ExportsrunGitso the manage route can reuse it.
- RemovedacceptSessionandrejectSession— cleanup is now handled by the preview instance itself.- `app/api/evolve/local/manage/route.ts`:
- Removed CORS headers andOPTIONShandler (no longer needed — same-origin only).
- AddedGEThandler that detects preview instances by reading the current branch viagit rev-parse --abbrev-ref HEADand checking whethergit config branch.<name>.parentis set. This is persistent across server restarts and manual dev server invocations — no environment variable required.
- RewrotePOSThandler: uses the same git-based branch detection, locates the parent repo root viagit rev-parse --git-common-dir, reads the parent branch fromgit config branch.<branch>.parent, performs the merge (accept) or skips it (reject), removes the worktree and branch in the parent repo, then callsprocess.exit(0)to shut itself down.- `components/ChatInterface.tsx`:
- RemoveduseSearchParamsimport and allsearchParams/previewSessionId/previewParentOriginstate.
- On mount, fetchesGET /api/evolve/local/manageto detect whether this instance is a preview; setsisPreviewInstanceaccordingly.
-handlePreviewAcceptandhandlePreviewRejectnow POST to the instance's own/api/evolve/local/manage(same origin, nosessionIdorparentOriginneeded).
- Preview URL shown in the parent chat is now the plainhttp://localhost:<port>link (no query params).
- Removed dead-codehandleLocalAcceptandhandleLocalReject.- `app/page.tsx`: Removed
<Suspense>wrapper — it was only needed becauseuseSearchParamsrequired it.
## Why
The previous implementation passed
sessionIdandparentOriginas 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 specificlocalhostport.Using
git configto 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 runningnpm 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.- `app/page.tsx`: Removed
▶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 ofstartLocalEvolve) no longer pre-finds a free port before
spawning Next.js. Instead it:1. Starts
bun run devwithout aPORTenvironment 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 setsession.portandsession.previewUrl.The
isPortAvailableandfindAvailablePorthelper functions, along with theimport * 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 viaPORT=...— 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.▶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.mjsnow additionally generateslib/generated/system-prompt.ts— a TypeScript module that exports the fullSYSTEM_PROMPTstring with PRIMORDIA.md and the last 30 changelog filenames baked in at build time.app/api/chat/route.tsnow importsSYSTEM_PROMPTfrom@/lib/generated/system-promptinstead of reading from the filesystem at runtime.lib/generated/added to.gitignore(build artifact).PRIMORDIA.mdupdated to document the newlib/generated/system-prompt.tsartifact 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.▶Strip thinking spinner img from CI chat messages
# Strip thinking spinner img from CI chat messages
## What changed
Added a
stripThinkingSpinner()helper inapp/api/evolve/status/route.tsthat 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.▶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): extractedSimpleMarkdown(inline renderer) fromChatInterface.tsxinto a shared module. AddedMarkdownContent(block renderer) that splits multi-line markdown into paragraphs and bullet lists, rendering each line withSimpleMarkdownfor inline formatting.
-components/ChatInterface.tsx: removed the inlineSimpleMarkdownfunction definition and imports it from./SimpleMarkdowninstead.
-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.▶Switch from npm and node to bun
# Switch from npm/node to bun
## What changed
-package.json: prebuild/predev scripts now invokebuninstead ofnodeto runscripts/generate-changelog.mjs
-scripts/generate-changelog.mjs: shebang updated from#!/usr/bin/env nodeto#!/usr/bin/env bun; inline comment updated to referencebun run predev
-lib/local-evolve-sessions.ts: the spawned dev server process now usesbun run devinstead ofnpm run dev; comments updated accordingly
-README.md: local dev setup instructions updated fromnpm install/npm run devtobun install/bun run dev
-PRIMORDIA.md: architecture data-flow diagram updated to referencebun run dev
-.gitignore: addedbun-debug.log*andbun-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.▶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: addedextractKeywords(request)helper that strips punctuation, filters words ≤ 3 characters, and takes the first 6 keywords.searchOpenEvolveIssuesnow accepts arequestparameter and appends those keywords to the GitHub search query, narrowing results to issues whose title/body/comments overlap with the user's request. Thesearchaction handler passesbody.requestthrough 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.▶Switch to file-based changelog system
Replaced git-history-based changelog with a file-based system where each change gets its own
.mdfile inchangelog/.Background: A git-history-based changelog was first experimented with (
scripts/generate-changelog.mjsreading fromgit 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.mdfile per change, namedYYYY-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): readschangelog/*.mdfiles. Parses date+time from the filename, uses the remaining filename part as the title, reads the file body as content, sorts newest-first, and writespublic/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: addedprebuildandpredevscripts that rungenerate-changelog.mjsautomatically before every build and local dev start.
-.gitignore: addedpublic/changelog.json— it's a build artifact and should not be committed to git.
-PRIMORDIA.md: updated File Map, Design Principles (new protocol: add achangelog/YYYY-MM-DD-HH-MM-SS Description.mdfile 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.
▶Check for missing API keys on page load
## Check for missing API keys on page load and warn in chat
What changed:
- Newapp/api/check-keys/route.ts: GET endpoint that checksANTHROPIC_API_KEY,GITHUB_TOKEN, andGITHUB_REPOserver-side and returns any that are absent.
-components/ChatInterface.tsx: newuseEffecton 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.
▶Local evolve flow now uses claude-agent-sdk
Replaced the spawned
claudeCLI in the local evolve flow with@anthropic-ai/claude-agent-sdk'squery()for structured, rich progress output.What changed:
-lib/local-evolve-sessions.ts: replacedspawn('claude', ...)withquery()from@anthropic-ai/claude-agent-sdk. The session now stores aprogressTextfield (formatted markdown) instead of rawlogs. As the SDK emitsassistantmessages, text blocks are appended verbatim andtool_useblocks are summarised as- 🔧 Read \path\`style lines. AsummarizeToolUse()` helper handles the common Claude Code tools (Read, Write, Edit, Glob, Grep, Bash, TodoWrite).
-app/api/evolve/local/route.ts: GET endpoint now returnsprogressTextinstead oflogs. Error handler updated to useappendProgress.
-components/ChatInterface.tsx:LocalEvolveSessioninterface renamedlogs→progressText. 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-sdkdependency.Why: Spawning the
claudeCLI 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.▶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 aMap. Contains the full business logic: creates a git worktree at../primordia-preview-{timestamp}, symlinksnode_modulesand.env.local, spawnsclaude --dangerouslySkipPermissions -p "..."as a child process, then startsnpm run devon the next available port ≥ 3001. Also exposesacceptSession(merge + cleanup) andrejectSession(cleanup only).
-app/api/evolve/local/route.ts(new):POSTstarts a session and returns asessionIdimmediately (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 onprocess.env.NODE_ENV === "development"to call the new local flow instead of the GitHub Issues flow. AddslocalEvolveSessionstate, alocalPollingRefinterval (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.
▶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.tsnow exposesVERCEL_PROJECT_PRODUCTION_URLto client components.components/ChatInterface.tsx: the "Primordia" h1 text is now wrapped in an<a>tag (when the production URL is available) pointing tohttps://${VERCEL_PROJECT_PRODUCTION_URL}withtarget="_blank". Styled withtext-white no-underline hover:text-gray-300to 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.
▶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 viasystemContextso 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.
▶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: claudeto the "Run Claude Code" step inevolve.ymland added a new "Create Pull Request" step after it. The new step runs only onissuesevents and only when the action produced abranch_nameoutput. It callsgh pr createwith a title derived from the issue title and a body that closes the originating issue.Why:
anthropics/claude-code-action@v1in interactive mode (triggered by@claudein 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 abranch_nameoutput that the new post-step uses to callgh pr createdirectly, completing the pipeline end-to-end without manual intervention.▶Prevent changelog merge conflicts with union merge strategy
Configured git to use the union merge strategy for
PRIMORDIA.mdto prevent conflicts when multiple PRs prepend changelog entries simultaneously.What changed: Added
.gitattributeswithPRIMORDIA.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. Theunionmerge 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.▶Comment on PR instead of issue when a PR already exists
When a PR already exists for an evolve issue, follow-up
@claudecomments are now posted to the PR instead of the issue.What changed:
-app/api/evolve/route.ts: thecommentaction now checks for an open PR whose branch matchesclaude/issue-{N}-*. If found, the@claudefollow-up comment is posted to the PR instead of the issue. ReturnsprNumber/prUrlin 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.
▶Add README
Created
README.mdfor the repository.What changed: Created
README.mdwith 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 toPRIMORDIA.mdfor 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.
▶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: changedmerge_methodfrom"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.
▶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 usingGITHUB_TOKEN.
-app/api/deploy-context/route.ts: now also returnsprNumberandprUrlalongsidecontextso the client can identify the PR without re-parsing the context string.
-components/ChatInterface.tsx:
- StoresdeployPrNumberfrom the deploy-context response.
- NewisMergeIntent()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.
▶Fix landmark-one-main axe rule
Fixed the
landmark-one-mainaxe rule ("Ensures the document has a main landmark").What changed: No additional code change required beyond the
aria-hidden-focusfix in the same session. Thelandmark-one-mainaxe rule fires when there is no *accessible*<main>landmark — i.e. when the only<main>element is an ancestor witharia-hidden="true". Moving<main>intoChatInterface.tsx(see previous entry) resolves both violations simultaneously.Why: Covered by the
aria-hidden-focusfix — same root cause, same fix.▶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 fromapp/page.tsx(which could receivearia-hidden="true"from Next.js App Router's concurrent rendering / Suspense machinery while still containing focusable elements). Moved the landmark directly ontoChatInterface's root element incomponents/ChatInterface.tsx, changing it from<div>to<main>and addingmx-autoto preserve horizontal centering.Why: The axe
aria-hidden-focusrule fires when an element witharia-hidden="true"contains focusable children. The<main>inpage.tsxwas 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.▶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.)
▶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:
- Newapp/api/deploy-context/route.ts: server-side endpoint that readsVERCEL_GIT_PULL_REQUEST_ID, fetches the PR from GitHub, extracts the linked issue (viaCloses #Nin the PR body), and returns a formatted context string.
-app/api/chat/route.ts: now accepts an optionalsystemContextfield 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, ifVERCEL_ENV === "preview", calls/api/deploy-contextand (a) prepends a visible amber system-message notice to the chat, (b) passes the context assystemContextin every/api/chatcall. 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.
▶Show PR link in header for deploy previews
On Vercel preview deployments, the top header now displays a linked
#Nbadge right after "Primordia", pointing to the GitHub PR for that preview.What changed:
next.config.tsnow exposes four Vercel system env vars (VERCEL_ENV,VERCEL_GIT_PULL_REQUEST_NUMBER,VERCEL_GIT_REPO_OWNER,VERCEL_GIT_REPO_SLUG) via theenvblock, which Next.js inlines at build time so client components can read them.ChatInterface.tsxconditionally renders the link whenVERCEL_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.
▶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_NUMBER→VERCEL_GIT_PULL_REQUEST_IDinnext.config.tsandChatInterface.tsx.Why:
VERCEL_GIT_PULL_REQUEST_NUMBERis not a real Vercel system env var. The correct name isVERCEL_GIT_PULL_REQUEST_ID. Without this fix the PR badge in the header would never render on preview deployments.▶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.tsnow supports three actions:search(find open evolve issues via GitHub Search API),comment(add a@claudefollow-up comment to an existing issue), andcreate(existing behavior, now explicit). Thecommentaction returnsissueNumberso 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.EvolveResulttype 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.
▶Live CI progress in Primordia chat
Added real-time CI progress display inside the Primordia chat.
What changed:
- Newapp/api/evolve/status/route.tsendpoint: given an issue number, fetches (a) the latest Claude bot comment body +updated_at, (b) any open PR whose branch matchesclaude/issue-{N}-*, and (c) a Vercel deploy preview URL from PR comments.
-components/ChatInterface.tsxnow 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.
▶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: changedtext-gray-500totext-gray-400(contrast onbg-gray-950improves from ~4.16:1 to ~7.9:1).
2. Evolve mode toggle button active state: changedbg-amber-600tobg-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.
▶Fix bold text duplication in SimpleMarkdown
Fixed a bug in
SimpleMarkdownwhere 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 innertextcapture. 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.
▶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)
-ModeTogglecomponent for switching between modes
-/api/chatroute: streams Claude responses via SSE
-/api/evolveroute: creates a labeled GitHub Issue via the GitHub API
-evolve.ymlGitHub Actions workflow: triggered by theprimordia-evolvelabel, runs Claude Code CLI, commits changes, opens a PR, and comments on the originating issue
-PRIMORDIA.md: living architecture document and changelogWhy: This is the first version — the foundation everything else evolves from.