🔥 Fabrik V3 — Arkitekturplan

Hybrid-arkitektur med persistent state + on-demand spawns. Senast uppdaterad 2026-03-30 10:05 UTC. ✅ ALLA 5 STEG IMPLEMENTERADE.

B1 — Hybrid-arkitektur

Lundin + Raffe = dedicerade agenter med persistent state. John, Janne, Niklasson, Ping = on-demand spawns.

B2 — Raffe som egen agent

Raffe-cron disablas. Raffe blir dedicerad agent i agents.list, triggad av Lundin.

B3 — Neo4j som delat sinne

Write-through, read-on-start, gemensamt schema. Lundin + Raffe skriver direkt.

B4 — Spawn-findings mönster C

Spawns skriver ALDRIG till Neo4j. Artefakt i GitHub + rapport via sessions_send. Validering + Neo4j-write = Lundin/Raffe.

B5 — Cron-cleanup

Factory-loop + orchestrator slås ihop. Raffe-cron disablas. Resten utvärderas.

1
Neo4j Graph-Schema
✅ APPROVED

Nodtyper

NodSyftePrimärnyckel
Brief NYKundintag / affärsbehov — blir Epic vid GOid (UUID)
EpicStort arbetspaket (godkänd Brief)id (UUID)
IssueGitHub Issue, 1:1-mappningrepo + number
DecisionArkitektur-/affärsbeslutid (UUID)
DeployVarje deploy till produktionid (UUID)
AgentPersistent agent eller spawn-registername
ProductRepo/produktslug
IncidentDriftstörningid (UUID)
LearningLärdom som påverkar framtida beslutid (UUID)
SpawnReport NYStrukturerad rapport från on-demand spawnid (UUID)

Relationer

FrånRelationTillEgenskaper
BriefBECAME NYEpicapprovedBy, at
BriefFOR_PRODUCT NYProduct
EpicCONTAINSIssueorder
IssueDEPENDS_ONIssuetype (blocks/needs)
IssueBELONGS_TOProduct
DecisionAFFECTSProduct
DecisionMADE_BYAgentat
DeployDEPLOYSProductcommit, runId, at
DeployTRIGGERED_BYIssue
AgentWORKED_ONIssuerole, at
AgentSPAWNEDAgenttask, at, sessionKey
SpawnReportREPORTED_BY NYAgent
SpawnReportCONCERNS NYIssue
SpawnReportVALIDATED_BY NYAgentat, verdict
IncidentAFFECTEDProductduration, severity
IncidentRESOLVED_BYIssue
LearningLEARNED_FROMIssue / Incident / Decision
LearningAPPLIES_TOProduct

Standardfält (alla noder)

created:  datetime()
updated:  datetime()
source:   string    // "lundin", "raffe", "heartbeat", "spawn-report"
status:   string    // nodtyp-specifikt

Status per nodtyp

NodTillåtna status
Brief NYdraft · proposed · approved · rejected · parked
Epicplanning · active · done · parked
Issuebacklog · ready · in-progress · in-review · done · blocked
Decisionproposed · approved · superseded
Deploysuccess · failure · rollback
Incidentactive · mitigated · resolved
SpawnReport NYreceived · processed · rejected

Constraints & Index

CREATE CONSTRAINT issue_unique FOR (i:Issue) REQUIRE (i.repo, i.number) IS UNIQUE;
CREATE CONSTRAINT agent_unique FOR (a:Agent) REQUIRE a.name IS UNIQUE;
CREATE CONSTRAINT product_unique FOR (p:Product) REQUIRE p.slug IS UNIQUE;
CREATE INDEX issue_status FOR (i:Issue) ON (i.status);
CREATE INDEX deploy_at FOR (d:Deploy) ON (d.at);
CREATE INDEX brief_status FOR (b:Brief) ON (b.status);
CREATE INDEX spawnreport_status FOR (sr:SpawnReport) ON (sr.status);

Exempel-queries

Brief → Epic lifecycle

// Skapa brief från kundintag
CREATE (b:Brief {id: randomUUID(), title: "Webshop för Härryda BBQ",
  description: "Kund vill sälja chark online", customer: "harrydabbq",
  source: "lundin", status: "draft", created: datetime()})

// Godkänn brief → skapa Epic
MATCH (b:Brief {id: $briefId})
SET b.status = "approved", b.updated = datetime()
CREATE (e:Epic {id: randomUUID(), title: b.title, status: "planning",
  source: "lundin", created: datetime()})
CREATE (b)-[:BECAME {approvedBy: "jorgen", at: datetime()}]->(e)
RETURN e

SpawnReport — ta emot + validera

// Spawn skickar rapport → Lundin/Raffe skapar SpawnReport-nod
CREATE (sr:SpawnReport {id: randomUUID(), status: "received",
  summary: "PR #42 skapad, 3 filer ändrade, alla tester gröna",
  prUrl: "https://github.com/diffen77/zoe-api/pull/42",
  filesChanged: ["src/auth.ts","src/middleware.ts","tests/auth.test.ts"],
  source: "john", created: datetime()})

// Lundin validerar rapporten
MATCH (sr:SpawnReport {id: $reportId})
MATCH (a:Agent {name: "lundin"})
SET sr.status = "processed", sr.updated = datetime()
CREATE (sr)-[:VALIDATED_BY {at: datetime(), verdict: "approved"}]->(a)

Vad har Raffe jobbat på senaste veckan?

MATCH (a:Agent {name:"raffe"})-[w:WORKED_ON]->(i:Issue)-[:BELONGS_TO]->(p:Product)
WHERE w.at > datetime() - duration('P7D')
RETURN p.slug, i.number, i.title, w.role, i.status
ORDER BY w.at DESC

Öppna issues med beroenden

MATCH (i:Issue)-[:BELONGS_TO]->(p:Product {slug:"zoe-api"})
WHERE i.status IN ["backlog","ready","in-progress","blocked"]
OPTIONAL MATCH (i)-[d:DEPENDS_ON]->(blocker:Issue)
RETURN i.number, i.title, i.status, collect(blocker.number) AS blocked_by

Raffe read-on-start (epic-kontext)

MATCH (e:Epic {id:$epicId})-[:CONTAINS]->(i:Issue)-[:BELONGS_TO]->(p:Product)
OPTIONAL MATCH (i)-[:DEPENDS_ON]->(dep:Issue)
OPTIONAL MATCH (l:Learning)-[:APPLIES_TO]->(p)
RETURN e, collect(DISTINCT i) AS issues,
       collect(DISTINCT dep) AS dependencies,
       collect(DISTINCT l) AS learnings

Obehandlade spawn-rapporter

MATCH (sr:SpawnReport {status: "received"})-[:REPORTED_BY]->(a:Agent)
OPTIONAL MATCH (sr)-[:CONCERNS]->(i:Issue)
RETURN sr.id, sr.summary, a.name AS reporter, i.number AS issue
ORDER BY sr.created ASC
Designval: Brief fångar kundintag innan de blir arbetspaket — status approved + BECAME-relation skapar Epic. SpawnReport persisterar spawns resultat med spårbar validering (vem godkände, när, verdict). Spawns skriver aldrig till Neo4j direkt — Lundin/Raffe skapar SpawnReport-noden efter mottagning.
2
Kommunikationsprotokoll
✅ APPROVED

Översikt

All inter-agent-kommunikation sker via sessions_send. Varje meddelande är JSON med ett type-prefix som bestämmer hur mottagaren ska hantera det. Spawns returnerar alltid strukturerade rapporter — aldrig fritext.

Meddelandetyper

typeRiktningSyfte
task:assignLundin → RaffeNytt uppdrag med epic/issue-kontext
task:updateRaffe → LundinStatusuppdatering under pågående arbete
task:completeRaffe → LundinUppdrag klart, PR skapad
task:blockedRaffe → LundinBlockerad, behöver beslut
spawn:resultSpawn → Lundin/RaffeSlutrapport från on-demand spawn
spawn:errorSpawn → Lundin/RaffeSpawn misslyckades
qa:reportQA-spawn → LundinTestresultat efter smoke/e2e
escalationRaffe → LundinKräver Lundins beslut eller eskalering till Jörgen

JSON-schema: task:assign

{
  "type": "task:assign",
  "epicId": "uuid-of-epic",
  "issueRef": "diffen77/zoe-api#42",
  "title": "Fix Clerk auth middleware",
  "ac": [
    "Clerk token verification works with CLERK_SECRET_KEY",
    "All /api/* endpoints return 200 for authenticated users",
    "Existing tests pass"
  ],
  "files": ["src/middleware.ts", "src/auth.ts"],
  "branch": "fix/clerk-auth-42",
  "context": {
    "product": "zoe-api",
    "runner": "[self-hosted, wsl2]",
    "notes": "Deploy-script already has CLERK_SECRET_KEY as of 2026-03-30"
  },
  "priority": "high",
  "deadline": "2026-03-30T18:00:00Z"
}

JSON-schema: spawn:result

{
  "type": "spawn:result",
  "spawnId": "session-key-of-spawn",
  "agent": "john",
  "issueRef": "diffen77/zoe-api#42",
  "status": "success",
  "summary": "PR #43 skapad. 3 filer ändrade, alla tester gröna.",
  "artifacts": {
    "pr": "https://github.com/diffen77/zoe-api/pull/43",
    "branch": "fix/clerk-auth-42",
    "filesChanged": ["src/middleware.ts", "src/auth.ts", "tests/auth.test.ts"],
    "testsRun": 12,
    "testsPassed": 12
  },
  "duration": "4m32s",
  "warnings": []
}

JSON-schema: spawn:error

{
  "type": "spawn:error",
  "spawnId": "session-key-of-spawn",
  "agent": "john",
  "issueRef": "diffen77/zoe-api#42",
  "status": "failed",
  "error": "Clone failed: repository not found",
  "phase": "setup",
  "duration": "0m12s",
  "retryable": true
}

JSON-schema: qa:report

{
  "type": "qa:report",
  "target": "https://harrydabbq.se",
  "product": "harrydabbq.se",
  "checks": [
    {"name": "HTTP 200", "status": "pass", "detail": "200 OK in 52ms"},
    {"name": "Hero renders", "status": "pass", "detail": "h1 'Härryda BBQ' found"},
    {"name": "/produkter content", "status": "fail", "detail": "Page blank, no product elements"},
    {"name": "Console errors", "status": "pass", "detail": "0 errors"}
  ],
  "summary": "3/4 pass, 1 critical fail: /produkter blank page",
  "screenshots": ["produkter-blank.png"],
  "severity": "critical"
}

Reellt exempel: QA Smoke Test 2026-03-30 08:52 UTC

Nedanstående är en faktisk qa:report från QA-cronen. Visar hur protokollet ser ut i produktion.
{
  "type": "qa:report",
  "timestamp": "2026-03-30T08:52:00Z",
  "targets": [
    {"url": "https://harrydabbq.se",       "http": 200, "ms": 63,  "render": "pass", "console": 0},
    {"url": "https://mfe.lediff.se",        "http": 200, "ms": 56,  "render": "pass", "console": 0},
    {"url": "https://rekopro.se",           "http": 200, "ms": 113, "render": "pass", "console": 0},
    {"url": "https://mission.lediff.se",    "http": 200, "ms": 53,  "render": "pass", "console": 1,
     "consoleDetail": "Mixed Content: /factory/ HTTP→HTTPS mismatch"},
    {"url": "https://zoe.lediff.se",        "http": 200, "ms": 58,  "render": "pass", "console": 0,
     "appError": "Dashboard: Kunde inte hämta statistik (API 401)"},
    {"url": "https://api-zoe.lediff.se/health", "http": 200, "ms": 94, "render": "n/a", "console": 0}
  ],
  "summary": "6/6 HTTP 200. 1 console warning (mission mixed-content). 1 app-level error (zoe dashboard 401).",
  "severity": "low",
  "actionItems": [
    {"target": "mission.lediff.se", "issue": "Mixed content on /factory route", "priority": "low"},
    {"target": "zoe.lediff.se", "issue": "Dashboard API 401 — CLERK_SECRET_KEY missing", "priority": "blocked"}
  ]
}

JSON-schema: task:complete

{
  "type": "task:complete",
  "epicId": "uuid-of-epic",
  "issueRef": "diffen77/zoe-api#42",
  "pr": "https://github.com/diffen77/zoe-api/pull/43",
  "branch": "fix/clerk-auth-42",
  "filesChanged": ["src/middleware.ts", "src/auth.ts"],
  "testResults": {"total": 12, "passed": 12, "failed": 0},
  "summary": "Clerk middleware now verifies JWT with secret key. All endpoints return 200 for valid tokens.",
  "readyForReview": true
}

JSON-schema: task:blocked

{
  "type": "task:blocked",
  "issueRef": "diffen77/zoe-api#42",
  "blocker": "CLERK_SECRET_KEY not in deploy script",
  "blockerType": "missing-secret",
  "triedSolutions": [
    "Checked INFRA.md — key not listed",
    "Checked docker inspect — env var missing"
  ],
  "needsFrom": "jorgen",
  "suggestion": "Get sk_live_* from Clerk Dashboard → Projects → API Keys"
}

JSON-schema: escalation

{
  "type": "escalation",
  "from": "raffe",
  "severity": "high",
  "subject": "Scope creep detected in PR #43",
  "detail": "Kodagent ändrade 7 filer istället för specificerade 3. PR stängd.",
  "action": "Behöver ny spawn med striktare filbegränsning",
  "suggestion": "Re-spawn med explicit BARA: [src/middleware.ts, src/auth.ts]"
}

Prefix-konventioner

PrefixBetydelseMottagarens agerande
task:Uppdragsrelaterat (assign/update/complete/blocked)Uppdatera Issue-status i Neo4j + GitHub labels
spawn:Spawn-livscykel (result/error)Skapa SpawnReport-nod, validera, agera på resultat
qa:KvalitetsrapporterLogga i Neo4j, eskalera vid critical severity
escalationKräver beslut från högre nivåLundin tar beslut eller eskalerar till Jörgen

Flödesexempel: Issue → Spawn → Validering

1. Lundin skickar task:assign till Raffe
   └─ Raffe spawnar John (kodagent) med issue-kontext
      └─ John jobbar, returnerar spawn:result till Raffe
         └─ Raffe validerar:
            ├─ OK → task:complete till Lundin + SpawnReport(processed)
            └─ FEL → spawn:error till Lundin + SpawnReport(rejected)
               └─ Lundin beslutar: re-spawn, eskalera, eller stäng

SpawnReport-livscykel (Neo4j-integration)

När en spawn skickar spawn:result eller qa:report, skapar mottagaren (Lundin/Raffe) en SpawnReport-nod i Neo4j. Hela rapporten persisteras — ingen data förloras vid compaction.

// 1. QA-spawn skickar qa:report via sessions_send
//    Lundin tar emot → skapar SpawnReport-nod

CREATE (sr:SpawnReport {
  id: randomUUID(),
  status: "received",
  type: "qa:report",
  summary: "6/6 HTTP 200. 1 console warning, 1 app-level error.",
  severity: "low",
  payload: '{"targets":[...]}',   // Hela JSON-rapporten som string
  source: "qa-spawn",
  created: datetime()
})

// 2. Lundin validerar — inga kritiska fel, loggar som processed
MATCH (sr:SpawnReport {id: $reportId})
MATCH (validator:Agent {name: "lundin"})
SET sr.status = "processed", sr.updated = datetime()
CREATE (sr)-[:VALIDATED_BY {at: datetime(), verdict: "approved"}]->(validator)

// 3. Om QA hittar kritisk bugg → SpawnReport kopplas till ny Issue
MATCH (sr:SpawnReport {id: $reportId})
MATCH (i:Issue {repo: "diffen77/harrydabbq.se", number: 201})
CREATE (sr)-[:CONCERNS]->(i)

Brief → Epic: Reellt flöde

Exempel: Jörgen säger "Kunder ska kunna beställa chark online". Lundin skapar Brief, Jörgen ger GO, Brief blir Epic med issues.

// 1. Lundin skapar Brief från kundintag
CREATE (b:Brief {
  id: randomUUID(),
  title: "Online charkbeställning",
  description: "Kunder ska kunna beställa och betala charkprodukter via harrydabbq.se",
  customer: "harrydabbq",
  requestedBy: "jorgen",
  source: "lundin",
  status: "draft",
  created: datetime()
})-[:FOR_PRODUCT]->(:Product {slug: "harrydabbq.se"})

// 2. Lundin analyserar, lägger till spec, föreslår
MATCH (b:Brief {id: $briefId})
SET b.status = "proposed",
    b.spec = "Produktkatalog + varukorg + Stripe checkout + orderbekräftelse",
    b.estimate = "5 issues, ~2 dagar",
    b.updated = datetime()

// 3. Jörgen ger GO → Brief becomes Epic
MATCH (b:Brief {id: $briefId})
SET b.status = "approved", b.updated = datetime()
CREATE (e:Epic {
  id: randomUUID(),
  title: b.title,
  description: b.spec,
  status: "planning",
  source: "lundin",
  created: datetime()
})
CREATE (b)-[:BECAME {approvedBy: "jorgen", at: datetime()}]->(e)

// 4. Lundin bryter ner Epic till Issues
MATCH (e:Epic {id: $epicId})
CREATE (i1:Issue {repo: "diffen77/harrydabbq.se", number: 210,
  title: "Produktkatalog-sida", status: "backlog", created: datetime()})
CREATE (e)-[:CONTAINS {order: 1}]->(i1)
// ... fler issues
Regel: Alla sessions_send-meddelanden MÅSTE vara parsbar JSON med type-fält. Fritext-meddelanden accepteras ALDRIG av mottagaren — de loggas som SpawnReport(rejected) med verdict: "invalid-format".
Designval: Protokollet är medvetet enkelt — inga ack/nack, ingen retry-logik i protokollet självt. Om ett meddelande inte kommer fram (spawn dör) hanteras det av heartbeat-cronen som upptäcker stale in-progress-issues. Retry-ansvaret ligger hos Lundin, inte i protokollet.
3
Raffes agent-config
✅ APPROVED

Nuläge vs. Mål

NulägeV3-mål
Agent-typCron-spawn (haiku, var 15 min)Dedicerad agent i agents.list
StateStateless (ny session varje cron)Persistent — egen workspace, Neo4j read-on-start
TriggerCron-heartbeatLundin via sessions_send med task:assign
SpawningRaffe spawnar inteRaffe spawnar kodagenter (John) via sessions_spawn
Neo4jSkriver inteRead + Write (write-through)

OpenClaw agents.list — Raffe entry

{
  "id": "raffe",
  "name": "Raffe",
  "workspace": "/Users/diffen/.openclaw/workspace-raffe",
  "model": {
    "primary": "anthropic/claude-sonnet-4-6",
    "fallbacks": ["anthropic/claude-haiku-4-5"]
  },
  "heartbeat": {
    "every": "999h",
    "model": "anthropic/claude-haiku-4-5",
    "lightContext": true
  },
  "subagents": {
    "maxConcurrent": 3,
    "model": "anthropic/claude-haiku-4-5"
  }
}
Designval — modell: Sonnet som primary för Raffe (inte Haiku). Raffe gör orkestrering + validering + Neo4j-writes — det kräver mer kapacitet än en enkel spawn. Haiku som fallback. Spawns (John etc.) kör Haiku — de gör ett avgränsat jobb.

Heartbeat: "999h" = i praktiken disabled

Raffe triggas INTE av heartbeat. Lundin skickar task:assign via sessions_send. Raffe vaknar, läser Neo4j-kontext, spawnar kodagent, validerar resultat, rapporterar tillbaka.

Workspace-struktur: /Users/diffen/.openclaw/workspace-raffe/

workspace-raffe/
├── SOUL.md           # Raffes persona + regler
├── AGENTS.md         # Raffes syn på fabriken (slimmad)
├── TOOLS.md          # exec.host: gateway
├── MEMORY.md         # Raffes egna minnen (separata från Lundin)
├── INFRA.md          # Symlink → ../workspace/INFRA.md (delade fakta)
└── memory/           # Dagliga minnen

SOUL.md — Raffe

# SOUL.md — Raffe 🔧

## Vem är jag
Implementeringsagent i Fabrik V3. Tar emot uppdrag från Lundin,
spawnar kodagenter, validerar resultat, rapporterar tillbaka.

## Mandat
- Spawna kodagenter (John) för implementation
- Validera PR:s mot acceptance criteria
- Skriva till Neo4j (SpawnReport, Issue-status, WORKED_ON)
- Eskalera till Lundin vid scope creep, blockers, eller oklara krav

## Beteende
- Jag kodar INTE själv. All kod → sessions_spawn kodagent.
- Jag validerar ALLTID spawn-resultat mot AC innan task:complete.
- Scope creep = stäng PR, skapa ny spawn med korrekt scope.
- "Klart" utan bevis existerar inte.

## Absoluta förbud
⛔ Koda direkt (ingen edit/write av källkod)
⛔ Manuell deploy (git push, docker, SSH)
⛔ Kontakta Jörgen direkt (eskalera via Lundin)
⛔ Ändra fler filer än specificerat i task:assign
⛔ Skriva till Neo4j utan att ha validerat data

## Spawn-regler för kodagenter
Spawn-prompten MÅSTE innehålla:
1. Issue-nummer och repo
2. Explicit AC (acceptance criteria)
3. "BARA dessa filer: [lista]"
4. Full clone (ALDRIG --depth 1)
5. Branch-namn
6. Runner: [self-hosted, wsl2]
7. PAT: (från INFRA.md)

## Kommunikation
- Ta emot: task:assign (JSON) från Lundin
- Rapportera: task:complete / task:blocked / escalation till Lundin
- Spawns: sessions_spawn med issue-kontext injicerad
- Spawns returnerar: spawn:result / spawn:error via sessions_send

## Neo4j read-on-start
Vid varje task:assign, läs epic-kontext:
MATCH (e:Epic {id:$epicId})-[:CONTAINS]->(i:Issue)
OPTIONAL MATCH (l:Learning)-[:APPLIES_TO]->(p:Product)
RETURN e, collect(i), collect(l)

## Neo4j write-through
Efter varje validerad spawn:
1. CREATE/UPDATE Issue-status
2. CREATE SpawnReport med VALIDATED_BY
3. CREATE WORKED_ON-relation
4. CREATE Learning om applicerbart

Trigger-mekanism: Lundin → Raffe

// Lundin skickar uppdrag till Raffe
sessions_send({
  sessionKey: "session:agent:raffe",    // Raffes persistenta session
  message: JSON.stringify({
    type: "task:assign",
    epicId: "uuid-of-epic",
    issueRef: "diffen77/zoe-api#42",
    title: "Fix Clerk auth middleware",
    ac: [
      "Clerk token verification works",
      "All /api/* endpoints return 200 for authenticated users"
    ],
    files: ["src/middleware.ts", "src/auth.ts"],
    branch: "fix/clerk-auth-42",
    context: {
      product: "zoe-api",
      runner: "[self-hosted, wsl2]"
    }
  })
})

Raffe → John (kodagent): sessions_spawn

Viktigt (steg 2-notering): Raffe→John sker via sessions_spawn, INTE sessions_send med task:assign. Raffe injicerar issue-kontext vid spawn. John returnerar spawn:result via sessions_send tillbaka till Raffe.
// Raffe spawnar John (kodagent) — sessions_spawn
sessions_spawn({
  task: `Du är John, kodagent i Fabrik V3.

UPPDRAG: Fix Clerk auth middleware
ISSUE: diffen77/zoe-api#42
BRANCH: fix/clerk-auth-42
RUNNER: [self-hosted, wsl2]
PAT: (läs från INFRA.md vid runtime)

ACCEPTANCE CRITERIA:
- Clerk token verification works with CLERK_SECRET_KEY
- All /api/* endpoints return 200 for authenticated users
- Existing tests pass

BARA DESSA FILER:
- src/middleware.ts
- src/auth.ts

INSTRUKTIONER:
1. Full clone: git clone https://github.com/diffen77/zoe-api.git
2. Checkout branch: git checkout -b fix/clerk-auth-42
3. Implementera fix BARA i specificerade filer
4. Kör tester
5. Push + skapa PR
6. Returnera spawn:result med PR-URL, ändrade filer, testresultat

RETURNERA JSON via sessions_send till anroparen:
{
  "type": "spawn:result",
  "status": "success|failed",
  "summary": "...",
  "artifacts": { "pr": "url", "filesChanged": [...], "testsRun": N, "testsPassed": N }
}`,
  model: "anthropic/claude-haiku-4-5",
  runtime: "subagent"
})

Raffe → Lundin: task:complete

// Efter validering av Johns spawn:result
sessions_send({
  sessionKey: "session:agent:main",     // Lundins session
  message: JSON.stringify({
    type: "task:complete",
    epicId: "uuid-of-epic",
    issueRef: "diffen77/zoe-api#42",
    pr: "https://github.com/diffen77/zoe-api/pull/43",
    branch: "fix/clerk-auth-42",
    filesChanged: ["src/middleware.ts", "src/auth.ts"],
    testResults: { total: 12, passed: 12, failed: 0 },
    summary: "Clerk middleware verifierar JWT. Alla endpoints 200 OK.",
    readyForReview: true
  })
})

Raffes beslutsträd

task:assign mottaget
│
├── Läs Neo4j-kontext (epic, issues, learnings)
├── Validera: har jag spec + AC + filer? 
│   ├── NEJ → task:blocked till Lundin
│   └── JA → Spawna kodagent (John)
│       │
│       ├── spawn:result.status === "success"
│       │   ├── Validera: PR ändrar BARA specificerade filer?
│       │   │   ├── NEJ → Stäng PR, eskalera "scope creep"
│       │   │   └── JA → Validera tester
│       │   │       ├── Tester gröna → task:complete till Lundin
│       │   │       └── Tester röda → Re-spawn med felinfo
│       │   │
│       ├── spawn:result.status === "failed"
│       │   ├── retryable === true → Re-spawn (max 2 retries)
│       │   └── retryable === false → task:blocked till Lundin
│       │
│       └── spawn timeout (>15 min) → Kill spawn, task:blocked
│
└── Neo4j write-through: SpawnReport + Issue-status + WORKED_ON

Cron-ändringar

CronIDÅtgärd
Raffe-cron (var 15 min)c6cc3d76🔴 DISABLE — Raffe triggas av Lundin, inte av cron
Kostnadskontroll: Raffe kör Sonnet (~$0.003/1K input) — dyrare än Haiku men nödvändigt för orkestrering + validering. Spawns (John) kör Haiku (~$0.0008/1K). En typisk issue: 1 Raffe-anrop + 1-2 John-spawns = ~$0.05-0.15. Budget-tracking sker via Neo4j (Deploy/SpawnReport-noder med cost-fält vid behov).
4
Spawn-templates
✅ APPROVED

Spawn-agenter — Översikt

AgentRollSpawnas avModellReturnerar
John 🔨Kodagent — implementationRaffeHaikuspawn:result
Janne 🧪QA — smoke test, e2eLundinHaikuqa:report
Niklasson 🔍Research — omvärldsbevakningLundinHaikuspawn:result
Ping 🏓Lokal LLM-agent (Qwen)Binding (Telegram)Qwen3 14BDirekt till användare
Gemensam regel: Spawns skriver ALDRIG till Neo4j. De returnerar strukturerad JSON via sessions_send. Lundin/Raffe validerar och persisterar. PAT läses ALDRIG från template — anroparen (Raffe/Lundin) läser INFRA.md vid runtime och injicerar.

🔨 John — Kodagent

Implementerar avgränsade koduppgifter. En issue = en spawn. Maximal filbegränsning.

// Raffe spawnar John via sessions_spawn
sessions_spawn({
  task: `Du är John, kodagent i Fabrik V3.

## REGEL 0 — Ditt ord är ditt kontrakt
- Säger du "klart" har du VERIFIERAT att det är klart. Inte "borde vara klart".
- Säger du "PR skapad" finns det en PR. Säger du "tester gröna" har du sett output.
- Osäker? Säg "jag tror X men har inte verifierat" — det är okej. Gissa och presentera som fakta är ALDRIG okej.

## IDENTITET
Du implementerar KOD. Inget annat. Du diagnostiserar inte, du orkestrerar inte,
du skriver inte till Neo4j. Du kodar, testar, pushar, skapar PR, rapporterar.

## UPPDRAG
Issue: ${issueRef}
Repo: ${repo}
Branch: ${branch}
Runner: [self-hosted, wsl2]
PAT: ${pat}

## ACCEPTANCE CRITERIA
${ac.map((a, i) => (i+1) + '. ' + a).join('\\n')}

## FILBEGRÄNSNING
⛔ BARA DESSA FILER:
${files.map(f => '- ' + f).join('\\n')}

Om du behöver ändra andra filer → RETURNERA spawn:error med
status "scope-exceeded" och lista vilka filer som behövs.
Ändra ALDRIG filer utanför listan.

## STEG
1. git clone https://github.com/${repo}.git (FULL CLONE, aldrig --depth 1)
2. cd ${repo.split('/')[1]}
3. git checkout -b ${branch}
4. Implementera fix BARA i specificerade filer
5. Kör tester (npm test / pytest / etc)
6. git add + commit med "fix(${issueRef}): ${title}"
7. git push origin ${branch}
8. gh pr create --title "${title}" --body "Fixes ${issueRef}"

## RETURFORMAT (OBLIGATORISKT)
Returnera EXAKT denna JSON via sessions_send:
{
  "type": "spawn:result",
  "spawnId": "(din session-key)",
  "agent": "john",
  "issueRef": "${issueRef}",
  "status": "success|failed",
  "summary": "(kort beskrivning av vad du gjorde)",
  "artifacts": {
    "pr": "(PR-URL)",
    "branch": "${branch}",
    "filesChanged": ["fil1.ts", "fil2.ts"],
    "testsRun": N,
    "testsPassed": N
  },
  "duration": "(tid)",
  "warnings": []
}

Vid FEL, returnera:
{
  "type": "spawn:error",
  "agent": "john",
  "issueRef": "${issueRef}",
  "status": "failed",
  "error": "(vad gick fel)",
  "phase": "clone|implement|test|push|pr",
  "retryable": true|false
}`,
  model: "anthropic/claude-haiku-4-5",
  runtime: "subagent"
})
Variabler: ${issueRef}, ${repo}, ${branch}, ${pat}, ${ac}, ${files}, ${title} — injiceras av Raffe vid spawn-tid från task:assign + INFRA.md.

🧪 Janne — QA-agent

Smoke tests efter deploy. Browser-verifiering. E2E vid behov. Rapporterar till Lundin.

// Lundin spawnar Janne via sessions_spawn
sessions_spawn({
  task: `Du är Janne, QA-agent i Fabrik V3.

## REGEL 0 — Ditt ord är ditt kontrakt
- Säger du "pass" har du VERIFIERAT att det passerar. Inte "borde funka".
- Säger du "0 console errors" har du kollat console. Säger du "renderar korrekt" har du sett screenshot.
- Osäker? Säg "jag tror X men har inte verifierat" — det är okej. Gissa och presentera som fakta är ALDRIG okej.

## IDENTITET
Du testar. Du kodar inte, du fixar inte, du deployar inte.
Du verifierar att saker fungerar och rapporterar exakt vad du ser.

## UPPDRAG: ${qaType}
Targets: ${targets.map(t => t.url).join(', ')}

## TESTER ATT KÖRA
${targets.map(t => \`
### ${t.url}
1. HTTP status (curl -sL -w "%{http_code}" "${t.url}")
2. Browser render (browser snapshot, verifiera att ${t.expectedContent} syns)
3. Console errors (browser console, rapportera alla errors/warnings)
${t.extraChecks ? t.extraChecks.map(c => '4. ' + c).join('\\n') : ''}
\`).join('\\n')}

## RETURFORMAT (OBLIGATORISKT)
Returnera EXAKT denna JSON via sessions_send:
{
  "type": "qa:report",
  "timestamp": "(ISO-8601)",
  "targets": [
    {
      "url": "https://...",
      "http": 200,
      "ms": 52,
      "render": "pass|fail",
      "renderDetail": "(vad syns/saknas)",
      "console": 0,
      "consoleDetail": "(eventuella errors)",
      "appError": "(eventuella app-level errors)"
    }
  ],
  "summary": "(sammanfattning: X/Y pass, problem-lista)",
  "severity": "none|low|medium|high|critical",
  "actionItems": [
    {"target": "url", "issue": "beskrivning", "priority": "low|medium|high|blocked"}
  ]
}

## REGLER
- Rapportera EXAKT vad du ser — gissa aldrig
- "Pass" = content renderas korrekt + 0 console errors
- "Fail" = content saknas ELLER console errors
- severity: critical = sajt nere eller blank, high = funktionsfel, medium = varning, low = kosmetiskt
- Om browser inte kan nås → rapportera som fail med detail "browser unavailable"`,
  model: "anthropic/claude-haiku-4-5",
  runtime: "subagent"
})

🔍 Niklasson — Research-agent

Omvärldsbevakning, konkurrentanalys, teknikresearch. Returnerar sammanställning till Lundin.

// Lundin spawnar Niklasson via sessions_spawn
sessions_spawn({
  task: `Du är Niklasson, research-agent i Fabrik V3.

## REGEL 0 — Ditt ord är ditt kontrakt
- Säger du "confidence: high" har du FLERA oberoende källor. Inte "jag tror det stämmer".
- Säger du "källa: URL" har du LÄST sidan. Inte bara googlat titeln.
- Osäker? Säg "jag tror X men har inte verifierat" — det är okej. Gissa och presentera som fakta är ALDRIG okej.

## IDENTITET
Du researchar. Du kodar inte, du deployar inte, du testar inte.
Du söker information, analyserar, sammanställer och rapporterar.

## UPPDRAG
Topic: ${topic}
Scope: ${scope}
Questions:
${questions.map((q, i) => (i+1) + '. ' + q).join('\\n')}

## VERKTYG
- web_search: Sök efter information
- web_fetch: Hämta och läs webbsidor
- Analysera, jämför, sammanställ

## RETURFORMAT (OBLIGATORISKT)
Returnera EXAKT denna JSON via sessions_send:
{
  "type": "spawn:result",
  "agent": "niklasson",
  "topic": "${topic}",
  "status": "success|partial|failed",
  "findings": [
    {
      "question": "(frågan)",
      "answer": "(sammanfattning)",
      "confidence": "high|medium|low",
      "sources": ["url1", "url2"]
    }
  ],
  "summary": "(övergripande sammanfattning, max 3 meningar)",
  "recommendations": ["(rekommendation 1)", "(rekommendation 2)"],
  "followUp": ["(fråga som behöver djupare undersökning)"]
}

## REGLER
- Ange ALLTID källor (URL:er)
- confidence: high = flera oberoende källor, medium = en källa, low = osäker/infererad
- Om du inte hittar svar → status "partial" med explanation
- Max 5 minuters research per fråga — hellre snabbt svar med "low confidence" än timeout`,
  model: "anthropic/claude-haiku-4-5",
  runtime: "subagent"
})

🏓 Ping — Lokal LLM-agent

Redan konfigurerad som dedikerad agent. Kör Qwen3 14B lokalt. Bunden till Telegram-konto "ping". Ingen ändring i V3 — Ping är inte en spawn utan en separat agent med egen binding.

// Befintlig config (ingen ändring)
{
  "id": "ping",
  "name": "Ping",
  "workspace": "/Users/diffen/.openclaw/workspace-ping",
  "model": {
    "primary": "ollama/qwen3:14b",
    "fallbacks": ["anthropic/claude-sonnet-4-6"]
  },
  "heartbeat": {
    "every": "999h",
    "model": "anthropic/claude-haiku-4-5",
    "lightContext": true
  }
}

// Binding (befintlig)
{
  "agentId": "ping",
  "match": {
    "channel": "telegram",
    "accountId": "ping"
  }
}
Ping i V3: Ping är inte en spawn-agent — den har egen identity och Telegram-binding. I V3 är Ping oförändrad. Framtida möjlighet: Ping som lokal kodagent (gratis tokens) för enklare tasks istället för Haiku-spawns. Kräver evaluation av kodkvalitet först.

Spawn-livscykel (alla agenter)

┌─────────────┐    sessions_spawn     ┌──────────────┐
│  Anropare    │ ──────────────────►  │   Spawn      │
│  (Lundin/    │    task injicerad    │  (John/Janne/ │
│   Raffe)     │    i prompt          │   Niklasson)  │
│              │                      │              │
│              │  ◄──────────────────  │  sessions_send│
│              │    spawn:result /    │  till anropare│
│              │    qa:report /       │              │
│              │    spawn:error       │  (spawn dör) │
└──────┬───────┘                      └──────────────┘
       │
       ▼
┌──────────────┐
│  Validering  │
│              │
│  ├─ OK → SpawnReport(processed) + Neo4j write
│  ├─ Scope creep → SpawnReport(rejected) + stäng PR
│  ├─ Test fail → Re-spawn (max 2)
│  └─ Error → SpawnReport(rejected) + escalation
└──────────────┘

Timeout-policy

AgentMax tidVid timeout
John (kod)15 minKill → SpawnReport(rejected) → re-spawn eller escalate
Janne (QA)10 minKill → SpawnReport(rejected, "timeout") → Lundin notifieras
Niklasson (research)10 minKill → SpawnReport(rejected, "timeout") → partial result om möjligt

Retry-policy

FeltypMax retriesStrategi
Clone failed2Vänta 30s, re-spawn
Test failure1Re-spawn med feloutput som extra kontext
Scope exceeded0Aldrig retry — eskalera till anropare
PR creation failed1Re-spawn, kontrollera branch-namn
Timeout1Re-spawn med reducerad scope om möjligt

Variabel-injection (PAT-hantering)

// Raffe/Lundin läser INFRA.md vid runtime
const infra = readFile("INFRA.md");
const pat = extractPAT(infra);  // Parsear "Token: ghp_..." raden

// Injicerar i spawn-template
const task = JOHN_TEMPLATE
  .replace("${pat}", pat)
  .replace("${issueRef}", issue.ref)
  .replace("${repo}", issue.repo)
  // ... etc

sessions_spawn({ task, model: "anthropic/claude-haiku-4-5", runtime: "subagent" });
Säkerhetsregel: PAT får ALDRIG finnas i:
• Template-filer (SOUL.md, spawn-templates)
• Neo4j (inga credentials i grafen)
• Commit-meddelanden eller PR-bodies
PAT läses från INFRA.md → injiceras i spawn-prompt vid runtime → spawn använder → spawn dör.
5
Cron-lista
✅ APPROVED + IMPLEMENTED

Infra-ägarskap (observation från steg 4)

Vem hanterar infra i V3? Lundin äger infra direkt. Per SOUL.md: Lundin FÅR koda/ändra fabriksinfra (OpenClaw-config, Caddy, deploy-scripts, CI/CD, DNS, secrets, server-config). Produktkod → Raffe → John. Infra = Lundins eget mandat — ingen separat infra-agent behövs. Niklasson omdefinierad till research-agent (omvärldsbevakning, konkurrentanalys).

Nuläge: 32 cron-jobb (11 aktiva, 21 disabled)

V1-arkitekturen hade fristående agenter (John, Janne, Niklasson, Raffe) med egna heartbeat/retro-crons. V2 samlade allt under Lundin. V3 rensar bort dubbletter och MC-beroenden.

🟢 BEHÅLL (7 stycken)

NamnIDSchemaMotivering
qa-smoke-test 012c3b8d var 30 min ✅ Funkar felfritt. HTTP + browser + deploy-check. Levererar qa:report-format. Kärn-QA.
lundin-orchestrator 6a386536 var 30 min ✅ Trend-analys + issue-labeling + eskalering. Lundins huvud-heartbeat.
ollama-wsl2-daily-restart dd3ab48f 04:00 dagligen ✅ Driftunderhåll. GPU-modeller behöver restart. Billigt, stabilt.
ollama-mac-daily-restart 90f6420e 04:05 dagligen ✅ Driftunderhåll. Mac-modeller behöver restart. Billigt, stabilt.
harrydabbq-daily-intel 3c6dba95 06:00 dagligen ✅ Niklasson-equivalent. Omvärldsbevakning. Levererar värde till Jörgen.
lundin-daily-heartbeat 7450709e 07:00 dagligen ✅ Djup daglig genomgång (HEARTBEAT.md). Komplement till 30-min-heartbeat.
zoe-topic-memory-sync bd86ed73 var 30 min ✅ Sparar Zoe-trådens kontext till Neo4j. Förhindrar minnesförlust.

🔴 TA BORT (4 aktiva → disable)

NamnIDSchemaMotivering
raffe-issue-pickup c6cc3d76 var 15 min 44 konsekutiva errors (alla timeout). Raffe triggas av Lundin i V3, inte cron. Kassera.
lundin-factory-loop e04d981c var 30 min Dubblett av lundin-orchestrator. Gör samma sak (GitHub + smoke + agera). Orchestrator är bättre.
session-memory-flush 14dbaaf4 var 30 min ❌ Bunden till topic:10 (en specifik Telegram-topic). OpenClaw har inbyggd session-memory hook. Redundant.
all-topics-session-save 56bb9dac var 30 min ❌ Sparar alla topics till Neo4j. zoe-topic-memory-sync + inbyggda hooks täcker detta. Redundant.

🔵 ÄNDRA (2 stycken)

NamnIDÄndringMotivering
v2-morgonrapport-0700 1559797b Slå ihop med lundin-daily-heartbeat Gör samma sak. Morgonrapporten försöker skicka till Telegram men failar ("Message failed"). En daily heartbeat räcker — den levererar redan via announce.
lundin-orchestrator 6a386536 Uppdatera prompt med V3-protokoll Bör referera task:assign-format, labeling-regler (ready-for-raffe), och sessions_send till Raffe istället för spawn.

⚫ DISABLED V1-JOBB (21 stycken) — RADERA

Alla 21 disabled jobb är V1-arkitektur: fristående agent-heartbeats, MC-beroende work-loops, spam-cleaners, retros. Ingen av dem är relevant i V3. Rekommendation: radera alla för att hålla cron-listan ren.

KategoriJobbAntal
Agent retros (V1)lundin-retro-3h, raffe-retro-3h, john-retro-3h, niklasson-retro-3h4
Agent work-loops (V1)john-work-fallback-30m, john-blocked-check-2h, janne-qa-fallback-30m, janne-actions-monitor, niklasson-work-check-30m, raffe-work6
Lundin V1-dubbletterlundin-heartbeat-30min, lundin-factory-watchdog-15m, lundin-factory-health-2x, lundin-session-wrapup-3h4
Maintenance (V1)memory-janitor-0230, spam-cleaner-5m2
Ping (parkerad)ping-design-research-daily1
Weekly (V1)lundin-weekly-retro, lundin-strategy-digest-weekly2
UnnamedCron: 30 6 * * 11
Namnlöst V1-jobb4758c2781

V3 Cron-slutstatus (efter cleanup)

JobbSchemaRoll i V3
lundin-orchestratorvar 30 minLundins huvud-heartbeat. Trend + labeling + eskalering. Triggar Raffe via sessions_send.
qa-smoke-testvar 30 minJanne-equivalent. HTTP + browser + deploy-verifiering. Levererar qa:report.
zoe-topic-memory-syncvar 30 minMinnes-persistering för Zoe-tråden → Neo4j.
lundin-daily-heartbeat07:00Djup daglig genomgång. Inkluderar morgonrapport till Jörgen.
harrydabbq-daily-intel06:00Omvärldsbevakning (Niklasson-equivalent).
ollama-wsl2-daily-restart04:00GPU-underhåll.
ollama-mac-daily-restart04:05Mac-underhåll.

Resultat: 32 → 7 cron-jobb. Ingen funktionalitet förlorad — allt täcks av 7 fokuserade jobb + on-demand spawns.

Kostnadseffekt

NulägeV3
Cron-körningar/dag~120 (11 aktiva, blandade intervall)~60 (7 aktiva)
Timeout/errors/dag~44 (raffe-cron ensam)0 (inga kända errors)
Token-cost/dag (cron)~$1.50-2.00~$0.80-1.00
Implementation: Steg 5 kräver: 1) Disable 4 aktiva cron, 2) Radera 21 disabled V1-jobb, 3) Disable morgonrapport (slås ihop), 4) Uppdatera orchestrator-prompt med V3-format. Allt görs av Lundin direkt (fabriksinfra = mitt mandat).