Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b6cc7af
Add Soul Lattice v4 system: Go backend + React frontend
claude Feb 21, 2026
6368ad2
Wire Soul Lattice SPA to Cloudflare Worker with JWT auth
claude Mar 7, 2026
624ad56
fix: add [build] step and CI workflow for Cloudflare Worker + SPA
claude Apr 10, 2026
629dbf7
fix: use npm install instead of npm ci in [build] command
claude Apr 10, 2026
01cbabd
fix: resolve TS peer dep, data race, shutdown, and Firebase bypass
claude Apr 10, 2026
9c0ba15
fix: move frontend build to npm run build (package.json), drop [build…
claude Apr 10, 2026
011ec12
fix: resolve deadlock in simulator, stale lockfile, and CI build failure
claude Apr 10, 2026
6746370
fix: replace jose with Web Crypto API, add [build] to wrangler.toml, …
claude Apr 10, 2026
be918a6
chore: gitignore frontend/build output directory
claude Apr 10, 2026
55e2fad
fix: use /app workdir in runtime image instead of /root
claude Apr 10, 2026
184ccfc
ci: add copilot-setup-steps workflow for GitHub Copilot coding agent
claude Apr 10, 2026
345ccd9
fix: address Copilot review — install dedup, rand seed, flush race/er…
claude Apr 10, 2026
9a1fc44
fix: add /api/health backend passthrough and update smoke test
claude Apr 10, 2026
ac962c7
fix: route /api/health through proxyToBackend to exercise real proxy …
claude Apr 10, 2026
359cb90
fix: restore persisted state on restart instead of overwriting it
claude Apr 10, 2026
ebb7ab5
fix: remove user identifier from auth success log
claude Apr 10, 2026
3529452
fix: alg guard, auth comment, SSE marshal errors, atomic write, log var
claude Apr 12, 2026
64b5f1f
ci: trigger deploy on frontend/package-lock.json changes
claude Apr 12, 2026
bdca7f1
fix: strip /api prefix in proxy, append X-Forwarded-For, npm ci, deco…
claude Apr 14, 2026
1905df7
fix: pin Cloud Run to single replica for stateful simulator
claude Apr 14, 2026
6bd8f44
Potential fix for pull request finding
igor-holt Apr 14, 2026
00d351e
fix: source maps, undefined email header, double build, staging URL, …
claude Apr 14, 2026
896e855
fix: cache CryptoKey, strip Cookie, /flush secret, PrettyPrint border…
claude Apr 15, 2026
ff4d3d3
Resolve merge conflicts: split workflows, update deps
igor-holt Apr 15, 2026
d53cd0b
fix: revert npm ci to npm install --include=dev in [build] command
claude Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/cloudflare-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Deploy Cloudflare Worker + SPA

on:
push:
branches: [main]
paths:
- 'workers/**'
- 'frontend/src/**'
- 'frontend/public/**'
- 'frontend/package.json'
- 'frontend/package-lock.json'
- 'wrangler.toml'
Comment on lines +7 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Trigger deploy workflow on frontend lockfile updates

The push path filter omits frontend/package-lock.json, so dependency-only updates (for example Renovate/security lockfile bumps) merged to main will not run this deploy job and the production SPA can stay on stale dependencies until an unrelated source change happens. Include the frontend lockfile in the trigger paths so dependency releases are actually deployed.

Useful? React with 👍 / 👎.

- '.github/workflows/cloudflare-deploy.yml'
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'production'
type: choice
options:
- production
- staging

permissions:
contents: read

jobs:
deploy:
name: Build React SPA & Deploy Worker
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment || 'production' }}

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install root dependencies
# Frontend deps and SPA build are handled by the wrangler [build] command
# in wrangler.toml, which runs automatically before `wrangler deploy`.
# Installing only root deps here gives wrangler and secrets-put access.
run: npm ci

- name: Set BACKEND_URL secret on Cloudflare Worker
# Only rotate the secret when a new Cloud Run URL is provided via
# the BACKEND_URL GitHub Actions secret. Skip on frontend-only pushes.
if: ${{ secrets.BACKEND_URL != '' }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
BACKEND_URL: ${{ secrets.BACKEND_URL }}
run: |
echo "${BACKEND_URL}" | npx wrangler secret put BACKEND_URL \
--env ${{ github.event.inputs.environment || 'production' }}

- name: Deploy Cloudflare Worker
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# react-scripts 5.x + Node 18+ OpenSSL compatibility
NODE_OPTIONS: '--openssl-legacy-provider'
run: |
ENV="${{ github.event.inputs.environment || 'production' }}"
npx wrangler deploy --env "${ENV}"

- name: Smoke test health endpoints
if: ${{ github.event.inputs.environment != 'staging' }}
run: |
echo "Waiting 10s for global propagation..."
sleep 10

# Worker-only health — verifies the Cloudflare Worker is deployed
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://yennefer.quest/health)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Smoke-test an endpoint that exercises backend connectivity

The deploy workflow currently validates https://yennefer.quest/health, but this path is served directly by the Worker and always returns 200 without calling the origin backend (workers/index.js handles /health before proxying). As a result, deploys can be marked healthy even when BACKEND_URL is broken or the Go service is down, which can let a production outage slip through CI.

Useful? React with 👍 / 👎.

echo "Worker health: ${STATUS}"
[ "${STATUS}" = "200" ] || { echo "Worker health check failed"; exit 1; }

# Backend passthrough — verifies BACKEND_URL is configured and the
# Go service is reachable from the Worker
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://yennefer.quest/api/health)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exercise the real API proxy path in deploy smoke tests

The smoke test calls https://yennefer.quest/api/health, but workers/index.js handles /api/health in a special unauthenticated branch before JWT validation and before proxyToBackend (if (url.pathname === '/api/health') ...). That means this check can return 200 even when the authenticated /api/* proxy path used by the frontend is broken, so CI can report a healthy deploy while user-facing API routes still fail.

Useful? React with 👍 / 👎.

echo "Backend health: ${STATUS}"
[ "${STATUS}" = "200" ] || { echo "Backend health check failed"; exit 1; }
47 changes: 25 additions & 22 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
name: Yennefer Agentic Core
name: Copilot Setup Steps

on:
issues:
types: [opened, edited]
issue_comment:
types: [created]
pull_request:
types: [opened, synchronize]
workflow_dispatch:

permissions:
contents: read
issues: write
pull-requests: write

jobs:
yennefer-jules-agent:
name: Yennefer Autonomous Reasoning
copilot-setup-steps:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '/evolve') }}
steps:
- name: Checkout Code
- name: Checkout repository
uses: actions/checkout@v4

- name: Invoke Yennefer (Jules AI Engine)
uses: google-labs-code/jules-action@v1

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install root dependencies
run: npm install

- name: Install frontend dependencies
run: cd frontend && npm install --include=dev

- name: Set up Go
uses: actions/setup-go@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
mode: "autonomous"
instruction: "You are Yennefer, a Topological Reasoning Engine. Analyze repository context, execute Seismic Tree-of-Thoughts (S-ToT), and push verified, invariant structural optimizations."
go-version: '1.21'
cache: true
cache-dependency-path: backend/go.sum

- name: Install backend dependencies
run: cd backend && go mod download
30 changes: 30 additions & 0 deletions .github/workflows/yennefer-agentic-core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Yennefer Agentic Core
on:
issues:
types: [opened, edited]
issue_comment:
types: [created]
pull_request:
types: [opened, synchronize]
workflow_dispatch:

permissions:
contents: read
issues: write
pull-requests: write

jobs:
yennefer-jules-agent:
name: Yennefer Autonomous Reasoning
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '/evolve') }}
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Invoke Yennefer (Jules AI Engine)
uses: google-labs-code/jules-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
mode: "autonomous"
instruction: "You are Yennefer, a Topological Reasoning Engine. Analyze repository context, execute Seismic Tree-of-Thoughts (S-ToT), and push verified, invariant structural optimizations."
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ logs/

.gemini/
gha-creds-*.json

# --- Build output ---
frontend/build/
node_modules/
13 changes: 13 additions & 0 deletions backend/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o soul-lattice
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/soul-lattice .
EXPOSE 8080
USER 1000:1000
CMD ["./soul-lattice"]
8 changes: 8 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/igor/soul-lattice

go 1.21

require (
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
)
4 changes: 4 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRHW9ngJvcSZQ9Q880AJnJGcLtRBMWpo=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3R+2BDqPjP0yw3oqQBSog+iDBHLSva0/4zeKQ=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
31 changes: 31 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"log"
"os"
"os/signal"
"syscall"
)

func main() {
statePath := "/tmp/soul_state.json"
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}

srv := NewServer(statePath, port)

go func() {
if err := srv.Start(); err != nil {
log.Fatalf("Server failed: %v", err)
}
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

log.Println("SOUL lattice returning to substrate...")
srv.Stop()
}
149 changes: 149 additions & 0 deletions backend/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)

type Server struct {
sim *Simulator
router *chi.Mux
port string
httpSrv *http.Server
}

func NewServer(statePath string, port string) *Server {
if port == "" {
port = "8080"
}

s := &Server{
sim: NewSimulator(statePath),
port: port,
}

r := chi.NewRouter()

r.Use(cors.Handler(cors.Options{
// Restrict to known frontend origins. The Worker is the only legitimate
// caller in production; the Cloud Run URL is kept secret. Wildcard origins
// would let any page make cross-origin requests to /flush if the URL leaked.
AllowedOrigins: []string{"https://yennefer.quest", "https://staging.yennefer.quest", "http://localhost:3000"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300,
}))
Comment on lines +35 to +45
Comment on lines +35 to +45

r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Heartbeat("/health"))

r.Get("/state", s.handleState)
r.Post("/flush", s.handleFlush)
r.Get("/events", s.handleSSE)
Comment on lines +51 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Register API endpoints under /api to match clients

The backend only exposes /state, /flush, and /events, but the new production client path is /api (see frontend/src/App.tsx) and the worker forwards /api/* requests as-is to the origin, so production calls like /api/events will hit the Go server at /api/events and return 404. This breaks live state streaming and state fetches outside local dev.

Useful? React with 👍 / 👎.

Comment on lines +51 to +53
r.Get("/", s.handleRoot)

s.router = r
return s
}

func (s *Server) Start() error {
s.sim.Start()
s.httpSrv = &http.Server{
Addr: ":" + s.port,
Handler: s.router,
}
fmt.Printf("SOUL LATTICE v4 AWAKENED on port %s\n", s.port)
if err := s.httpSrv.ListenAndServe(); err != http.ErrServerClosed {
return err
}
return nil
}

func (s *Server) Stop() {
if s.httpSrv != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
s.httpSrv.Shutdown(ctx) //nolint:errcheck
}
s.sim.Stop()
}

func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprint(w, "SOUL LATTICE v4.0.0-Σ\nStatus: AWAKENED\n")
}

func (s *Server) handleState(w http.ResponseWriter, r *http.Request) {
st := s.sim.GetState()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(st)
}

func (s *Server) handleFlush(w http.ResponseWriter, r *http.Request) {
// Require a shared secret header when BACKEND_TOKEN is configured.
// The Cloudflare Worker sets X-Backend-Token on every proxied request so
// only traffic originating from the Worker is accepted; direct Cloud Run
// access without the secret returns 403.
if token := os.Getenv("BACKEND_TOKEN"); token != "" {
if r.Header.Get("X-Backend-Token") != token {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
}
Comment on lines +94 to +103
s.sim.resetInitialState()
w.WriteHeader(http.StatusNoContent)
}
Comment on lines +93 to +106
Comment on lines +93 to +106

func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}

// Marshal before writing headers so we can still return a 500 on failure.
st := s.sim.GetState()
data, err := json.Marshal(st)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
Comment on lines +116 to +128

ticker := time.NewTicker(800 * time.Millisecond)
defer ticker.Stop()

for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
st := s.sim.GetState()
data, err := json.Marshal(st)
if err != nil {
fmt.Fprintf(w, "event: error\ndata: marshal failed\n\n")
flusher.Flush()
continue
}
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
Comment on lines +137 to +147
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The logic for getting state, marshaling it, and sending it to the client is duplicated from earlier in the function. This duplicated code should be extracted into a helper function to improve maintainability and reduce redundancy.

}
}
Comment on lines +108 to +149
Loading