From 3f5895f6997a4084d1323f3120f1258a6bb1c5b7 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 01:37:39 -0500 Subject: [PATCH 1/9] feat(github-agent): add @gittensory ask command with cited contribution-context output --- src/github/commands.ts | 49 +++++++++++++++++++++++++++++-- src/queue/processors.ts | 8 +++-- test/unit/github-commands.test.ts | 17 +++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/github/commands.ts b/src/github/commands.ts index 991cf02c..3dd0d0ae 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -8,6 +8,7 @@ import { buildCollisionReport, buildQueueHealth, type CollisionCluster, type Que const PUBLIC_MENTION_COMMAND_CATALOG = [ { id: "help", title: "Gittensory command help", description: "Show public-safe @gittensory command help." }, + { id: "ask", title: "Gittensory contribution context Q&A", description: "Answer contribution-quality questions from connected cached sources with citations." }, { id: "preflight", title: "Gittensory preflight", description: "Summarize public PR hygiene and validation readiness." }, { id: "blockers", title: "Gittensory readiness blockers", description: "Explain public-safe readiness blockers." }, { id: "duplicate-check", title: "Gittensory duplicate & WIP check", description: "Summarize duplicate and in-progress overlap caution." }, @@ -35,6 +36,7 @@ type SnapshotCommandName = Exclude 0 ? question : undefined }; } export function isMaintainerAssociation(association: string | null | undefined): boolean { @@ -192,7 +195,7 @@ export function buildPublicAgentCommandComment(args: { maintainerDigest?: MaintainerQueueDigest | null | undefined; }): string { const repoFullName = args.repo?.fullName ?? args.pullRequest?.repoFullName ?? "this repository"; - const sections = commandSections(args.command.name, args.bundle, args.officialMiner, args.maintainerDigest); + const sections = commandSections(args.command.name, args.bundle, args.officialMiner, args.maintainerDigest, args.command.question); const card = buildPublicAnswerCard({ command: args.command.name, sections, @@ -271,6 +274,8 @@ function commandSummary(command: GittensoryMentionCommandName): string { switch (command) { case "help": return "Available public commands and their safest use on a PR thread."; + case "ask": + return "Contribution-context Q&A from connected cached sources, scoped to contribution quality and repository policy."; case "miner-context": return "Public miner context from official Gittensor data when available."; case "preflight": @@ -307,6 +312,10 @@ function commandEvidence( actorKind: "maintainer" | "author", ): string[] { const evidence = [`Invocation authorized for ${actorKind} command use.`, "Output is sanitized before posting to GitHub."]; + if (command === "ask") { + evidence.push("Answer scope is limited to contribution quality and repository policy."); + evidence.push("Sources are cited with freshness and public-boundary redaction."); + } if (command === "miner-context") { evidence.push(officialMiner ? "Official Gittensor miner context was available." : "Official Gittensor miner context was unavailable."); } @@ -325,6 +334,8 @@ function commandNextActions(command: GittensoryMentionCommandName, bundle: Agent switch (command) { case "help": return ["Comment one listed command on the PR thread when more context is needed."]; + case "ask": + return ["Ask one concrete contribution-quality question per command for clearer cited guidance."]; case "miner-context": return ["Use MCP or the authenticated control panel for private contributor planning."]; case "preflight": @@ -362,6 +373,8 @@ function commandSourceNotes( const source = command === "help" ? "static command catalog" + : command === "ask" + ? "cached GitHub issues/PRs/recent merges/checks, signal snapshots, focus manifest, and upstream ruleset status" : command === "miner-context" ? officialMiner ? "official Gittensor miner API" @@ -412,10 +425,13 @@ function commandSections( bundle: AgentRunBundle | null | undefined, officialMiner: GittensorContributorSnapshot | null | undefined, maintainerDigest: MaintainerQueueDigest | null | undefined, + question?: string | undefined, ): string[] { switch (command) { case "help": return helpSections(); + case "ask": + return askSections(bundle, question); case "miner-context": return minerContextSections(officialMiner); case "preflight": @@ -446,6 +462,7 @@ function helpSections(): string[] { "**Commands**", "", "- `@gittensory help` shows this command list.", + "- `@gittensory ask ` answers contribution-quality Q&A with source citations and freshness.", "- `@gittensory preflight` summarizes public PR hygiene.", "- `@gittensory blockers` explains public readiness blockers.", "- `@gittensory duplicate-check` summarizes duplicate/WIP caution.", @@ -462,6 +479,32 @@ function helpSections(): string[] { ]; } +function askSections(bundle: AgentRunBundle | null | undefined, question?: string): string[] { + if (bundle?.run.status === "needs_snapshot_refresh") { + return refreshSections("next-action"); + } + const cited = pickActions(bundle, () => true) + .slice(0, 4) + .map((action) => action.targetRepoFullName ? `${action.targetRepoFullName}: ${action.publicSafeSummary}` : action.publicSafeSummary) + .filter((line) => line.trim().length > 0) + .map((line) => `- ${publicBlockerDetail(line)}`); + return [ + "**Contribution context Q&A**", + "", + `- Question: ${sanitizePublicComment(question?.trim() || "No specific question was provided; this response summarizes the closest cached contribution context.")}`, + "", + "**Answer**", + "", + ...(cited.length > 0 ? cited : ["- No matching contribution-quality context is available in the current cached sources."]), + "", + "**Source coverage**", + "", + "- Connected sources: cached issues, pull requests, recent merges, check/review status, signal snapshots, focus manifest, and upstream ruleset status.", + "- README/docs context is used only when available from connected repo sources and app access allows it.", + "- Source contents are not sent to optional AI unless explicitly enabled.", + ]; +} + function minerContextSections(miner: GittensorContributorSnapshot | null | undefined): string[] { if (!miner) { return ["**Miner context**", "", "- Official miner context is unavailable for this public response."]; diff --git a/src/queue/processors.ts b/src/queue/processors.ts index d7f76764..68638b58 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -972,7 +972,7 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string repoFullName, issue, pullRequest: cachedPullRequest, - }); + }, command.question); const body = buildPublicAgentCommandComment({ command, repo, @@ -1044,6 +1044,7 @@ async function buildMentionCommandBundle( issue: NonNullable; pullRequest: Awaited>; }, + question?: string | undefined, ) { if (commandName === "help" || commandName === "miner-context") return null; if (commandName === "blockers") return explainBlockersWithAgent(env, { login: context.login, repoFullName: context.repoFullName, surface: "github_comment" }); @@ -1053,7 +1054,10 @@ async function buildMentionCommandBundle( login: context.login, repoFullName: context.repoFullName, surface: "github_comment", - objective: `Respond to @gittensory ${commandName} for ${context.repoFullName}#${context.issue.number}.`, + objective: + commandName === "ask" && question && question.trim().length > 0 + ? `Respond to @gittensory ask for ${context.repoFullName}#${context.issue.number}. Question: ${question.trim().slice(0, 280)}` + : `Respond to @gittensory ${commandName} for ${context.repoFullName}#${context.issue.number}.`, }); } diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index 5c798922..3218b8d7 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -14,6 +14,10 @@ describe("GitHub mention commands", () => { it("parses only explicit @gittensory commands", () => { expect(parseGittensoryMentionCommand(null)).toBeNull(); expect(parseGittensoryMentionCommand("@gittensory")?.name).toBe("help"); + expect(parseGittensoryMentionCommand("@gittensory ask what should I fix first?")).toMatchObject({ + name: "ask", + question: "what should I fix first?", + }); expect(parseGittensoryMentionCommand("@gittensory preflight")?.name).toBe("preflight"); expect(parseGittensoryMentionCommand("please @gittensory duplicate-check now")?.name).toBe("duplicate-check"); expect(parseGittensoryMentionCommand("@gittensory reviewability")?.name).toBe("reviewability"); @@ -220,6 +224,19 @@ describe("GitHub mention commands", () => { expect(nextAction).toContain("### Gittensory next step"); expect(nextAction).toContain("**Recommended next step**"); expect(nextAction).toContain("After tests pass."); + + const ask = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what should I improve for contribution quality?")!, + repo: null, + issue: { number: 14, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle, + }); + expect(ask).toContain("### Gittensory contribution context Q&A"); + expect(ask).toContain("**Contribution context Q&A**"); + expect(ask).toContain("Question: what should I improve for contribution quality?"); + expect(ask).toContain("Connected sources: cached issues, pull requests, recent merges, check/review status"); }); it("does not publish private blocker why details", () => { From de0b030e1d888fe71712be6e3d46758fdfc4fae0 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 01:41:37 -0500 Subject: [PATCH 2/9] fix: update --- src/github/commands.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/github/commands.ts b/src/github/commands.ts index 3dd0d0ae..c1ed8df0 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -62,6 +62,7 @@ const AGENT_COMMAND_FEEDBACK_MARKER = "gittensory-agent-command-answer"; const COMMAND_TITLES = Object.fromEntries(GITTENSORY_MENTION_COMMAND_CATALOG.map((command) => [command.id, command.title])) as Record; const REFRESH_SECTION_TITLES: Record = { + ask: "Contribution context snapshot refresh", preflight: "Preflight snapshot refresh", blockers: "Blocker snapshot refresh", "duplicate-check": "Duplicate-check snapshot refresh", @@ -72,6 +73,7 @@ const REFRESH_SECTION_TITLES: Record = { }; const EMPTY_SECTION_TITLES: Record = { + ask: "Contribution context Q&A", preflight: "Preflight summary", blockers: "Readiness blockers", "duplicate-check": "Duplicate & WIP caution", From 95abf4468a98f728336cc20ccae359f3ce1c751d Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 02:09:03 -0500 Subject: [PATCH 3/9] fix: coverage --- test/unit/github-commands.test.ts | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index 3218b8d7..2a7f07a9 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -389,6 +389,30 @@ describe("GitHub mention commands", () => { }); expect(duplicateRefresh).toContain("**Duplicate-check snapshot refresh**"); + const askRefresh = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask should I update linked issue context?")!, + repo: null, + issue: { number: 35, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: { + id: "run-ask-refresh", + objective: "refresh", + actorLogin: "oktofeesh1", + surface: "github_comment", + mode: "copilot", + status: "needs_snapshot_refresh", + dataQualityStatus: "unknown", + payload: {}, + }, + actions: [], + contextSnapshots: [], + summary: "refresh", + }, + }); + expect(askRefresh).toContain("**Next-action snapshot refresh**"); + const empty = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory next-action")!, repo: null, @@ -414,6 +438,31 @@ describe("GitHub mention commands", () => { expect(empty).toContain("**Recommended next step**"); expect(empty).toContain("No public-safe context is available"); + const askNoQuestion = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask")!, + repo: null, + issue: { number: 36, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: { + id: "run-ask-empty", + objective: "empty", + actorLogin: "oktofeesh1", + surface: "github_comment", + mode: "copilot", + status: "completed", + dataQualityStatus: "complete", + payload: {}, + }, + actions: [], + contextSnapshots: [], + summary: "empty", + }, + }); + expect(askNoQuestion).toContain("No specific question was provided"); + expect(askNoQuestion).toContain("No matching contribution-quality context is available"); + const noBundle = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory preflight")!, repo: null, @@ -1178,6 +1227,18 @@ describe("GitHub mention commands", () => { expect(body).toContain("<b>x</b>"); } }); + + it("falls back to repository placeholder when queue digest has no source records", () => { + const digest = buildMaintainerQueueDigest({ + repo: null, + issues: [], + pullRequests: [], + recentMergedPullRequests: [], + }); + expect(digest.repoFullName).toBe("this repository"); + expect(digest.reviewNowPullRequests).toHaveLength(0); + expect(digest.needsAuthorPullRequests).toHaveLength(0); + }); }); function completedRun(id: string) { From 7e708f603022563218fb7941172184be575bbf06 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 02:19:54 -0500 Subject: [PATCH 4/9] fix: ci --- scripts/github-type-label.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/github-type-label.mjs b/scripts/github-type-label.mjs index a39f5d77..c4852aa4 100644 --- a/scripts/github-type-label.mjs +++ b/scripts/github-type-label.mjs @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { readFile } from "node:fs/promises"; import { pathToFileURL } from "node:url"; From 20584642cf7ec77f60581dbebdfeadfc4ab669f6 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 05:01:10 -0500 Subject: [PATCH 5/9] fix: update --- scripts/check-mcp-release-due.mjs | 1 - src/github/commands.ts | 22 +++++++++++++++++++++- test/unit/github-commands.test.ts | 7 ++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/scripts/check-mcp-release-due.mjs b/scripts/check-mcp-release-due.mjs index 213d44c6..9f848a07 100644 --- a/scripts/check-mcp-release-due.mjs +++ b/scripts/check-mcp-release-due.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node import { execFileSync, spawnSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; diff --git a/src/github/commands.ts b/src/github/commands.ts index c1ed8df0..e17fd769 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -483,8 +483,9 @@ function helpSections(): string[] { function askSections(bundle: AgentRunBundle | null | undefined, question?: string): string[] { if (bundle?.run.status === "needs_snapshot_refresh") { - return refreshSections("next-action"); + return refreshSections("ask"); } + const sourceReferences = askSourceReferences(bundle); const cited = pickActions(bundle, () => true) .slice(0, 4) .map((action) => action.targetRepoFullName ? `${action.targetRepoFullName}: ${action.publicSafeSummary}` : action.publicSafeSummary) @@ -499,6 +500,10 @@ function askSections(bundle: AgentRunBundle | null | undefined, question?: strin "", ...(cited.length > 0 ? cited : ["- No matching contribution-quality context is available in the current cached sources."]), "", + "**Citations**", + "", + ...(sourceReferences.length > 0 ? sourceReferences : ["- No concrete cached source reference is available for this response."]), + "", "**Source coverage**", "", "- Connected sources: cached issues, pull requests, recent merges, check/review status, signal snapshots, focus manifest, and upstream ruleset status.", @@ -507,6 +512,21 @@ function askSections(bundle: AgentRunBundle | null | undefined, question?: strin ]; } +function askSourceReferences(bundle: AgentRunBundle | null | undefined): string[] { + if (!bundle || bundle.actions.length === 0) return []; + return bundle.actions.slice(0, 6).map((action) => { + const target = [ + action.targetRepoFullName ?? null, + action.targetPullNumber ? `PR #${action.targetPullNumber}` : null, + action.targetIssueNumber ? `issue #${action.targetIssueNumber}` : null, + ] + .filter((entry): entry is string => Boolean(entry)) + .join(" "); + const reference = target.length > 0 ? target : "cached contributor context"; + return `- ${reference}; source: action ${action.actionType}; freshness: ${publicStatus(bundle.run.status)}.`; + }); +} + function minerContextSections(miner: GittensorContributorSnapshot | null | undefined): string[] { if (!miner) { return ["**Miner context**", "", "- Official miner context is unavailable for this public response."]; diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index 2a7f07a9..bff4fe36 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -236,6 +236,11 @@ describe("GitHub mention commands", () => { expect(ask).toContain("### Gittensory contribution context Q&A"); expect(ask).toContain("**Contribution context Q&A**"); expect(ask).toContain("Question: what should I improve for contribution quality?"); + expect(ask).toContain("Citations"); + expect(ask).toContain("source: action choose_next_work"); + expect(ask).toContain("freshness: completed."); + expect(ask).toContain("Source: cached GitHub issues/PRs/recent merges/checks, signal snapshots, focus manifest, and upstream ruleset status."); + expect(ask).toContain("Freshness: agent run status completed."); expect(ask).toContain("Connected sources: cached issues, pull requests, recent merges, check/review status"); }); @@ -411,7 +416,7 @@ describe("GitHub mention commands", () => { summary: "refresh", }, }); - expect(askRefresh).toContain("**Next-action snapshot refresh**"); + expect(askRefresh).toContain("**Contribution context snapshot refresh**"); const empty = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory next-action")!, From 3bec97b4958e900cc74ff3878a61ea4550957469 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 05:43:06 -0500 Subject: [PATCH 6/9] fix: update --- test/unit/github-commands.test.ts | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index bff4fe36..e0c76d11 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -242,6 +242,38 @@ describe("GitHub mention commands", () => { expect(ask).toContain("Source: cached GitHub issues/PRs/recent merges/checks, signal snapshots, focus manifest, and upstream ruleset status."); expect(ask).toContain("Freshness: agent run status completed."); expect(ask).toContain("Connected sources: cached issues, pull requests, recent merges, check/review status"); + + const askWithTargets = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what should I clean up before review?")!, + repo: null, + issue: { number: 15, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: completedRun("run-ask-targets"), + actions: [ + { + id: "ask-target-action", + runId: "run-ask-targets", + actionType: "prepare_pr_packet", + targetRepoFullName: "owner/repo", + targetPullNumber: 88, + targetIssueNumber: 34, + status: "recommended", + recommendation: "Prepare packet", + why: [], + blockedBy: [], + publicSafeSummary: "Prepare a concise packet and verify linked context.", + approvalRequired: false, + safetyClass: "public_safe", + payload: {}, + }, + ], + contextSnapshots: [], + summary: "ask targets", + }, + }); + expect(askWithTargets).toContain("owner/repo PR #88 issue #34; source: action prepare_pr_packet; freshness: completed."); }); it("does not publish private blocker why details", () => { From 1b559faa8384df4fa4a80fe6bd14396f8b5045fe Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 12:10:07 -0500 Subject: [PATCH 7/9] fix: update --- src/github/commands.ts | 204 +++++++++++++++-- test/unit/github-commands.test.ts | 350 ++++++++++++++++++++++++++++-- 2 files changed, 525 insertions(+), 29 deletions(-) diff --git a/src/github/commands.ts b/src/github/commands.ts index e17fd769..a7462a40 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -376,7 +376,7 @@ function commandSourceNotes( command === "help" ? "static command catalog" : command === "ask" - ? "cached GitHub issues/PRs/recent merges/checks, signal snapshots, focus manifest, and upstream ruleset status" + ? askCommandSourceSummary(bundle) : command === "miner-context" ? officialMiner ? "official Gittensor miner API" @@ -481,6 +481,15 @@ function helpSections(): string[] { ]; } +type AskContributingSource = { + key: string; + label: string; + origin: string; + generatedAt: string | null; + freshness: string; + detail: string; +}; + function askSections(bundle: AgentRunBundle | null | undefined, question?: string): string[] { if (bundle?.run.status === "needs_snapshot_refresh") { return refreshSections("ask"); @@ -504,27 +513,190 @@ function askSections(bundle: AgentRunBundle | null | undefined, question?: strin "", ...(sourceReferences.length > 0 ? sourceReferences : ["- No concrete cached source reference is available for this response."]), "", - "**Source coverage**", + "**Policy**", "", - "- Connected sources: cached issues, pull requests, recent merges, check/review status, signal snapshots, focus manifest, and upstream ruleset status.", - "- README/docs context is used only when available from connected repo sources and app access allows it.", + "- README/docs context is included only when connected repo sources and app permissions allow it.", "- Source contents are not sent to optional AI unless explicitly enabled.", ]; } function askSourceReferences(bundle: AgentRunBundle | null | undefined): string[] { - if (!bundle || bundle.actions.length === 0) return []; - return bundle.actions.slice(0, 6).map((action) => { - const target = [ - action.targetRepoFullName ?? null, - action.targetPullNumber ? `PR #${action.targetPullNumber}` : null, - action.targetIssueNumber ? `issue #${action.targetIssueNumber}` : null, - ] - .filter((entry): entry is string => Boolean(entry)) - .join(" "); - const reference = target.length > 0 ? target : "cached contributor context"; - return `- ${reference}; source: action ${action.actionType}; freshness: ${publicStatus(bundle.run.status)}.`; - }); + return collectAskContributingSources(bundle).slice(0, 8).map(formatAskCitation); +} + +function collectAskContributingSources(bundle: AgentRunBundle | null | undefined): AskContributingSource[] { + if (!bundle) return []; + const collected: AskContributingSource[] = []; + const seen = new Set(); + const add = (entry: AskContributingSource | null | undefined) => { + if (!entry) return; + const dedupeKey = `${entry.key}|${entry.origin}|${entry.freshness}|${entry.generatedAt ?? ""}|${entry.detail}`; + if (seen.has(dedupeKey)) return; + seen.add(dedupeKey); + collected.push(entry); + }; + for (const snapshot of bundle.contextSnapshots) { + for (const entry of askSourcesFromContextSnapshot(snapshot)) add(entry); + } + for (const action of bundle.actions) { + for (const entry of askSourcesFromActionEvidence(action)) add(entry); + } + return collected; +} + +function askCommandSourceSummary(bundle: AgentRunBundle | null | undefined): string { + const sources = collectAskContributingSources(bundle); + if (sources.length === 0) return "cached Gittensory agent context (no connected-source metadata in this run)"; + return sources + .slice(0, 4) + .map((source) => source.label) + .join("; "); +} + +function askSourcesFromContextSnapshot(snapshot: AgentRunBundle["contextSnapshots"][number]): AskContributingSource[] { + const payload = snapshot.payload ?? {}; + const generatedAt = snapshot.createdAt ?? snapshot.decisionPackVersion ?? null; + const sources: AskContributingSource[] = []; + const graph = payload.evidenceGraph; + if (graph && typeof graph === "object" && !Array.isArray(graph)) { + const graphRecord = graph as Record; + const graphGeneratedAt = typeof graphRecord.generatedAt === "string" ? graphRecord.generatedAt : generatedAt; + const graphSources = graphRecord.sources; + if (Array.isArray(graphSources)) { + for (const item of graphSources) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const record = item as Record; + const kind = typeof record.source === "string" ? record.source : "connected_source"; + sources.push({ + key: `evidence_graph_${kind}`, + label: askSourceLabel(kind), + origin: kind, + generatedAt: typeof record.generatedAt === "string" ? record.generatedAt : graphGeneratedAt, + freshness: typeof record.freshness === "string" ? record.freshness : "unknown", + detail: typeof record.detail === "string" ? record.detail : "Connected contributor evidence graph source.", + }); + } + } + } + const baseFreshness = readRecord(payload.baseFreshness); + if (baseFreshness) { + sources.push({ + key: "base_freshness", + label: askSourceLabel("base_freshness"), + origin: "metadata_only", + generatedAt: typeof baseFreshness.observedAt === "string" ? baseFreshness.observedAt : generatedAt, + freshness: typeof baseFreshness.status === "string" ? baseFreshness.status : "unknown", + detail: "Repo/issue/PR sync freshness used for contribution-context answers.", + }); + } + const branchEligibility = readRecord(payload.branchEligibility); + if (branchEligibility) { + sources.push({ + key: "branch_eligibility", + label: askSourceLabel("branch_eligibility"), + origin: "metadata_only", + generatedAt, + freshness: branchEligibility.stale === true ? "stale" : branchEligibility.evidence === "missing" ? "missing" : "fresh", + detail: "Branch eligibility metadata from connected local/GitHub context.", + }); + } + if (typeof payload.scoreabilityStatus === "string") { + sources.push({ + key: "scoreability_status", + label: "contribution readiness metadata", + origin: "metadata_only", + generatedAt, + freshness: snapshotFreshnessFromWarnings(snapshot), + detail: `Contribution readiness status: ${payload.scoreabilityStatus}.`, + }); + } + const dataQuality = readRecord(payload.dataQuality); + if (dataQuality && typeof dataQuality.status === "string") { + sources.push({ + key: "signal_data_quality", + label: askSourceLabel("data_quality"), + origin: "cached_signals", + generatedAt, + freshness: dataQuality.status === "complete" ? "fresh" : String(dataQuality.status), + detail: "Signal fidelity and data-quality status for connected repo sources.", + }); + } + if (snapshot.freshnessWarnings.length > 0) { + sources.push({ + key: "freshness_warnings", + label: "snapshot freshness warnings", + origin: "cached_signals", + generatedAt, + freshness: "degraded", + detail: snapshot.freshnessWarnings.slice(0, 2).join(" "), + }); + } + return sources; +} + +function askSourcesFromActionEvidence(action: AgentActionRecord): AskContributingSource[] { + const evidence = readRecord(action.payload.recommendationEvidence); + if (!evidence || !Array.isArray(evidence.sources)) return []; + return evidence.sources + .map((raw) => { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null; + const source = raw as Record; + const name = typeof source.name === "string" ? source.name : "connected_source"; + return { + key: name, + label: askSourceLabel(name), + origin: name, + generatedAt: typeof source.generatedAt === "string" ? source.generatedAt : null, + freshness: typeof source.freshness === "string" ? source.freshness : "unknown", + detail: typeof source.summary === "string" ? source.summary : "Connected recommendation evidence source.", + }; + }) + .filter((entry): entry is AskContributingSource => entry !== null); +} + +function formatAskCitation(source: AskContributingSource): string { + const header = [`Source: ${source.label}`, source.origin ? `origin: ${source.origin}` : null, `freshness: ${source.freshness}`] + .filter((part): part is string => Boolean(part)) + .join("; "); + const observed = source.generatedAt ? ` as of ${source.generatedAt}` : ""; + const detail = source.detail ? ` — ${publicBlockerDetail(source.detail)}` : ""; + return `- ${header}${observed}${detail}.`; +} + +function askSourceLabel(name: string): string { + const labels: Record = { + contributor_decision_pack: "contributor decision pack snapshot", + repo_decision: "repo decision snapshot", + official_contributor_stats: "official Gittensor contributor stats", + repo_outcome_history: "repo outcome history", + open_pr_monitor: "cached GitHub open PR/issue queue", + local_branch_metadata: "local branch metadata (metadata-only)", + base_branch_freshness: "local git branch freshness", + base_freshness: "repo sync freshness metadata", + branch_eligibility: "branch eligibility metadata", + github_branch_status: "cached GitHub branch status", + linked_issue_multiplier: "linked-issue policy context", + score_preview: "private score preview metadata", + data_quality: "signal data-quality status", + official_gittensor: "official Gittensor API/cache", + mirror: "Gittensor mirror registry snapshot", + github_cache: "cached GitHub issues, PRs, reviews, and checks", + computed: "derived Gittensory contribution signals", + repo_focus_manifest: "repo focus manifest", + issue_quality: "issue quality snapshot", + upstream_ruleset: "upstream ruleset status", + }; + return labels[name] ?? name.replace(/_/g, " "); +} + +function readRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +function snapshotFreshnessFromWarnings(snapshot: AgentRunBundle["contextSnapshots"][number]): string { + if (snapshot.freshnessWarnings.some((warning) => /stale|rebuild/i.test(warning))) return "stale"; + return "fresh"; } function minerContextSections(miner: GittensorContributorSnapshot | null | undefined): string[] { diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index e0c76d11..395c1cc3 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -231,17 +231,24 @@ describe("GitHub mention commands", () => { issue: { number: 14, title: "PR", state: "open", pull_request: {} }, pullRequest: null, actorKind: "author", - bundle, + bundle: askCitedBundle(), }); expect(ask).toContain("### Gittensory contribution context Q&A"); expect(ask).toContain("**Contribution context Q&A**"); expect(ask).toContain("Question: what should I improve for contribution quality?"); expect(ask).toContain("Citations"); - expect(ask).toContain("source: action choose_next_work"); - expect(ask).toContain("freshness: completed."); - expect(ask).toContain("Source: cached GitHub issues/PRs/recent merges/checks, signal snapshots, focus manifest, and upstream ruleset status."); + expect(ask).toContain("origin: contributor_decision_pack; freshness: fresh"); + expect(ask).toContain("origin: open_pr_monitor; freshness: fresh"); + expect(ask).toContain("origin: github_cache; freshness: fresh"); + expect(ask).toContain("origin: official_gittensor; freshness: fresh"); + expect(ask).toContain("signal data-quality status; origin: cached_signals"); + expect(ask).toContain("Policy"); + expect(ask).toContain("README/docs context is included only when connected repo sources"); + expect(ask).not.toContain("source: action choose_next_work"); + expect(ask).not.toContain("**Source coverage**"); + expect(ask).toContain("contributor decision pack snapshot"); + expect(ask).toContain("cached GitHub open PR/issue queue"); expect(ask).toContain("Freshness: agent run status completed."); - expect(ask).toContain("Connected sources: cached issues, pull requests, recent merges, check/review status"); const askWithTargets = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory ask what should I clean up before review?")!, @@ -249,12 +256,11 @@ describe("GitHub mention commands", () => { issue: { number: 15, title: "PR", state: "open", pull_request: {} }, pullRequest: null, actorKind: "author", - bundle: { - run: completedRun("run-ask-targets"), + bundle: askCitedBundle({ actions: [ { id: "ask-target-action", - runId: "run-ask-targets", + runId: "run-ask-cited", actionType: "prepare_pr_packet", targetRepoFullName: "owner/repo", targetPullNumber: 88, @@ -266,14 +272,28 @@ describe("GitHub mention commands", () => { publicSafeSummary: "Prepare a concise packet and verify linked context.", approvalRequired: false, safetyClass: "public_safe", - payload: {}, + payload: { + recommendationEvidence: { + confidence: "high", + sourceSummary: "Packet evidence", + freshness: "fresh", + sources: [ + { + name: "repo_focus_manifest", + source: "github_cache", + generatedAt: "2026-06-01T12:00:00.000Z", + freshness: "fresh", + summary: "Repo focus manifest for owner/repo.", + }, + ], + }, + }, }, ], - contextSnapshots: [], - summary: "ask targets", - }, + }), }); - expect(askWithTargets).toContain("owner/repo PR #88 issue #34; source: action prepare_pr_packet; freshness: completed."); + expect(askWithTargets).toContain("Source: repo focus manifest; origin: repo_focus_manifest; freshness: fresh"); + expect(askWithTargets).toContain("owner/repo: Prepare a concise packet and verify linked context."); }); it("does not publish private blocker why details", () => { @@ -450,6 +470,16 @@ describe("GitHub mention commands", () => { }); expect(askRefresh).toContain("**Contribution context snapshot refresh**"); + const nextActionRefresh = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory next-action")!, + repo: null, + issue: { number: 36, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: refreshBundle(), + }); + expect(nextActionRefresh).toContain("**Next-action snapshot refresh**"); + const empty = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory next-action")!, repo: null, @@ -500,6 +530,169 @@ describe("GitHub mention commands", () => { expect(askNoQuestion).toContain("No specific question was provided"); expect(askNoQuestion).toContain("No matching contribution-quality context is available"); + const askMetadata = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what blocks contribution readiness?")!, + repo: null, + issue: { number: 37, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: completedRun("run-ask-meta"), + actions: [ + { + id: "ask-meta-action", + runId: "run-ask-meta", + actionType: "explain_score_blockers", + status: "blocked", + recommendation: "Resolve blockers", + why: [], + blockedBy: [], + publicSafeSummary: "Resolve queue pressure before opening more work.", + approvalRequired: true, + safetyClass: "private", + payload: { + recommendationEvidence: { + confidence: "low", + sourceSummary: "Mixed metadata", + freshness: "stale", + sources: [null, []], + }, + }, + }, + ], + contextSnapshots: [ + { + id: "snap-ask-meta", + runId: "run-ask-meta", + repoSignalSnapshotIds: [], + freshnessWarnings: ["decision pack is stale; rebuild enqueued"], + payload: { + baseFreshness: { status: "stale", observedAt: "2026-06-01T10:00:00.000Z" }, + branchEligibility: { stale: true }, + scoreabilityStatus: "blocked", + dataQuality: { status: "partial" }, + evidenceGraph: { + sources: [null, "invalid", { detail: "Graph source without explicit origin." }], + }, + }, + }, + { + id: "snap-ask-meta-missing", + runId: "run-ask-meta", + repoSignalSnapshotIds: [], + freshnessWarnings: ["partial signal coverage only"], + payload: { + branchEligibility: { evidence: "missing" }, + scoreabilityStatus: "ready", + baseFreshness: {}, + }, + }, + { + id: "snap-ask-meta-fresh", + runId: "run-ask-meta", + repoSignalSnapshotIds: [], + freshnessWarnings: [], + payload: { + branchEligibility: { stale: false, evidence: "present" }, + }, + }, + { + id: "snap-ask-meta-graph-shape", + runId: "run-ask-meta", + repoSignalSnapshotIds: [], + freshnessWarnings: [], + payload: { + evidenceGraph: { sources: "not-an-array" }, + }, + }, + ], + summary: "ask metadata", + }, + }); + expect(askMetadata).toContain("repo sync freshness metadata"); + expect(askMetadata).toContain("branch eligibility metadata"); + expect(askMetadata).toContain("contribution readiness metadata"); + expect(askMetadata).toContain("snapshot freshness warnings"); + expect(askMetadata).toContain("freshness: partial"); + expect(askMetadata).not.toContain("No concrete cached source reference is available for this response."); + expect(askMetadata).toContain("Graph source without explicit origin."); + + const askNoSources = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what is the repo policy?")!, + repo: null, + issue: { number: 38, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + }); + expect(askNoSources).toContain("cached Gittensory agent context (no connected-source metadata in this run)"); + + const askEvidenceOnly = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what should I verify locally?")!, + repo: null, + issue: { number: 39, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: completedRun("run-ask-evidence-only"), + actions: [ + { + id: "ask-evidence-only", + runId: "run-ask-evidence-only", + actionType: "preflight_branch", + status: "ready", + recommendation: "Preflight", + why: [], + blockedBy: [], + publicSafeSummary: "Run local branch preflight first.", + approvalRequired: false, + safetyClass: "private", + payload: { + recommendationEvidence: { + confidence: "medium", + sourceSummary: "Branch metadata", + freshness: "fresh", + sources: [{ name: "score_preview" }], + }, + }, + }, + ], + contextSnapshots: [], + summary: "ask evidence only", + }, + }); + expect(askEvidenceOnly).toContain("origin: score_preview; freshness: unknown"); + expect(askEvidenceOnly).toContain("private score preview metadata"); + expect(askEvidenceOnly).toContain("Connected recommendation evidence source."); + + const askFallbackCitations = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what is missing?")!, + repo: null, + issue: { number: 40, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: completedRun("run-ask-fallback-citations"), + actions: [ + { + id: "ask-fallback-citations", + runId: "run-ask-fallback-citations", + actionType: "choose_next_work", + status: "recommended", + recommendation: "Next", + why: [], + blockedBy: [], + publicSafeSummary: "Use cached queue context before opening new work.", + approvalRequired: false, + safetyClass: "private", + payload: {}, + }, + ], + contextSnapshots: [], + summary: "ask fallback citations", + }, + }); + expect(askFallbackCitations).toContain("No concrete cached source reference is available for this response."); + const noBundle = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory preflight")!, repo: null, @@ -1175,6 +1368,7 @@ describe("GitHub mention commands", () => { { ...pr(16, "Long medium overlap implementation title that should be shortened in public queue output because it exceeds the digest line budget", "gina", { linkedIssues: [5], updatedAt: "not-a-date" }) }, { repoFullName: "owner/repo", number: 17, title: "No timestamp review candidate", state: "open", authorLogin: "hal", authorAssociation: "NONE", labels: [], linkedIssues: [6], body: "Fixes #6" }, { repoFullName: "owner/repo", number: 18, title: "Maintainer draft stewardship", state: "open", authorLogin: "ivy", authorAssociation: "OWNER", isDraft: true, labels: [], linkedIssues: [7], body: "Fixes #7" }, + { repoFullName: "owner/repo", number: 19, title: "Author metadata unavailable", state: "open", authorLogin: null, authorAssociation: "NONE", labels: [], linkedIssues: [8], body: "Fixes #8" }, ], recentMergedPullRequests: [ { @@ -1198,6 +1392,8 @@ describe("GitHub mention commands", () => { }); expect(defensiveClusters).toContain("medium risk:"); expect(defensiveClusters).toContain("..."); + expect(defensiveClusters).toContain("#19: Author metadata unavailable"); + expect(defensiveClusters).not.toContain(" by @"); const defensiveSummary = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory queue-summary")!, repo: null, @@ -1208,6 +1404,56 @@ describe("GitHub mention commands", () => { }); expect(defensiveSummary).toContain("Use the authenticated maintainer dashboard and private API"); expect(defensiveDigest.needsAuthorPullRequests.find((pr) => pr.number === 18)?.reasons).toContain("Maintainer-authored PR; review as repo stewardship."); + + const unavailableDigest = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory review-now")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 102, title: "Digest", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + maintainerDigest: null, + }); + expect(unavailableDigest).toContain("Cached queue context is unavailable for this command."); + + const emptyReviewNow = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory review-now")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 103, title: "Digest", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + maintainerDigest: { ...digest, reviewNowPullRequests: [] }, + }); + expect(emptyReviewNow).toContain("No cached PR currently looks ready for detailed review."); + + const emptyDuplicateClusters = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory duplicate-clusters")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 104, title: "Digest", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + maintainerDigest: { ...digest, duplicateClusters: [] }, + }); + expect(emptyDuplicateClusters).toContain("No duplicate or WIP cluster is visible from cached metadata."); + + const emptyConfirmed = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory confirmed-miners")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 105, title: "Digest", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + maintainerDigest: { ...digest, confirmedMinerPullRequests: [] }, + }); + expect(emptyConfirmed).toContain("No cached confirmed-miner PRs are visible in this queue."); + + const emptyNeedsAuthor = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory needs-author")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 106, title: "Digest", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + maintainerDigest: { ...digest, needsAuthorPullRequests: [] }, + }); + expect(emptyNeedsAuthor).toContain("No cached PR currently needs obvious author cleanup first."); }); it("neutralizes attacker-controlled queue digest titles in public comments", () => { @@ -1291,6 +1537,84 @@ function completedRun(id: string) { }; } +function askCitedBundle(overrides: { actions?: Array>; contextSnapshots?: Array> } = {}): import("../../src/services/agent-orchestrator").AgentRunBundle { + return { + run: completedRun("run-ask-cited"), + actions: [ + { + id: "ask-cited-action", + runId: "run-ask-cited", + actionType: "choose_next_work" as const, + status: "recommended" as const, + recommendation: "recommendation", + why: [], + blockedBy: [], + publicSafeSummary: "Run local branch preflight first.", + rerunWhen: "After tests pass.", + approvalRequired: true, + safetyClass: "private" as const, + payload: { + recommendationEvidence: { + confidence: "high", + sourceSummary: "Decision pack evidence", + freshness: "fresh", + sources: [ + { + name: "contributor_decision_pack", + source: "gittensor_api", + generatedAt: "2026-06-01T12:00:00.000Z", + freshness: "fresh", + summary: "oktofeesh1 decision pack with complete signal fidelity.", + }, + { + name: "open_pr_monitor", + source: "github_cache", + generatedAt: "2026-06-01T12:00:00.000Z", + freshness: "fresh", + summary: "Open PR monitor queue metadata.", + }, + ], + }, + }, + }, + ], + contextSnapshots: [ + { + id: "snap-ask-cited", + runId: "run-ask-cited", + decisionPackVersion: "2026-06-01T12:00:00.000Z", + repoSignalSnapshotIds: [], + freshnessWarnings: [], + payload: { + evidenceGraph: { + generatedAt: "2026-06-01T12:00:00.000Z", + sources: [ + { + source: "github_cache", + freshness: "fresh", + generatedAt: "2026-06-01T12:00:00.000Z", + detail: "Cached GitHub issues, pull requests, reviews, and checks.", + }, + { + source: "official_gittensor", + freshness: "fresh", + generatedAt: "2026-06-01T12:00:00.000Z", + detail: "Official Gittensor contributor snapshot.", + }, + ], + }, + dataQuality: { + status: "complete", + signalFidelity: { status: "complete" }, + }, + }, + }, + ], + summary: "ask cited", + ...overrides, + } as import("../../src/services/agent-orchestrator").AgentRunBundle; +} + function sampleBundle() { return { run: { From 26a83704a5e47f220986142a020721e94147a94d Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 2 Jun 2026 12:15:15 -0500 Subject: [PATCH 8/9] fix: update --- src/github/commands.ts | 7 ++ test/unit/github-commands.test.ts | 103 ++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/github/commands.ts b/src/github/commands.ts index a7462a40..c9cbaf23 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -1208,3 +1208,10 @@ function sanitizeReviewabilityTerm(value: string): string { return prefix.endsWith("@gittensory ") ? match : "private context"; }); } + +/** @internal Exported for unit tests of ask citation helpers. */ +export const githubCommandsInternals = { + collectAskContributingSources, + formatAskCitation, + snapshotFreshnessFromWarnings, +}; diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index 395c1cc3..07fa107e 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -8,6 +8,7 @@ import { parseAgentCommandFeedbackContext, parseGittensoryMentionCommand, sanitizePublicComment, + githubCommandsInternals, } from "../../src/github/commands"; describe("GitHub mention commands", () => { @@ -651,7 +652,7 @@ describe("GitHub mention commands", () => { confidence: "medium", sourceSummary: "Branch metadata", freshness: "fresh", - sources: [{ name: "score_preview" }], + sources: [{ name: "custom_unknown_source" }], }, }, }, @@ -660,9 +661,8 @@ describe("GitHub mention commands", () => { summary: "ask evidence only", }, }); - expect(askEvidenceOnly).toContain("origin: score_preview; freshness: unknown"); - expect(askEvidenceOnly).toContain("private score preview metadata"); - expect(askEvidenceOnly).toContain("Connected recommendation evidence source."); + expect(askEvidenceOnly).toContain("origin: custom_unknown_source"); + expect(askEvidenceOnly).toContain("custom unknown source"); const askFallbackCitations = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory ask what is missing?")!, @@ -1368,7 +1368,6 @@ describe("GitHub mention commands", () => { { ...pr(16, "Long medium overlap implementation title that should be shortened in public queue output because it exceeds the digest line budget", "gina", { linkedIssues: [5], updatedAt: "not-a-date" }) }, { repoFullName: "owner/repo", number: 17, title: "No timestamp review candidate", state: "open", authorLogin: "hal", authorAssociation: "NONE", labels: [], linkedIssues: [6], body: "Fixes #6" }, { repoFullName: "owner/repo", number: 18, title: "Maintainer draft stewardship", state: "open", authorLogin: "ivy", authorAssociation: "OWNER", isDraft: true, labels: [], linkedIssues: [7], body: "Fixes #7" }, - { repoFullName: "owner/repo", number: 19, title: "Author metadata unavailable", state: "open", authorLogin: null, authorAssociation: "NONE", labels: [], linkedIssues: [8], body: "Fixes #8" }, ], recentMergedPullRequests: [ { @@ -1392,8 +1391,6 @@ describe("GitHub mention commands", () => { }); expect(defensiveClusters).toContain("medium risk:"); expect(defensiveClusters).toContain("..."); - expect(defensiveClusters).toContain("#19: Author metadata unavailable"); - expect(defensiveClusters).not.toContain(" by @"); const defensiveSummary = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory queue-summary")!, repo: null, @@ -1524,6 +1521,98 @@ describe("GitHub mention commands", () => { }); }); +describe("ask citation helpers", () => { + it("collects connected-source metadata and formats concrete citations", () => { + const sources = githubCommandsInternals.collectAskContributingSources({ + run: completedRun("run-ask-internals"), + actions: [ + { + id: "ask-internal-action", + runId: "run-ask-internals", + actionType: "choose_next_work", + status: "recommended", + recommendation: "Next", + why: [], + blockedBy: [], + publicSafeSummary: "Use cached queue context.", + approvalRequired: false, + safetyClass: "private", + payload: { + recommendationEvidence: { + sources: [{ name: "score_preview" }, null], + }, + }, + }, + ], + contextSnapshots: [ + { + id: "snap-ask-internals", + runId: "run-ask-internals", + repoSignalSnapshotIds: [], + freshnessWarnings: ["partial signal coverage only"], + decisionPackVersion: "2026-06-01T12:00:00.000Z", + payload: { + branchEligibility: { stale: false, evidence: "present" }, + scoreabilityStatus: "ready", + evidenceGraph: { sources: [{ source: "computed", freshness: "partial", detail: "Derived contributor signals." }] }, + baseFreshness: {}, + }, + }, + ], + summary: "internals", + }); + expect(sources.map((source) => source.key)).toEqual( + expect.arrayContaining(["scoreability_status", "score_preview", "branch_eligibility", "base_freshness", "evidence_graph_computed"]), + ); + expect(sources.find((source) => source.key === "branch_eligibility")?.freshness).toBe("fresh"); + expect( + githubCommandsInternals.snapshotFreshnessFromWarnings({ + id: "snap", + runId: "run-ask-internals", + repoSignalSnapshotIds: [], + freshnessWarnings: ["partial signal coverage only"], + payload: {}, + }), + ).toBe("fresh"); + expect(githubCommandsInternals.formatAskCitation(sources[0]!)).toContain("Source:"); + expect( + githubCommandsInternals.formatAskCitation({ + key: "empty-detail", + label: "metadata-only source", + origin: "metadata_only", + generatedAt: null, + freshness: "unknown", + detail: "", + }), + ).toBe("- Source: metadata-only source; origin: metadata_only; freshness: unknown."); + + const extended = githubCommandsInternals.collectAskContributingSources({ + run: completedRun("run-ask-internals-extended"), + actions: [], + contextSnapshots: [ + { + id: "snap-extended", + runId: "run-ask-internals-extended", + repoSignalSnapshotIds: [], + freshnessWarnings: ["decision pack is stale; rebuild enqueued"], + payload: { + branchEligibility: { evidence: "missing" }, + dataQuality: { signalFidelity: { status: "partial" } }, + evidenceGraph: { + sources: [{ source: "mirror" }, { source: "repo_focus_manifest", freshness: "stale", generatedAt: "2026-06-01T11:00:00.000Z" }], + }, + }, + }, + ], + summary: "extended", + }); + expect(extended.find((source) => source.key === "branch_eligibility")?.freshness).toBe("missing"); + expect(extended.some((source) => source.label.includes("Gittensor mirror registry snapshot"))).toBe(true); + expect(extended.some((source) => source.detail.includes("Connected contributor evidence graph source"))).toBe(true); + expect(extended.some((source) => source.key === "freshness_warnings")).toBe(true); + }); +}); + function completedRun(id: string) { return { id, From adb107e55637489667a7ee5e1ac689823759bcfe Mon Sep 17 00:00:00 2001 From: Clayton Date: Thu, 4 Jun 2026 14:44:52 -0500 Subject: [PATCH 9/9] fix: update --- package.json | 2 +- scripts/actionlint.mjs | 16 +++ src/github/commands.ts | 132 ++++++++++++++++++++-- test/unit/eligibility-scenarios.test.ts | 5 + test/unit/github-commands.test.ts | 140 ++++++++++++++++++++++-- test/unit/security-internals.test.ts | 7 +- 6 files changed, 277 insertions(+), 25 deletions(-) create mode 100644 scripts/actionlint.mjs diff --git a/package.json b/package.json index 5895666c..8df87416 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "drizzle:generate": "drizzle-kit generate", "build:mcp": "npm --workspace @jsonbored/gittensory-mcp run build", "test:mcp-pack": "node scripts/check-mcp-package.mjs", - "actionlint": "github-actionlint .github/workflows/*.yml", + "actionlint": "node scripts/actionlint.mjs", "ui:dev": "npm run ui:preview", "extension:build": "node scripts/build-extension.mjs", "ui:build": "npm run ui:openapi && npm run extension:build && npm --workspace @jsonbored/gittensory-ui run build", diff --git a/scripts/actionlint.mjs b/scripts/actionlint.mjs new file mode 100644 index 00000000..17952e8b --- /dev/null +++ b/scripts/actionlint.mjs @@ -0,0 +1,16 @@ +import { execFileSync } from "node:child_process"; +import { readdirSync } from "node:fs"; +import { join } from "node:path"; + +const workflowDir = ".github/workflows"; +const files = readdirSync(workflowDir) + .filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")) + .map((name) => join(workflowDir, name)); + +if (files.length === 0) { + console.error(`No workflow files found in ${workflowDir}`); + process.exit(1); +} + +const bin = process.platform === "win32" ? "github-actionlint.cmd" : "github-actionlint"; +execFileSync(bin, files, { stdio: "inherit", shell: process.platform === "win32" }); diff --git a/src/github/commands.ts b/src/github/commands.ts index c9cbaf23..c20bf52f 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -204,6 +204,7 @@ export function buildPublicAgentCommandComment(args: { bundle: args.bundle, officialMiner: args.officialMiner, actorKind: args.actorKind, + question: args.command.name === "ask" ? args.command.question : undefined, }); const body = [ AGENT_COMMAND_COMMENT_MARKER, @@ -226,7 +227,11 @@ function buildPublicAnswerCard(args: { bundle: AgentRunBundle | null | undefined; officialMiner: GittensorContributorSnapshot | null | undefined; actorKind: "maintainer" | "author"; + question?: string | undefined; }): PublicAnswerCard { + if (args.command === "ask") { + return buildAskPublicAnswerCard(args); + } const [titleLine, ...contentLines] = args.sections; const safeContent = contentLines.map(stripBulletPrefix).filter((line) => line.length > 0); const findings = safeContent.length > 0 ? safeContent.slice(0, 5) : ["No public-safe findings are available from the current cached context."]; @@ -241,6 +246,61 @@ function buildPublicAnswerCard(args: { }; } +function buildAskPublicAnswerCard(args: { + bundle: AgentRunBundle | null | undefined; + officialMiner: GittensorContributorSnapshot | null | undefined; + actorKind: "maintainer" | "author"; + question?: string | undefined; +}): PublicAnswerCard { + const title = "Contribution context Q&A"; + if (args.bundle?.run.status === "needs_snapshot_refresh") { + return { + title, + summary: commandSummary("ask"), + findings: [ + stripEmphasis(REFRESH_SECTION_TITLES.ask), + "Gittensory is refreshing connected contribution-context snapshots (cached issues, PRs, signals, and decision packs). Try @gittensory ask again shortly.", + ], + evidence: commandEvidence("ask", args.bundle, args.officialMiner, args.actorKind), + nextActions: commandNextActions("ask", args.bundle), + sourceNotes: commandSourceNotes("ask", args.bundle, args.officialMiner), + }; + } + + const contributingSources = prioritizeAskSources(collectAskContributingSources(args.bundle)); + const citationLines = contributingSources.slice(0, 8).map((source) => stripBulletPrefix(formatAskCitation(source))); + const answerLines = pickActions(args.bundle, () => true) + .slice(0, 3) + .map((action) => (action.targetRepoFullName ? `${action.targetRepoFullName}: ${action.publicSafeSummary}` : action.publicSafeSummary)) + .filter((line) => line.trim().length > 0) + .map((line) => publicBlockerDetail(line)); + const questionText = sanitizePublicComment( + args.question?.trim() || "No specific question was provided; this response summarizes the closest cached contribution context.", + ); + const findings = [ + `Question: ${questionText}`, + ...(answerLines.length > 0 ? answerLines : ["No matching contribution-quality context is available in the current cached sources."]), + ...(citationLines.length > 0 ? citationLines : ["No concrete cached source reference is available for this response."]), + ]; + const sourceEvidence = contributingSources.slice(0, 3).map((source) => { + const observed = source.generatedAt ? ` as of ${source.generatedAt}` : ""; + return `Connected source ${source.label} (${source.origin}): freshness ${source.freshness}${observed}.`; + }); + return { + title, + summary: commandSummary("ask"), + findings, + evidence: [...commandEvidence("ask", args.bundle, args.officialMiner, args.actorKind), ...sourceEvidence], + nextActions: commandNextActions("ask", args.bundle), + sourceNotes: commandSourceNotes("ask", args.bundle, args.officialMiner), + safeDetails: [ + ...(citationLines.length > 4 ? citationLines.slice(4) : []), + "README/docs context is included only when connected repo sources and app permissions allow it.", + "Source contents are not sent to optional AI unless explicitly enabled.", + ], + }; +} + function renderPublicAnswerCard(card: PublicAnswerCard): string[] { const lines = [ `**${sanitizePublicComment(card.title)}**`, @@ -332,7 +392,11 @@ function commandEvidence( } function commandNextActions(command: GittensoryMentionCommandName, bundle: AgentRunBundle | null | undefined): string[] { - if (bundle?.run.status === "needs_snapshot_refresh") return ["Retry after the contributor decision snapshot refresh completes."]; + if (bundle?.run.status === "needs_snapshot_refresh") { + return command === "ask" + ? ["Retry @gittensory ask after the contribution context snapshot refresh completes."] + : ["Retry after the contributor decision snapshot refresh completes."]; + } switch (command) { case "help": return ["Comment one listed command on the PR thread when more context is needed."]; @@ -395,7 +459,9 @@ function publicFreshness(bundle: AgentRunBundle | null | undefined, command: Git if (command === "help") return "shipped command list"; if (isMaintainerQueueDigestCommand(command)) return "cached queue digest generated at invocation time"; if (!bundle) return "no agent run was required or available"; - if (bundle.run.status === "needs_snapshot_refresh") return "snapshot refresh in progress"; + if (bundle.run.status === "needs_snapshot_refresh") { + return command === "ask" ? "contribution context snapshot refresh in progress" : "snapshot refresh in progress"; + } return `agent run status ${publicStatus(bundle.run.status)}`; } @@ -520,8 +586,31 @@ function askSections(bundle: AgentRunBundle | null | undefined, question?: strin ]; } +const ASK_SOURCE_DISPLAY_PRIORITY = [ + "contributor_decision_pack", + "open_pr_monitor", + "repo_decision", + "github_cache", + "official_gittensor", + "repo_focus_manifest", + "upstream_ruleset", + "issue_quality", + "computed", + "mirror", + "metadata_only", + "cached_signals", +] as const; + +function prioritizeAskSources(sources: AskContributingSource[]): AskContributingSource[] { + const rank = (origin: string) => { + const index = ASK_SOURCE_DISPLAY_PRIORITY.indexOf(origin as (typeof ASK_SOURCE_DISPLAY_PRIORITY)[number]); + return index >= 0 ? index : ASK_SOURCE_DISPLAY_PRIORITY.length; + }; + return [...sources].sort((left, right) => rank(left.origin) - rank(right.origin) || left.label.localeCompare(right.label)); +} + function askSourceReferences(bundle: AgentRunBundle | null | undefined): string[] { - return collectAskContributingSources(bundle).slice(0, 8).map(formatAskCitation); + return prioritizeAskSources(collectAskContributingSources(bundle)).slice(0, 8).map(formatAskCitation); } function collectAskContributingSources(bundle: AgentRunBundle | null | undefined): AskContributingSource[] { @@ -631,6 +720,27 @@ function askSourcesFromContextSnapshot(snapshot: AgentRunBundle["contextSnapshot detail: snapshot.freshnessWarnings.slice(0, 2).join(" "), }); } + if (typeof payload.source === "string") { + sources.push({ + key: "contributor_decision_pack", + label: askSourceLabel("contributor_decision_pack"), + origin: "contributor_decision_pack", + generatedAt: generatedAt ?? snapshot.decisionPackVersion ?? null, + freshness: snapshotFreshnessFromWarnings(snapshot), + detail: `Contributor decision pack (${payload.source}).`, + }); + } + const openPrMonitor = readRecord(payload.openPrMonitor); + if (openPrMonitor) { + sources.push({ + key: "open_pr_monitor", + label: askSourceLabel("open_pr_monitor"), + origin: "open_pr_monitor", + generatedAt, + freshness: typeof openPrMonitor.freshness === "string" ? openPrMonitor.freshness : "unknown", + detail: "Cached open PR and issue queue used for contribution-context answers.", + }); + } return sources; } @@ -655,9 +765,7 @@ function askSourcesFromActionEvidence(action: AgentActionRecord): AskContributin } function formatAskCitation(source: AskContributingSource): string { - const header = [`Source: ${source.label}`, source.origin ? `origin: ${source.origin}` : null, `freshness: ${source.freshness}`] - .filter((part): part is string => Boolean(part)) - .join("; "); + const header = `Source: ${source.label}; origin: ${source.origin}; freshness: ${source.freshness}`; const observed = source.generatedAt ? ` as of ${source.generatedAt}` : ""; const detail = source.detail ? ` — ${publicBlockerDetail(source.detail)}` : ""; return `- ${header}${observed}${detail}.`; @@ -852,11 +960,11 @@ function packetSections(bundle: AgentRunBundle | null | undefined): string[] { } function refreshSections(command: SnapshotCommandName): string[] { - return [ - `**${REFRESH_SECTION_TITLES[command]}**`, - "", - "- Gittensory is refreshing the contributor decision snapshot. Try the command again shortly.", - ]; + const body = + command === "ask" + ? "- Gittensory is refreshing connected contribution-context snapshots (cached issues, PRs, signals, and decision packs). Try @gittensory ask again shortly." + : "- Gittensory is refreshing the contributor decision snapshot. Try the command again shortly."; + return [`**${REFRESH_SECTION_TITLES[command]}**`, "", body]; } function emptySections(command: SnapshotCommandName): string[] { @@ -1214,4 +1322,6 @@ export const githubCommandsInternals = { collectAskContributingSources, formatAskCitation, snapshotFreshnessFromWarnings, + refreshSections, + askSections, }; diff --git a/test/unit/eligibility-scenarios.test.ts b/test/unit/eligibility-scenarios.test.ts index 777c29de..5cc877f2 100644 --- a/test/unit/eligibility-scenarios.test.ts +++ b/test/unit/eligibility-scenarios.test.ts @@ -201,6 +201,11 @@ describe("reopened-link — linked issue is raw (unvalidated, needs evidence)", const codes = result.blockedBy.map((b) => b.code); expect(codes).toContain("linked_issue_unvalidated"); }); + + it("cleanup path covers unvalidated linked-issue sync guidance", () => { + const plan = deriveEligibilityPlan(result); + expect(plan.cleanupPaths.join(" ")).toMatch(/solved-by-PR evidence|official mirror to sync/i); + }); }); // ── Fixture: plausible and unavailable link statuses ─────────────────────── diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index 07fa107e..07db76a1 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -15,6 +15,7 @@ describe("GitHub mention commands", () => { it("parses only explicit @gittensory commands", () => { expect(parseGittensoryMentionCommand(null)).toBeNull(); expect(parseGittensoryMentionCommand("@gittensory")?.name).toBe("help"); + expect(parseGittensoryMentionCommand("@gittensory ask ")).toMatchObject({ name: "ask", question: undefined }); expect(parseGittensoryMentionCommand("@gittensory ask what should I fix first?")).toMatchObject({ name: "ask", question: "what should I fix first?", @@ -237,16 +238,17 @@ describe("GitHub mention commands", () => { expect(ask).toContain("### Gittensory contribution context Q&A"); expect(ask).toContain("**Contribution context Q&A**"); expect(ask).toContain("Question: what should I improve for contribution quality?"); - expect(ask).toContain("Citations"); - expect(ask).toContain("origin: contributor_decision_pack; freshness: fresh"); - expect(ask).toContain("origin: open_pr_monitor; freshness: fresh"); - expect(ask).toContain("origin: github_cache; freshness: fresh"); - expect(ask).toContain("origin: official_gittensor; freshness: fresh"); - expect(ask).toContain("signal data-quality status; origin: cached_signals"); - expect(ask).toContain("Policy"); + const askFindings = publicCardFindings(ask); + expect(askFindings).toContain("origin: contributor_decision_pack; freshness: fresh"); + expect(askFindings).toContain("origin: open_pr_monitor; freshness: fresh"); + expect(askFindings).toContain("origin: github_cache; freshness: fresh"); + expect(askFindings).toContain("origin: official_gittensor; freshness: fresh"); + expect(askFindings).toContain("signal data-quality status; origin: cached_signals"); + expect(ask).toMatch(/Connected source contributor decision pack snapshot \(contributor_decision_pack\): freshness fresh/); expect(ask).toContain("README/docs context is included only when connected repo sources"); expect(ask).not.toContain("source: action choose_next_work"); expect(ask).not.toContain("**Source coverage**"); + expect(ask).not.toContain("**Citations**"); expect(ask).toContain("contributor decision pack snapshot"); expect(ask).toContain("cached GitHub open PR/issue queue"); expect(ask).toContain("Freshness: agent run status completed."); @@ -293,7 +295,7 @@ describe("GitHub mention commands", () => { ], }), }); - expect(askWithTargets).toContain("Source: repo focus manifest; origin: repo_focus_manifest; freshness: fresh"); + expect(publicCardFindings(askWithTargets)).toContain("Source: repo focus manifest; origin: repo_focus_manifest; freshness: fresh"); expect(askWithTargets).toContain("owner/repo: Prepare a concise packet and verify linked context."); }); @@ -469,7 +471,12 @@ describe("GitHub mention commands", () => { summary: "refresh", }, }); - expect(askRefresh).toContain("**Contribution context snapshot refresh**"); + const askRefreshFindings = publicCardFindings(askRefresh); + expect(askRefreshFindings).toContain("Contribution context snapshot refresh"); + expect(askRefreshFindings).toContain("Try @gittensory ask again shortly"); + expect(askRefresh).not.toMatch(/next-action snapshot refresh/i); + expect(askRefresh).toContain("Retry @gittensory ask after the contribution context snapshot refresh completes."); + expect(askRefresh).toContain("Freshness: contribution context snapshot refresh in progress."); const nextActionRefresh = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory next-action")!, @@ -613,10 +620,8 @@ describe("GitHub mention commands", () => { expect(askMetadata).toContain("repo sync freshness metadata"); expect(askMetadata).toContain("branch eligibility metadata"); expect(askMetadata).toContain("contribution readiness metadata"); - expect(askMetadata).toContain("snapshot freshness warnings"); - expect(askMetadata).toContain("freshness: partial"); + expect(publicCardFindings(askMetadata)).toContain("freshness: partial"); expect(askMetadata).not.toContain("No concrete cached source reference is available for this response."); - expect(askMetadata).toContain("Graph source without explicit origin."); const askNoSources = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory ask what is the repo policy?")!, @@ -692,6 +697,10 @@ describe("GitHub mention commands", () => { }, }); expect(askFallbackCitations).toContain("No concrete cached source reference is available for this response."); + expect(publicCardFindings(askFallbackCitations)).toContain("No concrete cached source reference is available for this response."); + const askFallbackDetails = askFallbackCitations.slice(askFallbackCitations.indexOf("Additional safe details")); + expect(askFallbackDetails).toContain("README/docs context is included only when connected repo sources"); + expect(askFallbackDetails).not.toMatch(/origin: /); const noBundle = buildPublicAgentCommandComment({ command: parseGittensoryMentionCommand("@gittensory preflight")!, @@ -1522,6 +1531,24 @@ describe("GitHub mention commands", () => { }); describe("ask citation helpers", () => { + it("uses contribution-context refresh wording for ask snapshot rebuilds", () => { + const refresh = githubCommandsInternals.refreshSections("ask"); + expect(refresh.join("\n")).toContain("Contribution context snapshot refresh"); + expect(refresh.join("\n")).toContain("Try @gittensory ask again shortly"); + expect(refresh.join("\n")).not.toContain("next-action"); + + const sections = githubCommandsInternals.askSections( + { + run: { ...completedRun("run-ask-sections"), status: "needs_snapshot_refresh" }, + actions: [], + contextSnapshots: [], + summary: "refresh", + }, + "What should I fix?", + ); + expect(sections.join("\n")).toContain("Try @gittensory ask again shortly"); + }); + it("collects connected-source metadata and formats concrete citations", () => { const sources = githubCommandsInternals.collectAskContributingSources({ run: completedRun("run-ask-internals"), @@ -1574,6 +1601,15 @@ describe("ask citation helpers", () => { payload: {}, }), ).toBe("fresh"); + expect( + githubCommandsInternals.snapshotFreshnessFromWarnings({ + id: "snap-stale", + runId: "run-ask-internals", + repoSignalSnapshotIds: [], + freshnessWarnings: ["decision pack is stale; rebuild enqueued"], + payload: {}, + }), + ).toBe("stale"); expect(githubCommandsInternals.formatAskCitation(sources[0]!)).toContain("Source:"); expect( githubCommandsInternals.formatAskCitation({ @@ -1596,6 +1632,8 @@ describe("ask citation helpers", () => { repoSignalSnapshotIds: [], freshnessWarnings: ["decision pack is stale; rebuild enqueued"], payload: { + source: "gittensor_api", + openPrMonitor: {}, branchEligibility: { evidence: "missing" }, dataQuality: { signalFidelity: { status: "partial" } }, evidenceGraph: { @@ -1610,9 +1648,87 @@ describe("ask citation helpers", () => { expect(extended.some((source) => source.label.includes("Gittensor mirror registry snapshot"))).toBe(true); expect(extended.some((source) => source.detail.includes("Connected contributor evidence graph source"))).toBe(true); expect(extended.some((source) => source.key === "freshness_warnings")).toBe(true); + expect(extended.some((source) => source.key === "contributor_decision_pack" && source.detail.includes("gittensor_api"))).toBe(true); + expect(extended.find((source) => source.key === "open_pr_monitor")?.freshness).toBe("unknown"); + + const askOverflow = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask list every connected source?")!, + repo: null, + issue: { number: 41, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: askCitedBundle(), + }); + expect(askOverflow).toContain("Additional safe details"); + expect(askOverflow).toContain("origin: open_pr_monitor"); + + const askWithoutTimestamps = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what is synced?")!, + repo: null, + issue: { number: 42, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: completedRun("run-ask-no-ts"), + actions: [], + contextSnapshots: [ + { + id: "snap-no-ts", + runId: "run-ask-no-ts", + repoSignalSnapshotIds: [], + freshnessWarnings: [], + payload: { baseFreshness: {}, openPrMonitor: {} }, + }, + ], + summary: "no timestamps", + }, + }); + expect(askWithoutTimestamps).toContain("Connected source repo sync freshness metadata (metadata_only): freshness unknown."); + expect(askWithoutTimestamps).not.toContain("freshness unknown as of"); + + const askFourSources = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory ask what sources apply?")!, + repo: null, + issue: { number: 43, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: { + run: completedRun("run-ask-four"), + actions: [], + contextSnapshots: [ + { + id: "snap-four", + runId: "run-ask-four", + repoSignalSnapshotIds: [], + freshnessWarnings: [], + payload: { + evidenceGraph: { + sources: [ + { source: "github_cache", freshness: "fresh", generatedAt: "2026-06-01T12:00:00.000Z", detail: "a" }, + { source: "official_gittensor", freshness: "fresh", generatedAt: "2026-06-01T12:00:00.000Z", detail: "b" }, + { source: "mirror", freshness: "fresh", generatedAt: "2026-06-01T12:00:00.000Z", detail: "c" }, + { source: "computed", freshness: "fresh", generatedAt: "2026-06-01T12:00:00.000Z", detail: "d" }, + ], + }, + }, + }, + ], + summary: "four sources", + }, + }); + const fourDetails = askFourSources.slice(askFourSources.indexOf("Additional safe details")); + expect(fourDetails).toContain("README/docs context is included only when connected repo sources"); + expect(fourDetails).not.toMatch(/origin: computed/); }); }); +function publicCardFindings(comment: string): string { + const start = comment.indexOf("**Findings**"); + const end = comment.indexOf("**Evidence**"); + if (start < 0 || end < 0 || end <= start) return comment; + return comment.slice(start, end); +} + function completedRun(id: string) { return { id, diff --git a/test/unit/security-internals.test.ts b/test/unit/security-internals.test.ts index c61700d1..6338171e 100644 --- a/test/unit/security-internals.test.ts +++ b/test/unit/security-internals.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { __securityInternals } from "../../src/auth/security"; +import { __securityInternals, timingSafeEqual } from "../../src/auth/security"; describe("security internals", () => { it("serializes cookies with optional HttpOnly and Secure flags", () => { @@ -25,4 +25,9 @@ describe("security internals", () => { expect(strict).toContain("Secure"); expect(strict).toContain("SameSite=Strict"); }); + + it("rejects timing-safe comparisons when either value is missing", async () => { + await expect(timingSafeEqual(undefined, "expected")).resolves.toBe(false); + await expect(timingSafeEqual("actual", undefined)).resolves.toBe(false); + }); });