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
106 changes: 106 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -11301,6 +11301,36 @@
]
}
},
"/v1/app/commands/usefulness": {
"get": {
"responses": {
"200": {
"description": "Live app API response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
},
"/v1/app/digest": {
"get": {
"responses": {
Expand Down Expand Up @@ -11331,6 +11361,36 @@
]
}
},
"/v1/app/analytics/daily-rollups": {
"get": {
"responses": {
"200": {
"description": "Live app API response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
},
"/v1/app/analytics/mcp-compatibility": {
"get": {
"responses": {
Expand Down Expand Up @@ -11437,6 +11497,52 @@
]
}
},
"/v1/app/commands/feedback": {
"post": {
"responses": {
"200": {
"description": "Live app mutation or preview response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
}
}
},
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
}
}
},
"400": {
"description": "Invalid request"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
},
"/v1/app/digest/subscriptions": {
"post": {
"responses": {
Expand Down
44 changes: 44 additions & 0 deletions migrations/0015_github_agent_command_feedback.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
CREATE TABLE IF NOT EXISTS github_agent_command_answers (
id TEXT PRIMARY KEY,
repo_full_name TEXT NOT NULL,
issue_number INTEGER NOT NULL,
command TEXT NOT NULL,
request_comment_id INTEGER,
response_comment_id INTEGER,
response_url TEXT,
actor_kind TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata_json TEXT NOT NULL DEFAULT '{}'
);

CREATE INDEX IF NOT EXISTS github_agent_command_answers_repo_issue_idx
ON github_agent_command_answers(repo_full_name, issue_number);

CREATE INDEX IF NOT EXISTS github_agent_command_answers_command_updated_idx
ON github_agent_command_answers(command, updated_at);

CREATE TABLE IF NOT EXISTS github_agent_command_feedback (
id TEXT PRIMARY KEY,
answer_id TEXT NOT NULL,
repo_full_name TEXT NOT NULL,
issue_number INTEGER NOT NULL,
command TEXT NOT NULL,
actor_hash TEXT NOT NULL,
vote TEXT NOT NULL,
source TEXT NOT NULL,
actor_kind TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata_json TEXT NOT NULL DEFAULT '{}',
FOREIGN KEY(answer_id) REFERENCES github_agent_command_answers(id)
);

CREATE UNIQUE INDEX IF NOT EXISTS github_agent_command_feedback_actor_answer_unique
ON github_agent_command_feedback(answer_id, actor_hash);

CREATE INDEX IF NOT EXISTS github_agent_command_feedback_command_updated_idx
ON github_agent_command_feedback(command, updated_at);

CREATE INDEX IF NOT EXISTS github_agent_command_feedback_repo_issue_idx
ON github_agent_command_feedback(repo_full_name, issue_number);
71 changes: 71 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import {
countActiveAuthSessions,
countActiveDigestSubscriptions,
getBounty,
getAgentCommandAnswer,
getCommandUsefulnessSummary,
getFreshOfficialMinerDetection,
getIssue,
getInstallationHealth,
getLatestRepoGithubTotalsSnapshot,
Expand Down Expand Up @@ -71,6 +74,7 @@ import {
persistBountyLifecycleEvent,
persistScorePreview,
persistSignalSnapshot,
recordAgentCommandFeedback,
recordProductUsageEvent,
rollupProductUsageDaily,
summarizeMcpCompatibilityAdoption,
Expand Down Expand Up @@ -398,6 +402,13 @@ const commandPreviewSchema = z
})
.strict();

const commandFeedbackSchema = z
.object({
answerId: z.string().min(8).max(120).regex(/^[A-Za-z0-9_.:-]+$/),
vote: z.enum(["useful", "not_useful"]),
})
.strict();

const digestSubscriptionSchema = z
.object({
email: z.string().email().max(320),
Expand Down Expand Up @@ -774,6 +785,7 @@ export function createApp() {
usageRollups,
usageRollupStatus,
mcpCompatibilityAdoption,
commandUsefulness,
] = await Promise.all([
listRepositories(c.env),
listInstallations(c.env),
Expand All @@ -788,6 +800,7 @@ export function createApp() {
listProductUsageDailyRollups(c.env, { limit: 14 }),
getProductUsageRollupStatus(c.env),
summarizeMcpCompatibilityAdoption(c.env, usageSince),
getCommandUsefulnessSummary(c.env),
]);
const weeklyValueReport = buildWeeklyValueReport({
generatedAt: nowIso(),
Expand Down Expand Up @@ -818,6 +831,7 @@ export function createApp() {
{ label: "Active users", value: String(usageSummary.activeActors), delta: "hashed, last 7 days" },
{ label: "Activation rollups", value: usageRollupStatus.status, delta: usageRollupStatus.latestRollupDay ?? "not generated" },
{ label: "MCP stale clients", value: String(mcpCompatibilityAdoption.staleEvents + mcpCompatibilityAdoption.incompatibleEvents), delta: `${mcpCompatibilityAdoption.totalEvents} MCP event(s)` },
{ label: "Command usefulness", value: `${commandUsefulness.totals.usefulCount}/${commandUsefulness.totals.feedbackCount}`, delta: usefulnessDelta(commandUsefulness.totals.usefulnessRate) },
{ label: "Install issues", value: String(health.filter((record) => record.status !== "healthy").length), delta: "current health cache" },
{ label: "Rate-limit events", value: String(rateLimits.length), delta: "latest observations" },
],
Expand All @@ -832,6 +846,7 @@ export function createApp() {
usageRollups,
usageRollupStatus,
mcpCompatibilityAdoption,
commandUsefulness,
registry,
scoringModel: scoring,
upstreamDrift,
Expand Down Expand Up @@ -938,6 +953,53 @@ export function createApp() {
});
Comment thread
oktofeesh1 marked this conversation as resolved.
});

app.get("/v1/app/commands/usefulness", async (c) => {
const identity = await authenticateRequestIdentity(c);
if (!identity) return c.json({ error: "unauthorized" }, 401);
const days = Number(c.req.query("days") ?? 30);
return c.json(await getCommandUsefulnessSummary(c.env, { windowDays: clampInteger(days, 1, 180) }));
});

app.post("/v1/app/commands/feedback", async (c) => {
const identity = await authenticateRequestIdentity(c);
if (!identity) return c.json({ error: "unauthorized" }, 401);
const body = await c.req.json().catch(() => null);
const parsed = commandFeedbackSchema.safeParse(body);
if (!parsed.success) return c.json({ error: "invalid_command_feedback", issues: parsed.error.issues }, 400);
const answer = await getAgentCommandAnswer(c.env, parsed.data.answerId);
if (!answer) return c.json({ error: "command_answer_not_found" }, 404);
const actorLogin = identity.actor;
await recordAgentCommandFeedback(c.env, {
answerId: answer.id,
repoFullName: answer.repoFullName,
issueNumber: answer.issueNumber,
command: answer.command,
actorLogin,
vote: parsed.data.vote,
source: "app",
actorKind: "maintainer",
metadata: { surface: "app", identityKind: identity.kind },
});
await recordAuditEvent(c.env, {
eventType: "github_app.agent_command_feedback_recorded",
actor: actorLogin,
targetKey: `${answer.repoFullName}#${answer.issueNumber}`,
outcome: "completed",
metadata: { answerId: answer.id, command: answer.command, vote: parsed.data.vote, source: "app", identityKind: identity.kind },
});
return c.json({
ok: true,
generatedAt: nowIso(),
answer: {
id: answer.id,
repoFullName: answer.repoFullName,
issueNumber: answer.issueNumber,
command: answer.command,
},
vote: parsed.data.vote,
});
});

app.get("/v1/app/digest", async (c) => {
const forbidden = await requireAppRole(c, ["maintainer", "owner", "operator"]);
if (forbidden) return forbidden;
Expand Down Expand Up @@ -2152,6 +2214,15 @@ function buildCommandPreview(command: (typeof APP_COMMANDS)[number], request: z.
};
}

function usefulnessDelta(rate: number | null): string {
return rate === null ? "no feedback yet" : `${Math.round(rate * 100)}% useful over 30 days`;
}

function clampInteger(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min;
return Math.min(max, Math.max(min, Math.round(value)));
}

function buildDigestItems(args: {
repositories: RepositoryRecord[];
health: InstallationHealthRecord[];
Expand Down
Loading