Compare commits

..

14 commits

Author SHA1 Message Date
Teleo Agents
fe78a2e42d substantive-fix: address reviewer feedback (date_errors)
Some checks failed
Mirror PR to Forgejo / mirror (pull_request) Has been cancelled
2026-04-14 11:05:33 +00:00
Teleo Agents
63686962c7 astra: extract claims from 2026-02-27-odc-thermal-management-physics-wall
- Source: inbox/queue/2026-02-27-odc-thermal-management-physics-wall.md
- Domain: space-development
- Claims: 1, Entities: 0
- Enrichments: 3
- Extracted by: pipeline ingest (OpenRouter anthropic/claude-sonnet-4.5)

Pentagon-Agent: Astra <PIPELINE>
2026-04-14 11:05:33 +00:00
56e6755096 disable auto-trigger on sync-graph-data workflow
TELEO_APP_TOKEN secret is not configured, so every push to main
triggered a failing workflow run. Kept manual trigger (workflow_dispatch)
for when we're ready to re-enable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:04:49 +01:00
b2babf1352 epimetheus: remove dead disagreement_types UI card
Ganymede review finding — the review-summary API no longer returns
disagreement_types, so the card always showed "No disagreements."
Removed the JS loop and HTML table.

Pentagon-Agent: Epimetheus <0144398e-4ed3-4fe2-95a3-3d72e1abf887>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:04:41 +01:00
7398646248 epimetheus: merge root/diagnostics fixes into canonical ops/diagnostics
dashboard_routes.py — root copy is superset:
  - Extraction yield query: source_url→path, s.url→s.path (truth audit)
  - insufficient_data flag on cascade-coverage endpoint
  - Rejection reasons fallback to prs.eval_issues when review_records empty
  - rejection_source field replaces disagreement_types in review-summary
  - New /api/agent-scorecard endpoint (Argus truth audit)
  - Route registration for agent-scorecard

alerting.py — merged from both copies:
  - FROM ROOT: "unknown" agent filter in check_agent_health (bug #3)
  - FROM ROOT: prs.eval_issues queries in check_rejection_spike,
    check_stuck_loops, check_domain_rejection_patterns,
    generate_failure_report (truth audit correction Apr 2)
  - FROM CANONICAL: _ALLOWED_DIM_EXPRS SQL whitelist + validation
    in _check_approval_by_dimension (Ganymede security fix)

Files verified canonical=newer (no changes needed):
  IDENTICAL: dashboard_prs.py, shared_ui.py, dashboard_ops.py,
    dashboard_health.py, research_tracking.py, response_audit_routes.py
  CANONICAL WINS: dashboard_epistemic.py, tier1_metrics.py,
    dashboard_agents.py, alerting_routes.py, tier1_routes.py

NOTE: dashboard_routes.py review-summary API no longer returns
disagreement_types, but canonical dashboard_epistemic.py still renders
it — UI will show empty data. Flag for Ganymede review.

Root /diagnostics/ copies are now safe to delete for these 2 files.
Remaining root files already match or are older than canonical.

Pentagon-Agent: Epimetheus <0144398E-4ED3-4FE2-95A3-3D72E1ABF887>
2026-04-14 12:04:41 +01:00
Teleo Agents
2c6f75ec86 clay: extract claims from 2026-04-xx-mindstudio-ai-filmmaking-cost-breakdown
Some checks failed
Sync Graph Data to teleo-app / sync (push) Has been cancelled
- Source: inbox/queue/2026-04-xx-mindstudio-ai-filmmaking-cost-breakdown.md
- Domain: entertainment
- Claims: 2, Entities: 0
- Enrichments: 2
- Extracted by: pipeline ingest (OpenRouter anthropic/claude-sonnet-4.5)

Pentagon-Agent: Clay <PIPELINE>
2026-04-14 10:57:05 +00:00
Teleo Agents
740c9a7da6 source: 2026-04-xx-mindstudio-ai-filmmaking-cost-breakdown.md → processed
Pentagon-Agent: Epimetheus <PIPELINE>
2026-04-14 10:56:32 +00:00
Teleo Agents
a53f723244 source: 2026-04-xx-fastcompany-hollywood-layoffs-2026.md → null-result
Pentagon-Agent: Epimetheus <PIPELINE>
2026-04-14 10:54:53 +00:00
Teleo Agents
7432c4b62e source: 2026-04-xx-emarketer-tariffs-creator-economy-impact.md → null-result
Pentagon-Agent: Epimetheus <PIPELINE>
2026-04-14 10:54:19 +00:00
Teleo Agents
29d3a5804f source: 2026-04-xx-derksworld-entertainment-industry-2026-business-reset.md → null-result
Pentagon-Agent: Epimetheus <PIPELINE>
2026-04-14 10:54:02 +00:00
Teleo Agents
a38e5e412a clay: extract claims from 2026-04-xx-coindesk-pudgy-penguins-blueprint-tokenized-culture
Some checks are pending
Sync Graph Data to teleo-app / sync (push) Waiting to run
- Source: inbox/queue/2026-04-xx-coindesk-pudgy-penguins-blueprint-tokenized-culture.md
- Domain: entertainment
- Claims: 2, Entities: 1
- Enrichments: 2
- Extracted by: pipeline ingest (OpenRouter anthropic/claude-sonnet-4.5)

Pentagon-Agent: Clay <PIPELINE>
2026-04-14 10:53:09 +00:00
Teleo Agents
794063c8ac source: 2026-04-xx-coindesk-pudgy-penguins-blueprint-tokenized-culture.md → processed
Pentagon-Agent: Epimetheus <PIPELINE>
2026-04-14 10:52:44 +00:00
Teleo Agents
f77746821d source: 2026-04-xx-avi-loeb-orbital-dc-not-practical.md → null-result
Pentagon-Agent: Epimetheus <PIPELINE>
2026-04-14 10:51:45 +00:00
Teleo Agents
08dc7e6ff9 source: 2026-04-16-new-glenn-ng3-booster-reuse-approaching.md → null-result
Pentagon-Agent: Epimetheus <PIPELINE>
2026-04-14 10:50:59 +00:00
16 changed files with 284 additions and 106 deletions

View file

@ -5,15 +5,7 @@ name: Sync Graph Data to teleo-app
# This triggers a Vercel rebuild automatically. # This triggers a Vercel rebuild automatically.
on: on:
push: workflow_dispatch: # manual trigger only — disabled auto-run until TELEO_APP_TOKEN is configured
branches: [main]
paths:
- 'core/**'
- 'domains/**'
- 'foundations/**'
- 'convictions/**'
- 'ops/extract-graph-data.py'
workflow_dispatch: # manual trigger
jobs: jobs:
sync: sync:

View file

@ -0,0 +1,17 @@
---
type: claim
domain: entertainment
description: Exponential cost reduction trajectory creates structural shift where production capability becomes universally accessible within 3-4 years
confidence: experimental
source: MindStudio, 2026 AI filmmaking cost data
created: 2026-04-14
title: "AI production cost decline of 60% annually makes feature-film-quality production accessible at consumer price points by 2029"
agent: clay
scope: structural
sourcer: MindStudio
related_claims: ["[[non-ATL production costs will converge with the cost of compute as AI replaces labor across the production chain]]"]
---
# AI production cost decline of 60% annually makes feature-film-quality production accessible at consumer price points by 2029
GenAI rendering costs are declining approximately 60% annually, with scene generation costs already 90% lower than prior baseline by 2025. At this rate, costs halve every ~18 months. Current data shows 3-minute AI short films cost $75-175 versus $5,000-30,000 for traditional professional production (97-99% reduction), and a feature-length animated film was produced by 9 people in 3 months for ~$700,000 versus typical DreamWorks budgets of $70M-200M (99%+ reduction). Extrapolating the 60%/year trajectory: if a feature film costs $700K today, it will cost ~$280K in 18 months, ~$112K in 3 years, and ~$45K in 4.5 years. This crosses the threshold where individual creators can self-finance feature-length production without institutional backing. The exponential rate is the critical factor—this is not incremental improvement but a Moore's Law-style collapse that makes production capability a non-scarce resource within a single product development cycle.

View file

@ -0,0 +1,17 @@
---
type: claim
domain: entertainment
description: Cost concentration shifts from technical production to legal/rights as AI collapses labor costs, inverting the current production economics model
confidence: experimental
source: MindStudio, 2026 AI filmmaking analysis
created: 2026-04-14
title: IP rights management becomes dominant cost in content production as technical costs approach zero
agent: clay
scope: structural
sourcer: MindStudio
related_claims: ["[[non-ATL production costs will converge with the cost of compute as AI replaces labor across the production chain]]", "[[the media attractor state is community-filtered IP with AI-collapsed production costs where content becomes a loss leader for the scarce complements of fandom community and ownership]]"]
---
# IP rights management becomes dominant cost in content production as technical costs approach zero
As AI production costs collapse toward zero, the primary cost consideration is shifting to rights management—IP licensing, music rights, voice rights—rather than technical production. This represents a fundamental inversion of production economics: historically, technical production (labor, equipment, post-production) dominated costs while rights were a smaller line item. In the AI era, scene complexity is decoupled from cost—a complex VFX sequence costs the same as a simple dialogue scene in compute terms. The implication is that 'cost' of production is becoming a legal/rights problem, not a technical problem. If production costs decline 60% annually while rights costs remain constant or increase (due to scarcity), rights will dominate the cost structure within 2-3 years. This shifts competitive advantage from production capability to IP ownership and rights management expertise. Studios with large IP libraries gain structural advantage not from production infrastructure but from owning the rights that become the primary cost input.

View file

@ -0,0 +1,17 @@
---
type: claim
domain: entertainment
description: Pudgy Penguins demonstrates commercial IP success with cute characters and financial alignment but minimal world-building or narrative investment
confidence: experimental
source: CoinDesk Research, Luca Netz revenue confirmation, TheSoul Publishing partnership
created: 2026-04-14
title: Minimum viable narrative achieves $50M+ revenue scale through character design and distribution without story depth
agent: clay
scope: causal
sourcer: CoinDesk Research
related_claims: ["[[minimum-viable-narrative-strategy-optimizes-for-commercial-scale-through-volume-production-and-distribution-coverage-over-story-depth]]", "[[royalty-based-financial-alignment-may-be-sufficient-for-commercial-ip-success-without-narrative-depth]]", "[[distributed-narrative-architecture-enables-ip-scale-without-concentrated-story-through-blank-canvas-fan-projection]]"]
---
# Minimum viable narrative achieves $50M+ revenue scale through character design and distribution without story depth
Pudgy Penguins achieved ~$50M revenue in 2025 with minimal narrative investment, challenging assumptions about story depth requirements for commercial IP success. Characters exist (Atlas, Eureka, Snofia, Springer) but world-building is minimal. The Lil Pudgys animated series partnership with TheSoul Publishing (parent company of 5-Minute Crafts) follows a volume-production model rather than quality-first narrative investment. This is a 'minimum viable narrative' test: cute character design + financial alignment (NFT royalties) + retail distribution penetration (10,000+ locations) = commercial scale without meaningful story. The company targets $120M revenue in 2026 and IPO by 2027 while maintaining this production philosophy. This is NOT evidence that minimal narrative produces civilizational coordination or deep fandom—it's evidence that commercial licensing buyers and retail consumers will purchase IP based on character appeal and distribution coverage alone. The boundary condition: this works for commercial scale but may not work for cultural depth or long-term community sustainability.

View file

@ -0,0 +1,17 @@
---
type: claim
domain: entertainment
description: Unlike BAYC/Azuki's exclusive-community-first approach, Pudgy Penguins builds global IP through retail and viral content first, then adds NFT layer
confidence: experimental
source: CoinDesk Research, Luca Netz CEO confirmation
created: 2026-04-14
title: Pudgy Penguins inverts Web3 IP strategy by prioritizing mainstream distribution before community building
agent: clay
scope: structural
sourcer: CoinDesk Research
related_claims: ["[[community-owned-IP-grows-through-complex-contagion-not-viral-spread-because-fandom-requires-multiple-reinforcing-exposures-from-trusted-community-members]]", "[[progressive validation through community building reduces development risk by proving audience demand before production investment]]", "[[the media attractor state is community-filtered IP with AI-collapsed production costs where content becomes a loss leader for the scarce complements of fandom community and ownership]]"]
---
# Pudgy Penguins inverts Web3 IP strategy by prioritizing mainstream distribution before community building
Pudgy Penguins explicitly inverts the standard Web3 IP playbook. While Bored Ape Yacht Club and Azuki built exclusive NFT communities first and then attempted mainstream adoption, Pudgy Penguins prioritized physical retail distribution (2M+ Schleich figurines across 3,100 Walmart stores, 10,000+ retail locations) and viral content (79.5B GIPHY views) to acquire users through traditional consumer channels. CEO Luca Netz frames this as 'build a global IP that has an NFT, rather than being an NFT collection trying to become a brand.' This strategy achieved ~$50M revenue in 2025 with a 2026 target of $120M, demonstrating commercial viability of the mainstream-first approach. The inversion is structural: community-first models use exclusivity as the initial value proposition and face friction when broadening; mainstream-first models use accessibility as the initial value proposition and add financial alignment later. This represents a fundamental strategic fork in Web3 IP development, where the sequencing of community vs. mainstream determines the entire go-to-market architecture.

View file

@ -1,49 +1,52 @@
# Pudgy Penguins # Pudgy Penguins
**Type:** Company **Type:** Web3 IP / Consumer Brand
**Domain:** Entertainment **Founded:** 2021 (NFT collection), restructured 2022 under Luca Netz
**Status:** Active **CEO:** Luca Netz
**Founded:** 2021 (NFT collection), 2024 (corporate entity under Luca Netz) **Domain:** Entertainment, Consumer Products
**Status:** Active, targeting IPO 2027
## Overview ## Overview
Pudgy Penguins is a community-owned IP project that originated as an NFT collection and evolved into a multi-platform entertainment brand. Under CEO Luca Netz, the company pivoted from 'selling jpegs' to building a global consumer IP platform through mainstream retail distribution, viral social media content, and hidden blockchain infrastructure. Pudgy Penguins is a Web3 IP company that inverted the standard NFT-to-brand strategy by prioritizing mainstream retail distribution and viral content before community building. The company positions itself as "a global IP that has an NFT, rather than being an NFT collection trying to become a brand."
## Business Model ## Business Model
- **Retail Distribution:** 2M+ Schleich figurines across 10,000+ retail locations including 3,100 Walmart stores **Revenue Streams:**
- **Digital Media:** 79.5B GIPHY views (reportedly outperforms Disney and Pokémon per upload) - Physical retail products (Schleich figurines, trading cards)
- **Web3 Infrastructure:** Pudgy World game (launched March 9, 2026), PENGU token, NFT collections - NFT royalties and secondary sales
- **Content Production:** Lil Pudgys animated series (1,000+ minutes self-financed) - Licensing partnerships
- Digital collectibles (Pengu Card)
## Strategic Approach **Distribution Strategy:**
- Retail-first approach: 10,000+ retail locations globally
- Viral content: 79.5B GIPHY views (reportedly outperforms Disney/Pokémon per upload in reaction gif category)
- Physical products as primary customer acquisition channel
**Minimum Viable Narrative:** Partnership with TheSoul Publishing (parent of 5-Minute Crafts) for high-volume content production rather than narrative-focused studios. Characters described as 'four penguin roommates with basic personalities' in 'UnderBerg' setting. ## Key Metrics (2025-2026)
**Hiding Blockchain:** Deliberately designed consumer-facing products to hide crypto elements. CoinDesk noted Pudgy World 'doesn't feel like crypto at all.' Blockchain treated as invisible infrastructure. - **2025 Revenue:** ~$50M (CEO confirmed)
- **2026 Target:** $120M
- **Retail Distribution:** 2M+ Schleich figurines sold, 3,100 Walmart stores
- **Vibes TCG:** 4M cards sold
- **Pengu Card:** Available in 170+ countries
- **GIPHY Views:** 79.5B total
**Mainstream-First Acquisition:** Acquire users through viral media and retail before Web3 onboarding, inverting typical crypto project trajectory. ## Strategic Positioning
## Financial Trajectory Unlike Bored Ape Yacht Club and Azuki, which built exclusive NFT communities first and then aimed for mainstream adoption, Pudgy Penguins inverted the sequence: mainstream distribution and viral content first, with NFT/blockchain as invisible infrastructure layer.
- **2026 Revenue Target:** $50M-$120M (sources vary) ## Content Production
- **IPO Target:** 2027 (Luca Netz stated he'd be 'disappointed' without IPO within 2 years)
- **Pengu Card:** Operating in 170+ countries
## Key Personnel **Narrative Approach:** Minimum viable narrative—characters exist (Atlas, Eureka, Snofia, Springer) but minimal world-building investment.
- **Luca Netz:** CEO, architect of pivot from NFT project to consumer brand **Animation Partnership:** Lil Pudgys series produced with TheSoul Publishing (parent company of 5-Minute Crafts), following volume-production model rather than quality-first approach.
## Timeline ## Timeline
- **2021** — Pudgy Penguins NFT collection launched - **2021** — Original Pudgy Penguins NFT collection launched
- **2024** — Luca Netz acquires project, pivots strategy toward mainstream consumer brand - **2022** — Luca Netz acquires project and restructures strategy
- **2025-02** — Lil Pudgys animated series announced with TheSoul Publishing partnership - **2024** — Schleich figurine partnership launches, achieving mass retail distribution
- **2026-03-09** — Pudgy World game launched with hidden blockchain infrastructure - **2025** — Achieved ~$50M revenue; Vibes TCG launches with 4M cards sold
- **2026** — 2M+ Schleich figurines sold across 10,000+ retail locations; 79.5B GIPHY views achieved - **2026-02** — CoinDesk Research deep-dive published; company targeting $120M revenue
- **2027** — Target IPO date (CEO stated: "I'd be disappointed in myself if we don't IPO in the next two years")
## Sources
- Animation Magazine (2025-02): Lil Pudgys series announcement
- CoinDesk: Strategic framing and Pudgy World review
- kidscreen: Retail distribution and financial targets

View file

@ -7,9 +7,12 @@ date: 2026-02-01
domain: entertainment domain: entertainment
secondary_domains: [internet-finance] secondary_domains: [internet-finance]
format: article format: article
status: unprocessed status: processed
processed_by: clay
processed_date: 2026-04-14
priority: high priority: high
tags: [pudgy-penguins, community-owned-ip, tokenized-culture, web3-ip, commercial-scale, minimum-viable-narrative] tags: [pudgy-penguins, community-owned-ip, tokenized-culture, web3-ip, commercial-scale, minimum-viable-narrative]
extraction_model: "anthropic/claude-sonnet-4.5"
--- ---
## Content ## Content

View file

@ -7,9 +7,12 @@ date: 2026-03-01
domain: entertainment domain: entertainment
secondary_domains: [] secondary_domains: []
format: article format: article
status: unprocessed status: processed
processed_by: clay
processed_date: 2026-04-14
priority: high priority: high
tags: [AI-production, cost-collapse, independent-film, GenAI, progressive-control, production-economics] tags: [AI-production, cost-collapse, independent-film, GenAI, progressive-control, production-economics]
extraction_model: "anthropic/claude-sonnet-4.5"
--- ---
## Content ## Content

View file

@ -7,9 +7,10 @@ date: 2026-04-14
domain: space-development domain: space-development
secondary_domains: [] secondary_domains: []
format: article format: article
status: unprocessed status: null-result
priority: high priority: high
tags: [Blue-Origin, New-Glenn, NG-3, booster-reuse, AST-SpaceMobile, BlueBird, execution-gap, Pattern-2] tags: [Blue-Origin, New-Glenn, NG-3, booster-reuse, AST-SpaceMobile, BlueBird, execution-gap, Pattern-2]
extraction_model: "anthropic/claude-sonnet-4.5"
--- ---
## Content ## Content

View file

@ -7,9 +7,10 @@ date: 2026-04-01
domain: space-development domain: space-development
secondary_domains: [energy] secondary_domains: [energy]
format: article format: article
status: unprocessed status: null-result
priority: medium priority: medium
tags: [orbital-data-centers, SpaceX, feasibility, physics-critique, thermal-management, power-density, refrigeration] tags: [orbital-data-centers, SpaceX, feasibility, physics-critique, thermal-management, power-density, refrigeration]
extraction_model: "anthropic/claude-sonnet-4.5"
--- ---
## Content ## Content

View file

@ -7,9 +7,10 @@ date: 2026-03-15
domain: entertainment domain: entertainment
secondary_domains: [] secondary_domains: []
format: article format: article
status: unprocessed status: null-result
priority: medium priority: medium
tags: [entertainment-industry, business-reset, smaller-budgets, quality-over-volume, AI-efficiency, slope-reading] tags: [entertainment-industry, business-reset, smaller-budgets, quality-over-volume, AI-efficiency, slope-reading]
extraction_model: "anthropic/claude-sonnet-4.5"
--- ---
## Content ## Content

View file

@ -7,9 +7,10 @@ date: 2026-04-01
domain: entertainment domain: entertainment
secondary_domains: [] secondary_domains: []
format: article format: article
status: unprocessed status: null-result
priority: low priority: low
tags: [tariffs, creator-economy, production-costs, equipment, AI-substitution, macroeconomics] tags: [tariffs, creator-economy, production-costs, equipment, AI-substitution, macroeconomics]
extraction_model: "anthropic/claude-sonnet-4.5"
--- ---
## Content ## Content

View file

@ -7,9 +7,10 @@ date: 2026-04-01
domain: entertainment domain: entertainment
secondary_domains: [] secondary_domains: []
format: article format: article
status: unprocessed status: null-result
priority: medium priority: medium
tags: [hollywood, layoffs, AI-displacement, jobs, disruption, slope-reading] tags: [hollywood, layoffs, AI-displacement, jobs, disruption, slope-reading]
extraction_model: "anthropic/claude-sonnet-4.5"
--- ---
## Content ## Content

View file

@ -67,6 +67,8 @@ def check_agent_health(conn: sqlite3.Connection) -> list[dict]:
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
for r in rows: for r in rows:
agent = r["agent"] agent = r["agent"]
if agent in ("unknown", None):
continue
latest = r["latest"] latest = r["latest"]
if not latest: if not latest:
continue continue
@ -266,24 +268,22 @@ def check_rejection_spike(conn: sqlite3.Connection) -> list[dict]:
"""Detect single rejection reason exceeding REJECTION_SPIKE_RATIO of recent rejections.""" """Detect single rejection reason exceeding REJECTION_SPIKE_RATIO of recent rejections."""
alerts = [] alerts = []
# Total rejections in 24h # Total rejected PRs in 24h (prs.eval_issues is the canonical source — Epimetheus 2026-04-02)
total = conn.execute( total = conn.execute(
"""SELECT COUNT(*) as n FROM audit_log """SELECT COUNT(*) as n FROM prs
WHERE stage='evaluate' WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND event IN ('changes_requested','domain_rejected','tier05_rejected') AND created_at > datetime('now', '-24 hours')"""
AND timestamp > datetime('now', '-24 hours')"""
).fetchone()["n"] ).fetchone()["n"]
if total < 10: if total < 10:
return alerts # Not enough data return alerts # Not enough data
# Count by rejection tag # Count by rejection tag from prs.eval_issues
tags = conn.execute( tags = conn.execute(
"""SELECT value as tag, COUNT(*) as cnt """SELECT value as tag, COUNT(*) as cnt
FROM audit_log, json_each(json_extract(detail, '$.issues')) FROM prs, json_each(prs.eval_issues)
WHERE stage='evaluate' WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND event IN ('changes_requested','domain_rejected','tier05_rejected') AND created_at > datetime('now', '-24 hours')
AND timestamp > datetime('now', '-24 hours')
GROUP BY tag ORDER BY cnt DESC""" GROUP BY tag ORDER BY cnt DESC"""
).fetchall() ).fetchall()
@ -315,16 +315,13 @@ def check_stuck_loops(conn: sqlite3.Connection) -> list[dict]:
"""Detect agents repeatedly failing on the same rejection reason.""" """Detect agents repeatedly failing on the same rejection reason."""
alerts = [] alerts = []
# COALESCE: rejection events use $.agent, eval events use $.domain_agent (Epimetheus 2026-03-28) # Agent + rejection reason from prs table directly (Epimetheus correction 2026-04-02)
rows = conn.execute( rows = conn.execute(
"""SELECT COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) as agent, """SELECT agent, value as tag, COUNT(*) as cnt
value as tag, FROM prs, json_each(prs.eval_issues)
COUNT(*) as cnt WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
FROM audit_log, json_each(json_extract(detail, '$.issues')) AND agent IS NOT NULL
WHERE stage='evaluate' AND created_at > datetime('now', '-6 hours')
AND event IN ('changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', '-6 hours')
AND COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) IS NOT NULL
GROUP BY agent, tag GROUP BY agent, tag
HAVING cnt > ?""", HAVING cnt > ?""",
(STUCK_LOOP_THRESHOLD,), (STUCK_LOOP_THRESHOLD,),
@ -412,16 +409,13 @@ def check_domain_rejection_patterns(conn: sqlite3.Connection) -> list[dict]:
"""Track rejection reason shift per domain — surfaces domain maturity issues.""" """Track rejection reason shift per domain — surfaces domain maturity issues."""
alerts = [] alerts = []
# Per-domain rejection breakdown in 24h # Per-domain rejection breakdown in 24h from prs table (Epimetheus correction 2026-04-02)
rows = conn.execute( rows = conn.execute(
"""SELECT json_extract(detail, '$.domain') as domain, """SELECT domain, value as tag, COUNT(*) as cnt
value as tag, FROM prs, json_each(prs.eval_issues)
COUNT(*) as cnt WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
FROM audit_log, json_each(json_extract(detail, '$.issues')) AND domain IS NOT NULL
WHERE stage='evaluate' AND created_at > datetime('now', '-24 hours')
AND event IN ('changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', '-24 hours')
AND json_extract(detail, '$.domain') IS NOT NULL
GROUP BY domain, tag GROUP BY domain, tag
ORDER BY domain, cnt DESC""" ORDER BY domain, cnt DESC"""
).fetchall() ).fetchall()
@ -473,12 +467,11 @@ def generate_failure_report(conn: sqlite3.Connection, agent: str, hours: int = 2
hours = int(hours) # defensive — callers should pass int, but enforce it hours = int(hours) # defensive — callers should pass int, but enforce it
rows = conn.execute( rows = conn.execute(
"""SELECT value as tag, COUNT(*) as cnt, """SELECT value as tag, COUNT(*) as cnt,
GROUP_CONCAT(DISTINCT json_extract(detail, '$.pr')) as pr_numbers GROUP_CONCAT(DISTINCT number) as pr_numbers
FROM audit_log, json_each(json_extract(detail, '$.issues')) FROM prs, json_each(prs.eval_issues)
WHERE stage='evaluate' WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND event IN ('changes_requested','domain_rejected','tier05_rejected') AND agent = ?
AND json_extract(detail, '$.agent') = ? AND created_at > datetime('now', ? || ' hours')
AND timestamp > datetime('now', ? || ' hours')
GROUP BY tag ORDER BY cnt DESC GROUP BY tag ORDER BY cnt DESC
LIMIT 5""", LIMIT 5""",
(agent, f"-{hours}"), (agent, f"-{hours}"),

View file

@ -194,12 +194,6 @@ fetch('/api/review-summary?days=30')
reasonRows += '<tr><td><code>' + esc(r.reason) + '</code></td><td>' + r.count + '</td></tr>'; reasonRows += '<tr><td><code>' + esc(r.reason) + '</code></td><td>' + r.count + '</td></tr>';
}} }}
// Disagreement types
let disagreeRows = '';
for (const d of (data.disagreement_types || [])) {{
disagreeRows += '<tr><td>' + esc(d.type) + '</td><td>' + d.count + '</td></tr>';
}}
el.innerHTML = ` el.innerHTML = `
<div class="grid"> <div class="grid">
<div class="card"><div class="label">Total Reviews</div><div class="hero-value">${{data.total}}</div></div> <div class="card"><div class="label">Total Reviews</div><div class="hero-value">${{data.total}}</div></div>
@ -215,13 +209,6 @@ fetch('/api/review-summary?days=30')
${{reasonRows || '<tr><td colspan="2" style="color:#8b949e">No rejections</td></tr>'}} ${{reasonRows || '<tr><td colspan="2" style="color:#8b949e">No rejections</td></tr>'}}
</table> </table>
</div> </div>
<div class="card">
<div style="font-weight:600;margin-bottom:8px">Disagreement Types</div>
<table>
<tr><th>Type</th><th>Count</th></tr>
${{disagreeRows || '<tr><td colspan="2" style="color:#8b949e">No disagreements</td></tr>'}}
</table>
</div>
</div>`; </div>`;
}}).catch(() => {{ }}).catch(() => {{
document.getElementById('review-container').innerHTML = document.getElementById('review-container').innerHTML =

View file

@ -237,9 +237,9 @@ async def handle_extraction_yield_by_domain(request):
# Sources per domain (approximate from PR source_path domain) # Sources per domain (approximate from PR source_path domain)
source_counts = conn.execute( source_counts = conn.execute(
"""SELECT domain, COUNT(DISTINCT source_url) as sources """SELECT domain, COUNT(DISTINCT path) as sources
FROM sources s FROM sources s
JOIN prs p ON p.source_path LIKE '%' || s.url || '%' JOIN prs p ON p.source_path LIKE '%' || s.path || '%'
WHERE s.created_at > datetime('now', ? || ' days') WHERE s.created_at > datetime('now', ? || ' days')
GROUP BY domain""", GROUP BY domain""",
(f"-{days}",), (f"-{days}",),
@ -444,6 +444,8 @@ async def handle_cascade_coverage(request):
for r in triggered for r in triggered
] ]
insufficient_data = total_triggered < 5
return web.json_response({ return web.json_response({
"days": days, "days": days,
"total_triggered": total_triggered, "total_triggered": total_triggered,
@ -452,6 +454,7 @@ async def handle_cascade_coverage(request):
"total_notifications": summaries["total_notifications"] if summaries else 0, "total_notifications": summaries["total_notifications"] if summaries else 0,
"merges_with_cascade": summaries["total_merges_with_cascade"] if summaries else 0, "merges_with_cascade": summaries["total_merges_with_cascade"] if summaries else 0,
"by_agent": by_agent, "by_agent": by_agent,
"insufficient_data": insufficient_data,
}) })
finally: finally:
conn.close() conn.close()
@ -490,7 +493,7 @@ async def handle_review_summary(request):
(f"-{days}",), (f"-{days}",),
).fetchall() ).fetchall()
# Rejection reasons # Rejection reasons — try review_records first, fall back to prs.eval_issues
reasons = conn.execute( reasons = conn.execute(
"""SELECT rejection_reason, COUNT(*) as cnt """SELECT rejection_reason, COUNT(*) as cnt
FROM review_records FROM review_records
@ -500,15 +503,17 @@ async def handle_review_summary(request):
(f"-{days}",), (f"-{days}",),
).fetchall() ).fetchall()
# Disagreement types rejection_source = "review_records"
disagreements = conn.execute( if not reasons:
"""SELECT disagreement_type, COUNT(*) as cnt reasons = conn.execute(
FROM review_records """SELECT value AS rejection_reason, COUNT(*) as cnt
WHERE disagreement_type IS NOT NULL FROM prs, json_each(prs.eval_issues)
AND reviewed_at > datetime('now', ? || ' days') WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
GROUP BY disagreement_type ORDER BY cnt DESC""", AND created_at > datetime('now', ? || ' days')
GROUP BY value ORDER BY cnt DESC""",
(f"-{days}",), (f"-{days}",),
).fetchall() ).fetchall()
rejection_source = "prs.eval_issues"
# Per-reviewer breakdown # Per-reviewer breakdown
reviewers = conn.execute( reviewers = conn.execute(
@ -541,7 +546,7 @@ async def handle_review_summary(request):
"total": total, "total": total,
"outcomes": {r["outcome"]: r["cnt"] for r in outcomes}, "outcomes": {r["outcome"]: r["cnt"] for r in outcomes},
"rejection_reasons": [{"reason": r["rejection_reason"], "count": r["cnt"]} for r in reasons], "rejection_reasons": [{"reason": r["rejection_reason"], "count": r["cnt"]} for r in reasons],
"disagreement_types": [{"type": r["disagreement_type"], "count": r["cnt"]} for r in disagreements], "rejection_source": rejection_source,
"reviewers": [ "reviewers": [
{"reviewer": r["reviewer"], "approved": r["approved"], "approved_with_changes": r["approved_with_changes"], {"reviewer": r["reviewer"], "approved": r["approved"], "approved_with_changes": r["approved_with_changes"],
"rejected": r["rejected"], "total": r["total"]} "rejected": r["rejected"], "total": r["total"]}
@ -557,6 +562,124 @@ async def handle_review_summary(request):
conn.close() conn.close()
# ─── GET /api/agent-scorecard ──────────────────────────────────────────────
async def handle_agent_scorecard(request):
"""Per-agent scorecard: PRs submitted, review outcomes, rejection reasons.
Data from review_records (structured reviews) + prs (submission counts).
Falls back to prs.eval_issues for rejection reasons when review_records
has no rejections yet.
"""
conn = request.app["_get_conn"]()
try:
try:
days = min(int(request.query.get("days", "30")), 90)
except ValueError:
days = 30
day_filter = f"-{days}"
# PRs submitted per agent
prs_by_agent = conn.execute(
"""SELECT agent, COUNT(*) as cnt FROM prs
WHERE agent IS NOT NULL
AND created_at > datetime('now', ? || ' days')
GROUP BY agent""",
(day_filter,),
).fetchall()
prs_map = {r["agent"]: r["cnt"] for r in prs_by_agent}
# Review outcomes from review_records
review_data = {}
try:
reviews = conn.execute(
"""SELECT reviewer as agent, outcome, COUNT(*) as cnt
FROM review_records
WHERE reviewed_at > datetime('now', ? || ' days')
GROUP BY reviewer, outcome""",
(day_filter,),
).fetchall()
for r in reviews:
agent = r["agent"]
if agent not in review_data:
review_data[agent] = {"approved": 0, "approved_with_changes": 0, "rejected": 0, "total": 0}
review_data[agent][r["outcome"].replace("-", "_")] = r["cnt"]
review_data[agent]["total"] += r["cnt"]
except sqlite3.OperationalError:
pass
# If review_records is empty, fall back to audit_log eval events
if not review_data:
evals = conn.execute(
"""SELECT
COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) as agent,
event, COUNT(*) as cnt
FROM audit_log
WHERE stage='evaluate'
AND event IN ('approved','changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', ? || ' days')
GROUP BY agent, event""",
(day_filter,),
).fetchall()
for r in evals:
agent = r["agent"]
if not agent:
continue
if agent not in review_data:
review_data[agent] = {"approved": 0, "approved_with_changes": 0, "rejected": 0, "total": 0}
if r["event"] == "approved":
review_data[agent]["approved"] += r["cnt"]
elif r["event"] == "changes_requested": # fixer auto-remediated; equivalent in pre-review_records era
review_data[agent]["approved_with_changes"] += r["cnt"]
else:
review_data[agent]["rejected"] += r["cnt"]
review_data[agent]["total"] += r["cnt"]
# Rejection reasons from prs.eval_issues (canonical source)
reason_rows = conn.execute(
"""SELECT agent, value as reason, COUNT(*) as cnt
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND agent IS NOT NULL
AND created_at > datetime('now', ? || ' days')
GROUP BY agent, reason ORDER BY agent, cnt DESC""",
(day_filter,),
).fetchall()
reasons_map = {}
for r in reason_rows:
if r["agent"] not in reasons_map:
reasons_map[r["agent"]] = {}
reasons_map[r["agent"]][r["reason"]] = r["cnt"]
# Build scorecards
all_agents = sorted(set(list(prs_map.keys()) + list(review_data.keys())))
scorecards = []
for agent in all_agents:
if agent in ("unknown", None):
continue
rd = review_data.get(agent, {"approved": 0, "approved_with_changes": 0, "rejected": 0, "total": 0})
total_reviews = rd["total"]
approved = rd["approved"]
approved_wc = rd["approved_with_changes"]
rejected = rd["rejected"]
approval_rate = ((approved + approved_wc) / total_reviews * 100) if total_reviews else 0
scorecards.append({
"agent": agent,
"total_prs": prs_map.get(agent, 0),
"total_reviews": total_reviews,
"approved": approved,
"approved_with_changes": approved_wc,
"rejected": rejected,
"approval_rate": round(approval_rate, 1),
"rejection_reasons": reasons_map.get(agent, {}),
})
scorecards.sort(key=lambda x: x["total_reviews"], reverse=True)
return web.json_response({"days": days, "scorecards": scorecards})
finally:
conn.close()
# ─── Trace endpoint ──────────────────────────────────────────────────────── # ─── Trace endpoint ────────────────────────────────────────────────────────
@ -998,6 +1121,7 @@ def register_dashboard_routes(app: web.Application, get_conn):
app.router.add_get("/api/agents-dashboard", handle_agents_dashboard) app.router.add_get("/api/agents-dashboard", handle_agents_dashboard)
app.router.add_get("/api/cascade-coverage", handle_cascade_coverage) app.router.add_get("/api/cascade-coverage", handle_cascade_coverage)
app.router.add_get("/api/review-summary", handle_review_summary) app.router.add_get("/api/review-summary", handle_review_summary)
app.router.add_get("/api/agent-scorecard", handle_agent_scorecard)
app.router.add_get("/api/trace/{trace_id}", handle_trace) app.router.add_get("/api/trace/{trace_id}", handle_trace)
app.router.add_get("/api/growth", handle_growth) app.router.add_get("/api/growth", handle_growth)
app.router.add_get("/api/pr-lifecycle", handle_pr_lifecycle) app.router.add_get("/api/pr-lifecycle", handle_pr_lifecycle)