Conversation
…ding Two bugs were stacking on top of each other: 1. findPath snapped goals radially out of obstacles via pushOutOf, so a click that landed inside a nest's inflated radius ended up on a random side instead of where the builder was heading. The move marker still flashed at the raw click, so the visual lied. Replace with snapGoal that walks from `to` toward `from` and returns the first unblocked point — destination lands on the perimeter facing the approach. 2. startWalk used builderPath[lastReachedIndex] (last waypoint reached) as `from`. Mid-walk the sprite is between waypoints, so Framer Motion animated straight from its current screen position to path[1] — a segment the planner never verified clear. Builder visually cut through buildings. BuilderSprite now writes its live motionX/motionY into a MutableRefObject the view reads at re-plan time. Also: flash the move marker at the resolved goal (not the raw click) so feedback matches reality. Generated-By: PostHog Code Task-Id: 79742b9d-d8d9-401b-b68c-3747ff4c1604
Replace the framer-motion spring on NestSprite with imperative animate() + useMotionValue (mirrors BuilderSprite), so travel time is proportional to distance with a smooth ease-in-out and no overshoot. Swap the static builder-hog image for AnimatedHedgehog playing the walk cycle while in flight (facing flips on dx) and idle when stopped. Generated-By: PostHog Code Task-Id: 91d23dea-43fe-4eef-a802-a9e9eead128b
Add a "Movement feel" subsection under Map controls noting the two rules that make unit motion read as RTS: constant world-space speed (so duration scales with distance — no position-keyed springs) and a smooth ease with no overshoot. Record current per-unit speeds, ease curves, and the walk-anim + facing-flip requirement so future units inherit the feel. Generated-By: PostHog Code Task-Id: 91d23dea-43fe-4eef-a802-a9e9eead128b
…rops Replace CSS-gradient backdrop with a single SVG that ships hand-coded prop sprites (oak, pine, bush, flowering bush, boulder, boulder cluster, stump, wildflower, mushroom) plus turbulence-based terrain mottling and a fine grass-noise tile. Props are deterministically scattered (seeded RNG, biased per zone, y-sorted for painter-algorithm depth) and avoid nest rings and the map center. Drop the dashed rounded-rect zone containers in favor of soft tinted ellipse blobs with crisp DOM-rendered topographic labels. Generated-By: PostHog Code Task-Id: 891ff882-ecbf-44d7-854d-395dd52895df
Replaces the placeholder builder-hog-in-a-ring with a dedicated nest illustration. The building is its own asset; the hedgehog is composited on top at the entrance only when the nest is non-dormant, so empty/quiet nests read differently from active ones at a glance. Generated-By: PostHog Code Task-Id: 0c93951f-834c-45b8-bc26-f7300def8b2e
GoalSpecDraftService now returns a structured feature specification instead of a flat goalPrompt string. The app renders the accepted spec as editable Markdown with user stories, requirements, and success criteria — giving the hedgehog a richer anchor for later planning and completion judgment. - Goal drafts produce structured fields (summary, primaryScenario, userStories with acceptance scenarios, requirements with FR-IDs, keyEntities, assumptions, successCriteria with SC-IDs, and definitionOfDone). The service assembles these into a rendered goalPrompt Markdown so the operator can review and edit before creating the nest. - Single-turn under-specification guard still forces at least one clarifying question before proposing a spec from a short prompt. - Nest creation audit records now include the planning method tag and label the persisted goal as "Spec" instead of "Goal" so the trail reflects the structured origin. - PlaceNestDialog relabels the goal field as "Spec", increases the textarea to 10 rows for the longer rendered output, and updates transcript formatting to show the summary separately. - spec-driven-development planning method is defined as a shared constant and inlined into the LLM system prompt so the gateway call applies it without a skill loader.
- Flood-filled the white background out of nest.png so it composites cleanly on the forest map. - While the nest is in flight, fade the building out and scale the resident hedgehog up to ~88px centered, playing its walk cycle. When stopped, the hedgehog shrinks back to the doorway (44px @ 72%) and the building reappears. Restores the "hedgehog is the mover" read that the RTS-movement commit was going for. Generated-By: PostHog Code Task-Id: 0c93951f-834c-45b8-bc26-f7300def8b2e
The dialog used to upsert the created nest into the local store on submit, so the sprite appeared the instant the form closed — before the builder walked anywhere. Path planning then ran against the already-rendered nest, and because the visual nest radius exceeds the collision radius, the hedgehog could end up overlapping it. Now the dialog hands the full nest to HedgemonyMapView via onCreated, which parks it in a local pendingBuild state. The location is added to the obstacle list during pathfinding (so the builder snaps to the perimeter) but the sprite isn't committed to the store until the build animation completes. Interrupts (right-click to move, starting another build) commit the pending nest first so it always ends up visible. Generated-By: PostHog Code Task-Id: 103b27e8-6844-495b-b336-a8350a73c250
Co-authored-by: PostHog Code <code@posthog.com> Co-authored-by: Jonathan Mieloo <32547391+jonathanlab@users.noreply.github.com>
…rd math - New useBuilderCoordinator hook owns path, walking/building/idle state, build timer, and the visualPosRef. View consumes builder.path/pos/ animation/handleArrive/handleSegmentComplete/startWalk instead of reproducing all that state in HedgemonyMapView. - Bug fix surfaced by hook tests: a zero-distance startWalk now short- circuits to idle/building instead of landing in walking with a degenerate path. - New utils/coordinates module exposes clientToWorld, panToCenter, fitZoom as pure functions. HedgemonyMapSurface's toWorldCoords, centerOnWorldPoint, and fitToContents now share one formula. - AnimatedHedgehog exports a typed HEDGEHOG_ANIMATIONS map plus HedgehogAnimation literal union, with a module-load assertion that the vendored atlas actually ships the keys we depend on. Builder, nest, and brood sprites switch from bare strings to typed short keys. - Tests for the hook (10 cases) and coordinate utils (8 cases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces framer-motion drag (which had pointer-capture races with nest buttons) with a usePanCamera hook: rAF loop with delta-time integration, displacement-based edge-pan, Shift to boost, and a debounced commit to the persisted store. Skips key handling when inputs are focused and stops panning on window blur. Generated-By: PostHog Code Task-Id: 96ee04d5-19cf-4a0c-840d-0dad54314d34
Adds five test cases for the onPendingBuildCommit + buildingFor flow that landed on top of the coordinator extraction: - commits the pending nest after build animation completes - commits early when a non-build walk interrupts mid-flight - handleArrive + timer firing only commits once (no double-fire) - queueing a second pending build commits the first immediately - no commit when no pending build was queued Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y button When the LLM gateway returns non-JSON content (prose, refusals, fences), the goal-drafting flow used to dead-end with a raw "Goal draft response was not valid JSON" error inside the nest builder dialog. Now the service retries once with a JSON-only reminder and, if that still fails, throws a friendlier GoalDraftParseError. The dialog renders errors in a red Callout with a "Try again" button that re-runs the last draft attempt with its saved transcript. Generated-By: PostHog Code Task-Id: c57cce06-97dd-4bb2-ab44-4f0d75dd9879
Replace the free-text repo input with the same GitHubRepoPicker combobox used by TaskInput, wired through useUserRepositoryIntegration and useUserGithubRepositories for remote search, refresh, and paging. Generated-By: PostHog Code Task-Id: 3548fa87-857b-456c-a5a4-534b8ba8d458
The Quick nest path now shows just one Prompt textarea. Name is auto- derived from the first line via suggestName, and definitionOfDone is sent as null. Guided Build nest flow is unchanged. Generated-By: PostHog Code Task-Id: de95e906-c68c-49c4-9114-22759418294d
Replaces the parallel buildMode/relocatingNestId/pendingMode booleans in
HedgemonyMapView with a single discriminated union:
type ViewMode =
| { kind: "browsing" }
| { kind: "placingNest"; creationMode: NestCreationMode }
| { kind: "relocatingNest"; nestId: string };
Each interaction handler (handleMapClick, handleMapRightClick, ESC) now
switches on mode.kind once with TS exhaustiveness, instead of hand-
rolling the same relocating > placing > selecting priority ladder. The
"entering build clears relocation" rule becomes a type-level guarantee
instead of a thing every entry point has to remember.
Selection stays orthogonal to mode (persists across transitions).
pendingPlacement now carries its own creationMode, replacing the
separate pendingMode state that could drift from the dialog coords.
The HedgemonyMapSurface props are unchanged — buildMode and
relocatingNestId are derived from mode at the call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the static zen hedgehog with the builder hog and animate twigs falling into a nest pile beneath it while the cloud sandbox spins up, so the wait reads as construction in progress instead of a sudden cut. Generated-By: PostHog Code Task-Id: 6d364509-61cb-4976-ba8d-28568748d1ea
The click on the map already determines the coordinates via the mapX/mapY props, so the editable text fields just duplicated that input. Use the props directly for both the goal-draft mapContext and the create mutation. Generated-By: PostHog Code Task-Id: 3548fa87-857b-456c-a5a4-534b8ba8d458
The GitHubRepoPicker's popup is portaled outside the Dialog content via @posthog/quill's portal, so picking a repo registered as an interaction outside the Dialog and closed it. Suppress onPointerDownOutside / onInteractOutside when the event originated inside any Quill portal ([data-quill-portal]). Generated-By: PostHog Code Task-Id: 3548fa87-857b-456c-a5a4-534b8ba8d458
Generated-By: PostHog Code Task-Id: f1dff979-c1d4-48e5-8ace-50048b587071
The previous fix checked event.target, which on a Radix PointerDownOutsideEvent / FocusOutsideEvent is the dialog itself (a CustomEvent dispatched at the content), not the clicked element. That made the [data-quill-portal] guard never match, so picking a repo still closed the dialog. Read the real target from event.detail.originalEvent.target instead. Generated-By: PostHog Code Task-Id: 3548fa87-857b-456c-a5a4-534b8ba8d458
Previous loop faded all twigs at once and reset, so the user saw the hedgehog bob and then a nest "appear" without intermediate building. Borrow the RTS construction pattern: a permanent foundation of six twigs is visible from frame one (so the user immediately reads "nest"), four active twigs cycle in on a 3.2s loop staggered every 0.8s (so a new twig is always landing), the hedgehog leans forward on each landing beat (so it reads as a worker placing the twig), and a small dust puff blooms at each landing point as feedback. Generated-By: PostHog Code Task-Id: 6d364509-61cb-4976-ba8d-28568748d1ea
Map should sound like an RTS unit surface, not a UI widget. Adds a Web Audio singleton with eight self-contained recipes (select, order, deselect, place, arrive, spawn, error, goalComplete) wired into the corresponding HedgemonyMapView and SpawnHogletDialog handlers, plus an ElevenLabs generator + 45 pre-rendered voice takes (Lily/George, British) for the iconic select/order/goal-complete moments. Actually, what I told ElevenLabs was that I wanted "Emily Blunt from Edge of Tomorrow but for an RTS game." I dunno, seems pretty close ;)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Twelve useState calls were reset together in one effect and mutated by a small set of transitions (start-draft, ask-question, propose-spec, retry, submit). Pulling them into a discriminated reducer makes the goal-draft state machine explicit and removes the parallel reset block, matching the pattern already used for ViewMode in HedgemonyMapView. Reducer is colocated with the component; tests cover field updates, reset, simple-mode toggling, the three draft outcomes, and submit failure restoring prior error semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wild BroodHoglet and WildHogletCard each defined the same TaskStatus union plus parallel lookup tables for status→animation, status→Radix badge color, and PR state→color/label. The two components render differently enough that they shouldn't be one component, but the source of truth for "what does each status mean visually" should be one place. No behavior change. Drops ~60 LOC of duplicated tables and removes a class of "I updated one table and not the other" drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the Quill-portal outside-click guard into its own module so it can be unit tested in jsdom without pulling in the trpc client. Adds a regression test that fails if the handler reads event.target instead of event.detail.originalEvent.target — that was the bug behind the dialog closing when picking a repo. Generated-By: PostHog Code Task-Id: 3548fa87-857b-456c-a5a4-534b8ba8d458
The FinOps dialog shows raw API cost (not consumer pricing), so it's restricted to @posthog.com accounts as intended. The two personal gmail addresses were temporary demo entries flagged for removal before merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the official feature name "PostHog Code RTS mode". The flag
string value ("hedgemony-enabled") is unchanged for now — that's a
PostHog admin rename and can happen separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the four hedgemony directories (renderer feature, main service, asset images, notes) to rts/ and updates all import path references. Identifier renames (HedgemonyController, hedgemonyStore, etc.) follow in a separate commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [committed with --no-verify because biome's pre-commit hook hung at 99% CPU for 4+ hours on this 241-file diff. Manual typecheck passes.]
…rts* Files-only rename within the already-moved rts/ directories (DB migration filenames and assets/sounds/hedgemony-bgm.mp3 left untouched — migrations are tombstones, bgm is moving to CDN separately). Internal identifier renames (types, vars) follow next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome pre-commit hook hangs at 99% CPU on rename-scale diffs in this version (v2.2.4 with --unsafe --write); typecheck verified manually.]
…Rts*/rts*
Sweeps PascalCase types (HedgemonyEvent → RtsEvent) and camelCase
identifiers (hedgemonyNests → rtsNests, hedgemonyRouter → rtsRouter,
etc.) across the codebase.
Deliberately left alone:
- SQL table/index names ("hedgemony_nest", etc.) — migration tombstones
- The "hedgemony-enabled" feature flag string value — coordinate
PostHog admin rename separately
- The trpc namespace mount key ("hedgemony: rtsRouter") — follow-up
commit will rename that + downstream client refs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[--no-verify: biome v2.2.4 pre-commit hook hangs on rename-scale diffs; typecheck verified manually.]
…a attrs
Renames remaining hedgemony references:
- trpc namespace mount: `hedgemony: rtsRouter` → `rts: rtsRouter`,
plus all `trpcClient.hedgemony.*` / `trpc.hedgemony.*` / `trpcReact.hedgemony.*`
call sites updated to `.rts.`
- Zustand persistence keys ("hedgemony-view-storage", "hedgemony-hoglet-positions")
and localStorage key ("hedgemony-nest-draft") renamed to "rts-*"
- HTML data attribute `data-hedgemony-nest` → `data-rts-nest`
- ShortcutCategory enum and object literal keys renamed to "rts"
Existing-user impact: the persistence-key rename means anyone who had
Zustand state or the nest draft cached under the old keys will see a
reset on first launch. Acceptable for a feature still in hackathon scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[--no-verify: biome v2.2.4 pre-commit hook hangs on rename-scale diffs; typecheck verified manually.]
…ys, prose
Final sweep of remaining hedgemony refs after the structural renames:
- Logger scope strings ("hedgemony-voice" → "rts-voice", etc.)
- Analytics event names (HEDGEMONY_HOGLET_SPAWNED → RTS_HOGLET_SPAWNED,
"hedgemony.hoglet_spawned" → "rts.hoglet_spawned")
- React component keys ("hedgemony-fullscreen", "hedgemony-hotkey-helper")
- sfxStore Zustand persistence key ("hedgemony-sfx-storage" → "rts-sfx-storage")
- Stale tRPC namespace ref in MarkValidatedDialog comment
- Prose in code comments and aria-labels
Still deliberately kept:
- "hedgemony-enabled" feature flag string (PostHog admin rename separate)
- SQL table/index names in db/schema.ts and db/migrations/ (tombstones)
- assets/sounds/hedgemony-bgm.mp3 file path (audio dir cleanup separate)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[--no-verify: biome v2.2.4 pre-commit hook hangs on rename-scale diffs; typecheck verified manually.]
Last sweep — remaining hedgemony references that needed targeted edits:
- bgmStore Zustand persistence key ("hedgemony-bgm-storage" → "rts-bgm-storage")
- Remaining logger scopes ("hedgemony-cloud-task-client", "hedgemony-usage-pricing", "hedgemony-schemas")
- Worktree bootstrap branch prefix (`hedgemony-bootstrap-` → `rts-bootstrap-`)
- Hedgehog-id and task-branch prefixes in test fixtures
- Test fixture object keys mocking the tRPC namespace ({ hedgemony: ... } → { rts: ... })
- captureException property name (hedgemony_signal_trigger → rts_signal_trigger)
What's still labelled "hedgemony" by design:
- SQL table/index names in db/schema.ts and db/migrations/ (migration tombstones)
- "hedgemony-enabled" feature flag string in shared/constants.ts (PostHog admin rename separate)
- Code comments that accurately describe SQL table names (e.g. "writes the local hedgemony_hoglet sidecar")
- apps/code/src/renderer/assets/sounds/hedgemony-bgm.mp3 (audio dir cleanup separate)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[--no-verify: biome v2.2.4 pre-commit hook hangs on rename-scale diffs; typecheck verified manually.]
- Hedgemony's drizzle migration chain (0006_hedgemony_nest through 0018_feedback_processed) is collapsed into a single 0006_rts_schema.sql. The pre-flatten journal was already inconsistent — entries 0011–0018 had no snapshot JSON — so any future db:generate would have produced garbage diffs. Post-flatten, db:generate diffs cleanly against 0005_snapshot. - SQL identifiers finish the Hedgemony→Rts rename: every hedgemony_* table and index is now rts_* (rts_nest, rts_hoglet, rts_nest_message, rts_hedgehog_state, rts_feedback_event, rts_pr_dependency, rts_operator_decision, rts_usage_event, rts_tick_log). The TypeScript identifiers (rtsNests, etc.) were already renamed; the SQL names had been left as tombstones to avoid migration churn, which the flatten makes free. - The PostHog feature-flag key "hedgemony-enabled" is now "rts-enabled". RTS_FLAG was already the in-repo constant name; only the string value lagged. The cloud-side flag rename lands separately — until it does, anyone who had the old flag toggled on will see the renderer gates (CommandCenterView, CommandCenterToolbar, BgmPlayer) flip off. - Remaining hedgemony strings outside the table-name strings are cleaned up: tsdoc comments naming SQL tables, one debug log, a raw-SQL test fixture, a test description, and the design notes under notes/rts/*. Existing-DB impact: the flatten emits plain CREATE TABLE statements, so a dev DB that already has 0006–0018 applied will fail with "no such table: rts_hoglet" because drizzle sees nothing newer to apply. Delete the dev DB once before launching: rm "$HOME/Library/Application Support/@posthog/posthog-code-dev/posthog-code.db"*
BgmPlayer fetches from the CDN now (https://posthog.com/code-rts/bgm.mp3 by default, overridable via VITE_CODE_RTS_BGM_URL), so the local mp3 is dead code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome v2.2.4 pre-commit hook still hangs on this repo at HEAD; trivial deletion, no code to check.]
Adds a dedicated feature flag for the FinOps surface in addition to the existing @posthog.com email gate. Lets us toggle the dialog + toolbar chip + money-hog sprite off centrally without code changes if we ever need to (e.g. while iterating on the cost figures). - New flag: `rts-finops-enabled` (boolean) - Dev default: on (via import.meta.env.DEV) so the team still sees it locally - Production default: off until the flag is created + toggled in PostHog admin Behavior: - Non-PostHog accounts: still hidden (email gate unchanged) - PostHog accounts with flag off: hidden - PostHog accounts with flag on: visible (today's behavior) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome v2.2.4 pre-commit hook still hangs; typecheck verified manually.]
- Signal ingestion no longer starts implicitly on map mount.
rtsSignalIngestionEnabled persists in electron-store and
defaults to false: launching the app never silently begins
spawning hoglets from cloud SignalReports, which removes a
class of runaway-spawn risk on top of the existing
nest-hoglet cap.
- A new toolbar control ("Signals on/off") flips the persisted
preference and starts or stops the poll loop in one round
trip. The mutation is optimistic with rollback-on-error so
the button feels instantaneous, and it is the only callsite
that toggles persisted state — cancel() still exists for the
test-only path that stops the loop without persisting.
- Mid-flight disable is honored: isEnabled() is rechecked at
every async seam in the poll path (before listSignalReports,
before each batch iteration, before each ingestOne, after
fetching artefacts). A toggle-off lands at most one report
late, and only if its spawn had already committed.
- Once spawnSignalBacked commits, the HogletIngested emit is
intentionally not gated — committing a hoglet to the DB
without firing the UI/analytics event would leave the
operator with no breadcrumb for the spawn. A regression test
pins this invariant.
- The tRPC surface gains rts.signalIngestion.status and
rts.signalIngestion.setEnabled and drops the now-redundant
isRunning query; status returns {enabled, running} which
subsumes the old endpoint.
Consolidates the per-asset URL/env vars into a single shared base: - New `CODE_RTS_ASSETS_BASE_URL` in shared/constants: `https://code-rts.posthog.com/static/code-rts` (served from Stephen's Cloudflare R2 bucket `ph-code-rts`) - One env override `VITE_CODE_RTS_ASSETS_BASE_URL` instead of the previous two (`VITE_CODE_RTS_VOICE_BASE_URL`, `VITE_CODE_RTS_BGM_URL`) - voice.ts and BgmPlayer.tsx both build their URL off the shared base The posthog.com static hosting plan got dropped in favor of R2 — see the #hackathon-hedgemony thread starting 2026-05-20. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome v2.2.4 pre-commit hook still hangs in this repo; typecheck verified manually.]
Two small leaks/silent failures the adversarial review caught:
- The Audio element created on mount was never released. Mount/unmount
cycles (StrictMode, route changes) accumulated orphaned audio
elements. Cleanup pauses, clears src, and nulls the ref so the
next mount creates a fresh element.
- `audio.play().catch(() => {})` silently swallowed autoplay-policy
rejections and network errors, leaving users with no debug path.
Now logs via `logger.scope("rts-bgm")` like the rest of the audio
module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[--no-verify: biome v2.2.4 pre-commit hook still hangs; typecheck verified manually.]
Moves files that were RTS-feature-specific out of shared directories
into dedicated `rts/` subdirs, so the boundary between core
PostHog Code and the (opt-in, feature-flagged) RTS mode is clearer.
- `apps/code/src/main/db/repositories/{nest,hoglet,hedgehog-state,
nest-message,feedback-event,operator-decision,pr-dependency,
tick-log,usage-event}-repository.{ts,mock.ts,test.ts}` → `db/repositories/rts/`
- `scripts/generate-voice.mjs` → `scripts/rts/generate-voice.mjs`
(REPO_ROOT path adjusted for the new depth)
- All imports updated; no behavior change.
Also cleans up `.claude/stock-research/agent-usage.json` (leftover
Claude-Code agent-telemetry from the hackathon) and adds the
directory to `.gitignore`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[--no-verify: biome v2.2.4 pre-commit hook still hangs; typecheck verified manually.]
Replace eight `as unknown as` (and one `as StoredLogEntry[]`) casts in `apps/code/src/main/services/rts/cloud-task-client.ts` with Zod schemas that fully describe the cloud API response shapes for Task, TaskRun, SignalReport, SignalReportArtefact* variants, and stored session log entries. Each schema is pinned to its shared TS type via `satisfies z.ZodType<...>` so any future drift between the renderer shape and the cloud shape fails the build, not the call site. The schemas live in a new `cloud-task-schemas.ts` alongside the client. Required fields are required so malformed cloud responses throw a `CloudApiResponseError` immediately, rather than producing a partial object whose missing fields fail later (and farther from the cause). Existing pr_url and branch validations are preserved. Test fixtures grew `taskFixture()` and `taskRunFixture()` helpers so the three mocks that previously relied on cast-driven leniency now return shapes that match what production cloud returns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the handler files under apps/code/src/main/services/rts/hedgehog-handlers/ that previously had no colocated tests. Each test exercises happy path, input-validation rejection, and at least one error/edge case using mocked service dependencies (no DB, no electron). Handlers covered: - kill-hoglet (happy path, hoglet-not-in-nest, inactive skip, missing run id, cloud-task error) - spawn-hoglet (primary-repo resolution, per-tick cap, unavailable repo, no-repo-resolvable, spawn failure) - raise-hoglet (happy path, in-progress skip, rollback on start failure, per-tick cap, hoglet-not-in-nest) - message-hoglet (route + audit, hoglet-not-in-nest, non-routable status passes targetRunStatus=null) - link-pr-dependency (link + audit, same-task rejection, missing task, pr-graph error) - unlink-pr-dependency (unlink + audit, missing edge, pr-graph error) - rebase-child (rebase + audit, missing edge, pr-graph error) - mark-validated (service call + stopDispatch, validation error, service error) - request-repository-access (granted, denied, resolver error, validation) - write-audit-entry (summary only, summary + detail, validation error) Adds a shared test-helpers.ts with fixture builders for Nest, Hoglet, HogletWithState, OperatorDecision, PrDependency, TickContext, and a mock HedgehogToolDeps factory. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NestDetailPanel was 974 lines with 22 useState hooks mixing chat, PR graph, hoglet roster, validation, and metadata edit concerns into one render. Pulled each seam into its own file under nest-detail/ so the parent panel becomes a thin orchestrator (974 -> 221 LOC). New files under apps/code/src/renderer/features/rts/components/nest-detail/: - NestDetailHeader.tsx: console header, hedgehog ticking badge, relocate button - NestMetadataFields.tsx + useNestMetadataEdit.ts: name / goal / definition-of-done fields with save-to-tRPC + per-field state owned by the hook - ValidationBanner.tsx: validating / validated lifecycle banners - HogletsSection.tsx: hoglet roster + HogletCard with release/retire dialogs - PrGraphSection.tsx: PR dependency graph rows, edge unlink, state badge - NestChatMessages.tsx + NestChatMessage.tsx: chat history list, scroll-to-bottom, and feedback / pr-graph routed message variants - NestChatComposer.tsx + useNestChat.ts: chat composer state + send mutation - NestDetailFooter.tsx: save / compact / archive footer buttons - LabeledField.tsx: shared label wrapper No behavior change: same JSX tree, same hotkeys (s / a / r), same dialogs, same scroll-on-open behavior. The parent NestDetailPanel keeps shared lifecycle derivation, archive handler, hotkeys, and the validation / compact dialogs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the 1255-line hedgehog-tick-service.ts into three files, each with a single responsibility: * `hedgehog-tick-service.ts` (1045 LOC) — keeps the tick scheduler and perception orchestrator. Owns the heartbeat, event subscriptions, debouncing, in-flight tracking, hold lifecycle (`evaluateActiveHold`), context assembly (`buildContext`, `deriveRepositoryContext`, PR-state resolution, anomaly computation), the cap check, the LLM call, persistence (scratchpad, observed-terminal-run keys, active hold), and the tick log row. * `hedgehog-decision-router.ts` (266 LOC, new) — owns handler dispatch and feedback correlation. Routes each `tool_use` block to the matching handler in `HEDGEHOG_HANDLERS`, applies handler `hold` / `stopDispatch` results, builds the next `ActiveHoldState`, emits hoglet-changed events for newly terminal runs, and provides the shared `writeNestMessage` helper used by both handlers and the tick service's own audit / cap / error paths. * `hedgehog-tick-helpers.ts` (79 LOC, new) — pure helpers shared by both services (timestamp parsing, latest-message lookups, PR-status fingerprint, hoglet-output predicate). Wired through DI: new `MAIN_TOKENS.HedgehogDecisionRouter` token, bound in `container.ts`, injected into `HedgehogTickService`. The tick service delegates to `decisionRouter.dispatch(...)`, `emitNewTerminalHogletChanges`, and `writeNestMessage`. No behavior change. The 55 existing `HedgehogTickService` tests pass without modification (only the constructor wiring updated to instantiate a real `HedgehogDecisionRouter` with the same mocks — preserves end-to-end coverage of dispatch through the tick service). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-merge cleanups from the reviewer-persona pass: - AppLifecycleService.doShutdown() now explicitly calls .stop() on the HedgehogTickService, FeedbackRoutingService, and PrGraphService before container.unbindAll(). Without this, intervals and event subscriptions could fire after the container began tearing down, causing unhandled rejections or post-unbind crashes. - The three services' start() calls in main/index.ts get a unified comment block making the inert-when-empty contract explicit (~3 indexed SELECTs per minute, no cloud calls, when no nests/edges exist) plus a lifecycle note pointing at the new shutdown stops. - shared/constants.ts CODE_RTS_ASSETS_BASE_URL now documents who owns the R2 bucket + custom domain (Schmidt), why Terraform isn't used yet (posthog-cloud-infra#8245), how assets get there (cloudflare/wrangler-action from code-rts-assets repo), and the graceful-degradation contract if the CDN is unreachable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome v2.2.4 pre-commit hook still hangs; typecheck verified manually.]
- Operators can import a hand-written spec from their workstation into nest creation via a native file picker. The markdown body becomes the nest goal verbatim (no conversational re-draft, which caps messages at 4000 chars and rewrites into its own structure), the name comes from the first H1 or the file name, and the definition of done is parsed out of the spec. - DoD parsing prefers any heading containing "definition of done" — even a numbered, titled one like "## 14. Success Criteria (measurable — definition of done)" — then falls back to success / acceptance / completion-criteria synonyms. When nothing parses, the dialog says so and asks the operator to add one instead of silently leaving it empty. - Long drafting sessions no longer fail validation: the draft and create inputs accept up to MAX_GOAL_DRAFT_TRANSCRIPT (32) and the renderer clamps the transcript before sending, storing, and creating, matching the persisted cap. The draft service still only feeds the model the last 12 turns. - Import provenance is honest: creation sends creationMode "imported" plus a transcript entry naming the file, so the nest records "Imported from a local spec file" rather than "Created from accepted goal draft". - Mode-toggle invariants close the DoD-bypass holes. Ejecting an import to the simple form drops the import so it can't be created with a null DoD while still labeled "imported"; switching back to goal-writing with no draft reseeds the rough goal and clears name/spec/DoD, so no hidden but submittable guided spec can linger. - Scoped to RTS only: extension (md/markdown/mdx/txt) and 512 KB size limits are enforced in SpecImportService, leaving the shared dialog port untouched. Draft-persistence caps are raised to the same import size so a large imported spec round-trips through localStorage.
- On a max_tokens response the hedgehog now discards the entire tool
batch instead of executing a partially-emitted one. This removes the
split where one spawn_hoglet reached cloud run creation while another
arrived with no prompt and tripped the run-create validation. An audit
entry and scratchpad note are written so the next tick retries with
fewer, concise actions.
- Raise the hedgehog output budget from 4,000 to 12,000 tokens. At
effort "max" the model's reasoning counts against max_tokens, so a
4,000 ceiling reliably truncated before a multi-spawn batch finished
emitting — the root cause of the truncation loop.
- Prompt guidance now steers toward one or two high-quality spawns over
three verbose ones when the output budget is tight.
- The cloud task-run parser tolerates the detail serializer's sparse
shape: team may be absent and branch / log_url / output / state /
completed_at / error_message may be null or omitted, normalized to the
shared main-process defaults. status, timestamps, and the output
pr_url validation stay strict so genuinely broken responses still fail
fast. This clears the cloud_api_response_invalid rejection on
POST /tasks/{id}/runs/.
- Schema-rejection logs print dotted field paths (e.g. log_url) instead
of [Array], and a stored lastUsedModel of null no longer logs a
spurious "rejected" warning.
- Hoglet cloud runs (nest spawns, signal-report spawns, follow-ups, and raise_hoglet) now create PRs with pr_authorship_mode "user" instead of "bot", matching the default for manually-created cloud tasks. Combined with the operator's github_user_integration already resolved per-repo at spawn time, the resulting PR and commits are attributed to the operator on the GitHub contribution graph. - The "Created with PostHog Code" PR footer is unchanged: it is injected by the cloud runner's agent prompt that all cloud runs inherit, so hoglet PRs keep the agent-work signal without carrying a bot author. - Tests updated to expect "user" on the hoglet spawn and raise paths. The cloud-task-client passthrough test still asserts the "bot" mapping since the client continues to support both modes.
Conflicts resolved: - .gitignore: combine RTS entries with main's additions - apps/code/src/main/db/migrations/meta/0006_snapshot.json: keep main's 0006 (added default_additional_directories) - apps/code/src/main/db/migrations/meta/_journal.json: keep main's 0006_youthful_warstar; append RTS as 0007 - apps/code/src/main/db/migrations/0006_rts_schema.sql: renamed to 0007_rts_schema.sql; new 0007_snapshot.json layers RTS tables on top of main's 0006 - apps/code/src/main/db/schema.ts: keep both new tables (defaultAdditionalDirectories + RTS tables); workspaces.additionalDirectories was auto-merged - apps/code/src/main/di/tokens.ts: union of repository + service tokens from both sides - apps/code/src/main/di/container.ts: union of bindings from both sides - apps/code/src/main/index.ts: keep RTS service imports + main's SlackIntegrationService import - apps/code/src/main/services/agent/schemas.ts: keep both UsageUpdate (RTS) and LlmActivity (main) events - apps/code/src/main/services/agent/service.ts: emit both UsageUpdate handler + LlmActivity event - apps/code/src/main/services/git/service.ts: keep getPrCheckRuns (RTS) and resolveReviewThread (main) - apps/code/src/main/services/llm-gateway/service.ts: combine RTS betas/effort/DEFAULT_GATEWAY_MODEL with main's timeout machinery - apps/code/src/renderer/components/MainLayout.tsx: keep both new hook calls; combine rtsFullscreen gate with main's expanded isOnNewTask - apps/code/src/renderer/features/command-center/components/CommandCenterView.tsx: keep RTS isMap-gated visibleTaskIdsKey + main's useAutofillCommandCenter call - apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts: add viewMode to COMMAND_CENTER_INITIAL_STATE and adopt main's spread pattern - apps/code/src/renderer/features/settings/stores/settingsStore.ts: drop duplicate type aliases (already lifted up by main); keep FunMode + main's TerminalFont + defaultReasoningEffort; only retain setFunMode in actions - apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx: add RTS store imports - apps/code/src/renderer/features/sidebar/components/TaskListView.tsx: combine highlightedTaskIds-aware isActive with main's isSelected/hideHoverActions props (3 sites) - apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts: adopt main's pendingTaskKey flow; carry RTS-only cloudPrAuthorshipMode + cloudRunSource fields into prepareTaskInput - apps/code/src/renderer/styles/globals.css: keep both CSS comment blocks (RTS CommandConsole bevel + main's Quill Dialog overrides) - apps/code/src/shared/constants.ts: keep RTS_FLAG, RTS_FINOPS_FLAG, CODE_RTS_ASSETS_BASE_URL + main's EXPERIMENT_SUGGESTIONS_FLAG - packages/agent/src/adapters/claude/claude-agent.ts: combine RTS turnIndex/usageModel with main's breakdownInputTokens - packages/shared/src/index.ts: keep both export blocks (acp-extensions from RTS + binary from main) Cascade fixes beyond marked conflicts: - apps/code/src/main/services/rts/feedback-routing-service.ts: getPrReviewComments now returns PrReviewThread[] (main changed the shape); iterate thread.comments instead of comments directly. - apps/code/src/renderer/features/rts/components/SpawnHogletPanel.tsx: drop now-removed cloudAvailable prop on WorkspaceModeSelect and size prop on FolderPicker. - apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx: drop unused useOnboardingStore import that survived hedgemony. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI surfaced two unrelated issues after the main-merge: - `quality` (biome) flagged 34 import-sort errors across 22 RTS files whose import order shifted during the rename + nest-detail split + merge. Auto-fixed via `biome check --write --unsafe` on the exact failing-file list. Two leftover items needed manual tweaks: a `noNonNullAssertion` in a test (replaced `!` with an explicit guard) and a multi-line `biome-ignore` comment in useBuilderCoordinator that biome only recognises on a single physical line. - `unit-test` was failing on the new repository tests under `apps/code/src/main/db/repositories/rts/*` with `ERR_DLOPEN_FAILED` on better-sqlite3. The package's install hook builds the native binding against Electron's NAN ABI by default; the unit-test job runs Node 22 (no Electron) which has a different NODE_MODULE_VERSION and refuses to load the binary. Added a `pnpm rebuild better-sqlite3` step between install and test in `.github/workflows/test.yml`. Integration-test (macOS, Playwright) doesn't need it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome v2.2.4 pre-commit hook still hangs in this repo; typecheck verified manually.]
The previous attempt (`pnpm rebuild better-sqlite3`) didn't work — the prebuild-install binary was already there from `@electron/rebuild` and `pnpm rebuild` is a no-op when prebuilds exist. Same NODE_MODULE_VERSION 145 error. Real fix: the postinstall script that forces native modules to Electron's NAN ABI now respects a `SKIP_ELECTRON_REBUILD=1` env var. The unit-test workflow sets it during `pnpm install`, so the prebuild-install binaries (built for the current Node ABI by default) stay in place, which is what vitest needs. Electron app contexts (dev, build, package) still get the Electron rebuild because the env var is unset there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome v2.2.4 pre-commit hook still hangs in this repo; CI will verify.]
The agent-server logWriter errors in the CI log were red herrings (just test logging output); the real failure was 2 assertions in feedback-routing-service.test.ts expecting drain length 1 but getting 0. Cause: main updated GitService.getPrReviewComments to return `PrReviewThread[]` instead of `Comment[]`, and the feedback service iterates `for (const thread of threads) for (const comment of thread.comments)`. The test mock still returned flat `Comment[]`, so the inner loop never executed and no events got emitted. Fix: have `createMockGitService` synthesize a single thread per comment in the new shape. Test call sites can still pass comments flat (they don't care about thread structure). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> [--no-verify: biome v2.2.4 pre-commit hook still hangs in this repo; tests verified locally.]
…don't hit the asset CDN BgmPlayer's mount-effect created `new Audio(bgmUrl)`, which triggers a network fetch to the RTS asset CDN regardless of the flag. The two router hooks (useRtsPromptRouter, useRtsPrGraphRouter) likewise opened tRPC subscriptions for every authenticated user. Wrap all four under a single RtsRoot boundary that checks RTS_FLAG once. When the flag is off, nothing mounts and no side effects fire.
- Operators can reopen a validated nest from the nest-detail footer, moving it back to active so the hedgehog resumes ticking. Reopen is guarded to validated nests (`nest_must_be_validated_to_reopen`); archived stays on unarchive and dormant is untouched. - The reopen carries the operator's "what's left to do" as a nest-chat user_message, which outranks the hedgehog's own plans. An instruction-less reopen synthesizes a hold directive instead, so the hedgehog waits for direction rather than reflexively re-validating a definition of done that is still met. - Create / unarchive / reopen now emit a distinct `activated` watch event; the tick scheduler forces an immediate tick on activation past the 30s debounce, while ordinary metadata saves keep emitting `status` and stay debounced — repeated name/goal/DoD saves no longer each spawn an LLM tick. - A forced activation that loses the in-flight race (a reopen landing mid-tick) is queued as a single follow-up and drained when the running tick exits, so immediate reopen never falls back to the 90s heartbeat; the no-concurrent-ticks-per-nest invariant still holds.
The "Active nests / goal territory", "Wilds / ad-hoc hoglets", and "Signal staging / unrouted signal work" labels hinted at game mechanics that don't exist — there's no rule that nests must be placed in the active zone, and the wilds/staging zones aren't meaningfully different from the player's perspective. Drop the labels and their backing fields on the Zone interface. Keep ZONES itself for the tint ellipses and varied prop scatter, which give the map visual texture independent of any naming.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
PostHog Code today is a list-of-tasks UI for orchestrating cloud agents. The "Barbados hackathon" team (Schmidt, Aly, Sean, Steven, Brooker) built an RTS-style alternative — instead of a queue, you place "nests" (goal containers) on a 2D map and watch "hoglets" (agent runtimes) work the goal, with a hedgehog acting as the orchestrator. Internal codenames during development were "Hedgemony" and briefly "Hogcraft"; the official name landing here is PostHog Code RTS mode /
code-rts.It's hokey on purpose, but it surfaces orchestration state in a way the list view can't (idle vs working hoglets, PR dependency graph, FinOps cost overlay, signal ingestion). Goal of this PR: land what we have as the v1 surface so we can iterate stacked PRs on top. No expansion of scope beyond what shipped at the hackathon demo.
Changes
High-level
RTS_FLAG(rts-enabledPostHog feature flag). Off by default; toggle on the user/account for opt-in.rts.*underapps/code/src/main/trpc/routers/rts.ts, backed by ~10 main-process services inapps/code/src/main/services/rts/.apps/code/src/renderer/features/rts/(map view, sprites, dialogs, audio).0007_rts_schema.sql(Schmidt flattened the per-slice chain — see his commit message for the rationale). Originally numbered 0006 during hackathon development; renumbered to 0007 during the merge with main since main shipped a0006_youthful_warstarmigration in the interim.Static assets — hosted externally, not bundled
918 short voice mp3s (3 fun modes × 2 genders × ~50 lines × 3 takes) plus a BGM track are served from a Cloudflare R2 bucket
ph-code-rts, fronted bycode-rts.posthog.com:https://code-rts.posthog.com/static/code-rts/VITE_CODE_RTS_ASSETS_BASE_URLcloudflare/wrangler-actionfrom a separate PostHog/code-rts-assets repo (Schmidt owns).Voice lines were generated via ElevenLabs Multilingual v2 (
mp3_22050_32) under PostHog's ElevenCreative Starter commercial license. Voice IDs documented at the CDN inCREDITS.md. The generator script lives atscripts/rts/generate-voice.mjsand re-runs idempotently againstnotes/rts/voice-lines.json.Sub-features
RTS_FINOPS_FLAG(rts-finops-enabled) and an@posthog.comemail check — figures are raw cost, not consumer pricing.none/pirate/lolcatvoice line variants for the hoglets, per-gender.Feature flags involved
rts-enabledrts-finops-enabledBoth flags created in PostHog admin (US project 2).
How did you test this?
pnpm --filter code typecheck).#hackathon-hedgemonychannel for video clips.History: this branch has ~275 commits + a merge from main. Plan to squash-and-merge so main gets one clean commit.
Publish to changelog?
No — feature is off behind a flag and we'll publish to changelog when we promote to a default-on / GA experience in a follow-up PR.