Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ As usage grows, the platform needs stronger derived data pipelines, performance
- [x] [P0] Cursor transcript and tool-call extraction mapped onto the normalized session model
- [ ] [P1] Cursor native telemetry enrichment for approvals, change shape, and context-usage signals
- [ ] [P1] Cursor reliable token and model-usage extraction once source telemetry is trustworthy
- [ ] [P1] Workflow fingerprinting: infer common sequences like search -> read -> edit -> test -> fix
- [x] [P1] Workflow fingerprinting: infer common sequences like search -> read -> edit -> test -> fix
- [ ] [P1] Cursor-specific workflow fingerprinting and session archetype mapping
- [ ] [P1] Session archetype detection: debugging, feature delivery, refactor, migration, docs, investigation
- [x] [P1] Session archetype detection: debugging, feature delivery, refactor, migration, docs, investigation
- [ ] [P1] Delegation graph capture for multi-agent and subagent workflows
- [ ] [P2] Exemplar session library for high-value workflows and onboarding examples
- [ ] [P2] Prompt, skill, and template reuse analytics by workflow and outcome
Expand Down Expand Up @@ -268,7 +268,7 @@ As usage grows, the platform needs stronger derived data pipelines, performance
- [x] [P0] Cursor `agent_type` support across capture, sync, ingest, and analytics filters
- [ ] [P0] Durable background job system for sync, facet extraction, narratives, and alerts
- [ ] [P0] Scalable API key lookup and verification strategy
- [ ] [P1] Source-capability registry so Primer can safely gate analytics by what each agent source actually provides
- [x] [P1] Source-capability registry so Primer can safely gate analytics by what each agent source actually provides
- [ ] [P1] OpenTelemetry integration for metrics, traces, and logs
- [ ] [P1] Redis-backed caching for analytics query results and high-read metadata
- [ ] [P1] Analytics performance work for large orgs and concurrent dashboard usage
Expand Down
62 changes: 62 additions & 0 deletions alembic/versions/44c9b01ccad2_add_session_workflow_profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""add session workflow profiles

Revision ID: 44c9b01ccad2
Revises: 0f3b0a1e2c91
Create Date: 2026-03-13 13:02:58.876348

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "44c9b01ccad2"
down_revision: Union[str, None] = "0f3b0a1e2c91"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"session_workflow_profiles",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("session_id", sa.String(length=36), nullable=False),
sa.Column("fingerprint_id", sa.String(length=255), nullable=True),
sa.Column("label", sa.String(length=255), nullable=True),
sa.Column("steps", sa.JSON(), nullable=True),
sa.Column("archetype", sa.String(length=50), nullable=True),
sa.Column("archetype_source", sa.String(length=20), nullable=True),
sa.Column("archetype_reason", sa.Text(), nullable=True),
sa.Column("top_tools", sa.JSON(), nullable=True),
sa.Column("delegation_count", sa.Integer(), nullable=False),
sa.Column("verification_run_count", sa.Integer(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(
["session_id"],
["sessions.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_session_workflow_profiles_session_id",
"session_workflow_profiles",
["session_id"],
unique=True,
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index("ix_session_workflow_profiles_session_id", table_name="session_workflow_profiles")
op.drop_table("session_workflow_profiles")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ const baseSession: SessionDetailResponse = {
last_verification_status: "passed",
sample_recovery_commands: ["pytest -q"],
},
workflow_profile: {
fingerprint_id: "feature_delivery::read+edit+execute+test+ship",
label: "feature delivery: read -> edit -> execute -> test -> ship",
steps: ["read", "edit", "execute", "test", "ship"],
archetype: "feature_delivery",
archetype_source: "session_type",
archetype_reason: "Mapped from the extracted session type 'code_modification'.",
top_tools: ["Read", "Edit"],
delegation_count: 0,
verification_run_count: 1,
},
}

describe("SessionDetailPanel", () => {
Expand Down Expand Up @@ -165,6 +176,16 @@ describe("SessionDetailPanel", () => {
expect(screen.getByText("Sample Recovery Commands")).toBeInTheDocument()
})

it("renders workflow profile when present", () => {
render(<SessionDetailPanel session={baseSession} />)

expect(screen.getByText("Workflow Profile")).toBeInTheDocument()
expect(screen.getByText("Feature Delivery")).toBeInTheDocument()
expect(screen.getByText("Workflow Steps")).toBeInTheDocument()
expect(screen.getByText("Top Tools")).toBeInTheDocument()
expect(screen.getByText("Mapped from the extracted session type 'code_modification'.")).toBeInTheDocument()
})

it("counts hidden change-shape files from both named overflow and inferred files", () => {
render(
<SessionDetailPanel
Expand Down Expand Up @@ -212,4 +233,17 @@ describe("SessionDetailPanel", () => {

expect(screen.queryByText("Facets")).not.toBeInTheDocument()
})

it("does not render workflow profile card when null", () => {
render(
<SessionDetailPanel
session={{
...baseSession,
workflow_profile: null,
}}
/>,
)

expect(screen.queryByText("Workflow Profile")).not.toBeInTheDocument()
})
})
81 changes: 81 additions & 0 deletions frontend/src/components/sessions/session-detail-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,20 @@ const EXECUTION_STATUS_BADGE: Record<
unknown: "secondary",
}

const WORKFLOW_ARCHETYPE_LABELS: Record<string, string> = {
debugging: "Debugging",
feature_delivery: "Feature Delivery",
refactor: "Refactor",
migration: "Migration",
docs: "Docs",
investigation: "Investigation",
}

export function SessionDetailPanel({ session }: SessionDetailPanelProps) {
const { facets } = session
const changeShape = session.change_shape
const recoveryPath = session.recovery_path
const workflowProfile = session.workflow_profile
const visibleNamedFiles = changeShape?.named_touched_files?.slice(0, 8) ?? []
const hiddenChangeShapeFiles = changeShape
? changeShape.files_touched_count - visibleNamedFiles.length
Expand Down Expand Up @@ -235,6 +245,77 @@ export function SessionDetailPanel({ session }: SessionDetailPanelProps) {
</Card>
)}

{workflowProfile && (
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Workflow Profile</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div className="rounded-lg border border-border/70 px-3 py-2">
<p className="text-xs text-muted-foreground">Archetype</p>
<p className="mt-1 text-sm font-semibold">
{workflowProfile.archetype
? WORKFLOW_ARCHETYPE_LABELS[workflowProfile.archetype]
: "-"}
</p>
</div>
<div className="rounded-lg border border-border/70 px-3 py-2">
<p className="text-xs text-muted-foreground">Fingerprint</p>
<p className="mt-1 text-sm font-semibold">
{workflowProfile.label ?? "-"}
</p>
</div>
<div className="rounded-lg border border-border/70 px-3 py-2">
<p className="text-xs text-muted-foreground">Delegations</p>
<p className="mt-1 text-sm font-semibold">
{workflowProfile.delegation_count}
</p>
</div>
<div className="rounded-lg border border-border/70 px-3 py-2">
<p className="text-xs text-muted-foreground">Verification Runs</p>
<p className="mt-1 text-sm font-semibold">
{workflowProfile.verification_run_count}
</p>
</div>
</div>
{workflowProfile.steps && workflowProfile.steps.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Workflow Steps
</p>
<div className="flex flex-wrap gap-2">
{workflowProfile.steps.map((step) => (
<Badge key={step} variant="secondary">
{step.replace(/_/g, " ")}
</Badge>
))}
</div>
</div>
)}
{workflowProfile.top_tools && workflowProfile.top_tools.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Top Tools
</p>
<div className="flex flex-wrap gap-2">
{workflowProfile.top_tools.map((tool) => (
<Badge key={tool} variant="outline">
{tool}
</Badge>
))}
</div>
</div>
)}
{workflowProfile.archetype_reason && (
<p className="text-sm text-muted-foreground">
{workflowProfile.archetype_reason}
</p>
)}
</CardContent>
</Card>
)}

{changeShape && (
<Card>
<CardHeader>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/__tests__/session-detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const mockDetailSession = {
execution_evidence: [],
change_shape: null,
recovery_path: null,
workflow_profile: null,
}

describe("SessionDetailPage", () => {
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@ export interface ModelUsageResponse {

export type ExecutionEvidenceType = "test" | "lint" | "build" | "verification"
export type ExecutionEvidenceStatus = "passed" | "failed" | "unknown"
export type WorkflowStep =
| "search"
| "read"
| "edit"
| "execute"
| "test"
| "fix"
| "delegate"
| "integrate"
| "ship"
export type SessionArchetype =
| "debugging"
| "feature_delivery"
| "refactor"
| "migration"
| "docs"
| "investigation"
export type ArchetypeSource = "session_type" | "heuristic"

export interface SessionExecutionEvidenceResponse {
ordinal: number
Expand Down Expand Up @@ -129,13 +147,26 @@ export interface SessionRecoveryPathResponse {
sample_recovery_commands: string[] | null
}

export interface SessionWorkflowProfileResponse {
fingerprint_id: string | null
label: string | null
steps: WorkflowStep[] | null
archetype: SessionArchetype | null
archetype_source: ArchetypeSource | null
archetype_reason: string | null
top_tools: string[] | null
delegation_count: number
verification_run_count: number
}

export interface SessionDetailResponse extends SessionResponse {
facets: SessionFacetsResponse | null
tool_usages: ToolUsageResponse[]
model_usages: ModelUsageResponse[]
execution_evidence: SessionExecutionEvidenceResponse[]
change_shape: SessionChangeShapeResponse | null
recovery_path: SessionRecoveryPathResponse | null
workflow_profile: SessionWorkflowProfileResponse | null
}

export interface OverviewStats {
Expand Down
29 changes: 29 additions & 0 deletions src/primer/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ class Session(Base):
recovery_path: Mapped["SessionRecoveryPath | None"] = relationship(
back_populates="session", uselist=False, cascade="all, delete-orphan"
)
workflow_profile: Mapped["SessionWorkflowProfile | None"] = relationship(
back_populates="session", uselist=False, cascade="all, delete-orphan"
)
commits: Mapped[list["SessionCommit"]] = relationship(
back_populates="session", cascade="all, delete-orphan"
)
Expand Down Expand Up @@ -379,6 +382,32 @@ class SessionRecoveryPath(Base):
session: Mapped[Session] = relationship(back_populates="recovery_path")


class SessionWorkflowProfile(Base):
__tablename__ = "session_workflow_profiles"
__table_args__ = (
Index(
"ix_session_workflow_profiles_session_id",
"session_id",
unique=True,
),
)

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
session_id: Mapped[str] = mapped_column(ForeignKey("sessions.id"), nullable=False)
fingerprint_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
label: Mapped[str | None] = mapped_column(String(255), nullable=True)
steps: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)
archetype: Mapped[str | None] = mapped_column(String(50), nullable=True)
archetype_source: Mapped[str | None] = mapped_column(String(20), nullable=True)
archetype_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
top_tools: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)
delegation_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
verification_run_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

session: Mapped[Session] = relationship(back_populates="workflow_profile")


class Alert(Base):
__tablename__ = "alerts"

Expand Down
35 changes: 35 additions & 0 deletions src/primer/common/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ class PaginatedResponse[T](BaseModel):
InterventionStatus = Literal["planned", "in_progress", "completed", "dismissed"]
ExecutionEvidenceType = Literal["test", "lint", "build", "verification"]
ExecutionEvidenceStatus = Literal["passed", "failed", "unknown"]
WorkflowStep = Literal[
"search",
"read",
"edit",
"execute",
"test",
"fix",
"delegate",
"integrate",
"ship",
]
SessionArchetype = Literal[
"debugging",
"feature_delivery",
"refactor",
"migration",
"docs",
"investigation",
]
ArchetypeSource = Literal["session_type", "heuristic"]
RecoveryStrategy = Literal[
"inspect_context",
"edit_fix",
Expand Down Expand Up @@ -233,6 +253,20 @@ class SessionRecoveryPathResponse(BaseModel):
model_config = {"from_attributes": True}


class SessionWorkflowProfileResponse(BaseModel):
fingerprint_id: str | None
label: str | None
steps: list[WorkflowStep] | None
archetype: SessionArchetype | None
archetype_source: ArchetypeSource | None
archetype_reason: str | None
top_tools: list[str] | None
delegation_count: int
verification_run_count: int

model_config = {"from_attributes": True}


# --- Session Messages ---


Expand Down Expand Up @@ -368,6 +402,7 @@ class SessionDetailResponse(SessionResponse):
execution_evidence: list[SessionExecutionEvidenceResponse] = []
change_shape: SessionChangeShapeResponse | None = None
recovery_path: SessionRecoveryPathResponse | None = None
workflow_profile: SessionWorkflowProfileResponse | None = None


# --- Analytics ---
Expand Down
Loading
Loading