Hybrid-arkitektur med persistent state + on-demand spawns. Senast uppdaterad 2026-03-30 10:05 UTC. ✅ ALLA 5 STEG IMPLEMENTERADE.
Lundin + Raffe = dedicerade agenter med persistent state. John, Janne, Niklasson, Ping = on-demand spawns.
Raffe-cron disablas. Raffe blir dedicerad agent i agents.list, triggad av Lundin.
Write-through, read-on-start, gemensamt schema. Lundin + Raffe skriver direkt.
Spawns skriver ALDRIG till Neo4j. Artefakt i GitHub + rapport via sessions_send. Validering + Neo4j-write = Lundin/Raffe.
Factory-loop + orchestrator slås ihop. Raffe-cron disablas. Resten utvärderas.
| Nod | Syfte | Primärnyckel |
|---|---|---|
Brief NY | Kundintag / affärsbehov — blir Epic vid GO | id (UUID) |
Epic | Stort arbetspaket (godkänd Brief) | id (UUID) |
Issue | GitHub Issue, 1:1-mappning | repo + number |
Decision | Arkitektur-/affärsbeslut | id (UUID) |
Deploy | Varje deploy till produktion | id (UUID) |
Agent | Persistent agent eller spawn-register | name |
Product | Repo/produkt | slug |
Incident | Driftstörning | id (UUID) |
Learning | Lärdom som påverkar framtida beslut | id (UUID) |
SpawnReport NY | Strukturerad rapport från on-demand spawn | id (UUID) |
| Från | Relation | Till | Egenskaper |
|---|---|---|---|
| Brief | BECAME NY | Epic | approvedBy, at |
| Brief | FOR_PRODUCT NY | Product | — |
| Epic | CONTAINS | Issue | order |
| Issue | DEPENDS_ON | Issue | type (blocks/needs) |
| Issue | BELONGS_TO | Product | — |
| Decision | AFFECTS | Product | — |
| Decision | MADE_BY | Agent | at |
| Deploy | DEPLOYS | Product | commit, runId, at |
| Deploy | TRIGGERED_BY | Issue | — |
| Agent | WORKED_ON | Issue | role, at |
| Agent | SPAWNED | Agent | task, at, sessionKey |
| SpawnReport | REPORTED_BY NY | Agent | — |
| SpawnReport | CONCERNS NY | Issue | — |
| SpawnReport | VALIDATED_BY NY | Agent | at, verdict |
| Incident | AFFECTED | Product | duration, severity |
| Incident | RESOLVED_BY | Issue | — |
| Learning | LEARNED_FROM | Issue / Incident / Decision | — |
| Learning | APPLIES_TO | Product | — |
created: datetime() updated: datetime() source: string // "lundin", "raffe", "heartbeat", "spawn-report" status: string // nodtyp-specifikt
| Nod | Tillåtna status |
|---|---|
| Brief NY | draft · proposed · approved · rejected · parked |
| Epic | planning · active · done · parked |
| Issue | backlog · ready · in-progress · in-review · done · blocked |
| Decision | proposed · approved · superseded |
| Deploy | success · failure · rollback |
| Incident | active · mitigated · resolved |
| SpawnReport NY | received · processed · rejected |
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);
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
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.
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.
| type | Riktning | Syfte |
|---|---|---|
task:assign | Lundin → Raffe | Nytt uppdrag med epic/issue-kontext |
task:update | Raffe → Lundin | Statusuppdatering under pågående arbete |
task:complete | Raffe → Lundin | Uppdrag klart, PR skapad |
task:blocked | Raffe → Lundin | Blockerad, behöver beslut |
spawn:result | Spawn → Lundin/Raffe | Slutrapport från on-demand spawn |
spawn:error | Spawn → Lundin/Raffe | Spawn misslyckades |
qa:report | QA-spawn → Lundin | Testresultat efter smoke/e2e |
escalation | Raffe → Lundin | Kräver Lundins beslut eller eskalering till Jörgen |
{
"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"
}
{
"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": []
}
{
"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
}
{
"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"
}
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"}
]
}
{
"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
}
{
"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"
}
{
"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 | Betydelse | Mottagarens 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: | Kvalitetsrapporter | Logga i Neo4j, eskalera vid critical severity |
escalation | Kräver beslut från högre nivå | Lundin tar beslut eller eskalerar till Jörgen |
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
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)
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
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".
in-progress-issues. Retry-ansvaret ligger hos Lundin, inte i protokollet.
| Nuläge | V3-mål | |
|---|---|---|
| Agent-typ | Cron-spawn (haiku, var 15 min) | Dedicerad agent i agents.list |
| State | Stateless (ny session varje cron) | Persistent — egen workspace, Neo4j read-on-start |
| Trigger | Cron-heartbeat | Lundin via sessions_send med task:assign |
| Spawning | Raffe spawnar inte | Raffe spawnar kodagenter (John) via sessions_spawn |
| Neo4j | Skriver inte | Read + Write (write-through) |
{
"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"
}
}
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-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 🔧
## 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
// 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]"
}
})
})
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"
})
// 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
})
})
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 | ID | Åtgärd |
|---|---|---|
| Raffe-cron (var 15 min) | c6cc3d76 | 🔴 DISABLE — Raffe triggas av Lundin, inte av cron |
| Agent | Roll | Spawnas av | Modell | Returnerar |
|---|---|---|---|---|
| John 🔨 | Kodagent — implementation | Raffe | Haiku | spawn:result |
| Janne 🧪 | QA — smoke test, e2e | Lundin | Haiku | qa:report |
| Niklasson 🔍 | Research — omvärldsbevakning | Lundin | Haiku | spawn:result |
| Ping 🏓 | Lokal LLM-agent (Qwen) | Binding (Telegram) | Qwen3 14B | Direkt till användare |
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.
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"
})
${issueRef}, ${repo}, ${branch}, ${pat}, ${ac}, ${files}, ${title} — injiceras av Raffe vid spawn-tid från task:assign + INFRA.md.
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"
})
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"
})
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"
}
}
┌─────────────┐ 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
└──────────────┘
| Agent | Max tid | Vid timeout |
|---|---|---|
| John (kod) | 15 min | Kill → SpawnReport(rejected) → re-spawn eller escalate |
| Janne (QA) | 10 min | Kill → SpawnReport(rejected, "timeout") → Lundin notifieras |
| Niklasson (research) | 10 min | Kill → SpawnReport(rejected, "timeout") → partial result om möjligt |
| Feltyp | Max retries | Strategi |
|---|---|---|
| Clone failed | 2 | Vänta 30s, re-spawn |
| Test failure | 1 | Re-spawn med feloutput som extra kontext |
| Scope exceeded | 0 | Aldrig retry — eskalera till anropare |
| PR creation failed | 1 | Re-spawn, kontrollera branch-namn |
| Timeout | 1 | Re-spawn med reducerad scope om möjligt |
// 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" });
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.
| Namn | ID | Schema | Motivering |
|---|---|---|---|
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. |
| Namn | ID | Schema | Motivering |
|---|---|---|---|
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. |
| Namn | ID | Ändring | Motivering |
|---|---|---|---|
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. |
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.
| Kategori | Jobb | Antal |
|---|---|---|
| Agent retros (V1) | lundin-retro-3h, raffe-retro-3h, john-retro-3h, niklasson-retro-3h | 4 |
| Agent work-loops (V1) | john-work-fallback-30m, john-blocked-check-2h, janne-qa-fallback-30m, janne-actions-monitor, niklasson-work-check-30m, raffe-work | 6 |
| Lundin V1-dubbletter | lundin-heartbeat-30min, lundin-factory-watchdog-15m, lundin-factory-health-2x, lundin-session-wrapup-3h | 4 |
| Maintenance (V1) | memory-janitor-0230, spam-cleaner-5m | 2 |
| Ping (parkerad) | ping-design-research-daily | 1 |
| Weekly (V1) | lundin-weekly-retro, lundin-strategy-digest-weekly | 2 |
| Unnamed | Cron: 30 6 * * 1 | 1 |
| Namnlöst V1-jobb | 4758c278 | 1 |
| Jobb | Schema | Roll i V3 |
|---|---|---|
lundin-orchestrator | var 30 min | Lundins huvud-heartbeat. Trend + labeling + eskalering. Triggar Raffe via sessions_send. |
qa-smoke-test | var 30 min | Janne-equivalent. HTTP + browser + deploy-verifiering. Levererar qa:report. |
zoe-topic-memory-sync | var 30 min | Minnes-persistering för Zoe-tråden → Neo4j. |
lundin-daily-heartbeat | 07:00 | Djup daglig genomgång. Inkluderar morgonrapport till Jörgen. |
harrydabbq-daily-intel | 06:00 | Omvärldsbevakning (Niklasson-equivalent). |
ollama-wsl2-daily-restart | 04:00 | GPU-underhåll. |
ollama-mac-daily-restart | 04:05 | Mac-underhåll. |
Resultat: 32 → 7 cron-jobb. Ingen funktionalitet förlorad — allt täcks av 7 fokuserade jobb + on-demand spawns.
| Nuläge | V3 | |
|---|---|---|
| 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 |