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
4 changes: 4 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ Turn a long diff into an ordered explanation, one source-backed stop at a time.

- Follow an agent-organized route through related changes.
- See current and upcoming stops alongside their source anchors.
- Explanations must trace behavioral claims through callers, guards, and consumers.
Review questions are optional: agents must check their premises and explain
answers already settled by the repository, separating planned behavior from
implemented behavior.
- Explore freely, then return to your current stop.
- Switch the right pane between **Tour**, **Questions**, and **Comments**. Each
section gets the full pane; tab switches keep drafts and reading positions.
Expand Down
57 changes: 56 additions & 1 deletion scripts/semantic-diff-tour-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,63 @@ interface Case {
start?: number;
end?: number;
previous?: { title: string; objective: string };
/** Unchanged source outside the patch: the explanation must inspect the captured tree. */
supportingFiles?: Record<string, string>;
expectedStatus: string;
review: string[];
}
const source = (lines: string[]) => lines.join("\n") + "\n";
const sectionBefore = source([
"sealed interface Section<out T> {",
" data object Failed : Section<Nothing>",
" data object Retrying : Section<Nothing>",
" data class Shown<T>(val content: T) : Section<T>",
"}",
]);
const sectionAfter = sectionBefore.replace("val content: T)", "val content: T, val updateFailed: Boolean = false)");
const sectionRenderer = source([
"fun render(section: Section<String>): String = when (section) {",
" Section.Failed -> \"error card\"",
" Section.Retrying -> \"skeleton\"",
" is Section.Shown -> section.content",
"}",
]);
const cases: Case[] = [
{
id: "kotlin-guarded-retry", path: "Section.kt", expectedStatus: "analysed",
before: sectionBefore, after: sectionAfter,
previous: { title: "Retry content", objective: "Retrying replaces shown content with a skeleton." },
supportingFiles: {
"Renderer.kt": sectionRenderer,
"Controller.kt": source([
"fun retry(section: Section<String>): Section<String> =",
" if (section == Section.Failed) Section.Retrying else section",
"fun refresh(section: Section<String>): Section<String> = section",
]),
},
review: ["inspect unchanged Controller.kt in the captured tree, not just the state declaration", "retry only transitions Failed to Retrying; Shown is preserved", "correct the false premise in the previous stop", "do not ask whether retry should keep content that it does not discard", "the new flag represents a failed update; no code sets it yet", "a concise explanation without any review question is valid"],
},
{
id: "kotlin-planned-retry", path: "Section.kt", expectedStatus: "analysed",
before: sectionBefore, after: sectionAfter,
supportingFiles: {
"Renderer.kt": sectionRenderer,
"PLAN.md": "The controller is not implemented yet. Planned rules: Failed enters Retrying on an error-card retry. Shown remains Shown during refresh; a failed refresh keeps its content and sets updateFailed.\n",
},
review: ["read PLAN.md and label its transition rules as planned", "only the state contract and renderer are implemented", "do not claim an implemented Shown-to-Retrying path or content loss", "do not turn missing controller code into a defect or a product decision the plan already settled", "the updateFailed flag is representable, not proof of a wired refresh"],
},
{
id: "kotlin-reachable-content-loss", path: "Section.kt", expectedStatus: "analysed",
before: sectionBefore, after: sectionAfter,
supportingFiles: {
"Renderer.kt": sectionRenderer,
"Controller.kt": source([
"fun retry(section: Section<String>): Section<String> = Section.Retrying",
"fun refreshShown(content: String): String = render(retry(Section.Shown(content)))",
]),
},
review: ["inspect Controller.kt and Renderer.kt before claiming content loss", "refreshShown passes Shown into an unconditional retry and renders the resulting skeleton", "a content-retention question is legitimate here if it names this proven path", "do not blindly apply the safe Failed-only example in the prompt", "do not invent the author's intent or a failed-refresh implementation"],
},
{
id: "typescript-optional-discount", path: "cart.ts", expectedStatus: "analysed", start: 10, end: 13,
before: source(["export interface Item { price: number; quantity: number; }", "", "export function total(items: Item[]): number {", " return items.reduce((sum, item) => sum + item.price * item.quantity, 0);", "}"]),
Expand Down Expand Up @@ -97,7 +149,7 @@ await writeJSON(join(artifactRoot, `${mode.slice(2)}-environment.json`), {
selection, modelNote: "Configured CLI default; resolved model must be verified separately, not inferred.",
cliVersion: (await runProcess(["codex", "--version"], checkout)).stdout.trim(),
harnessHash: digest(await readFile(import.meta.filename, "utf8")),
sourceHashes: Object.fromEntries(await Promise.all(["src/agent.ts", "src/semantic-diff-tour.ts", "src/semantic-diff.ts", "src/comparison-source.ts", "src/git.ts"].map(async (path) => [path, digest(await readFile(join(checkout, path), "utf8"))]))),
sourceHashes: Object.fromEntries(await Promise.all(["src/agent.ts", "src/tour-grounding.ts", "src/semantic-diff-tour.ts", "src/semantic-diff.ts", "src/comparison-source.ts", "src/git.ts"].map(async (path) => [path, digest(await readFile(join(checkout, path), "utf8"))]))),
scope: "Human-authored fixed tour stops; real production explanation calls. No planner, browser, statistical A/B or compiler evaluation.",
});
let failures = 0;
Expand All @@ -115,6 +167,9 @@ for (const fixture of selected) {
await git(repositoryRoot, "config", "user.email", "evaluation@example.test");
await git(repositoryRoot, "config", "core.hooksPath", "/dev/null");
await writeFile(join(repositoryRoot, fixture.oldPath ?? fixture.path), fixture.before);
for (const [path, content] of Object.entries(fixture.supportingFiles ?? {})) {
await writeFile(join(repositoryRoot, path), content);
}
await git(repositoryRoot, "add", ".");
await git(repositoryRoot, "-c", "commit.gpgsign=false", "commit", "-m", "Controlled before source");
if (fixture.oldPath) await rm(join(repositoryRoot, fixture.oldPath));
Expand Down
21 changes: 20 additions & 1 deletion skills/diffcompass/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ merely because the first or current stop is visible in the browser.
Prompt kinds:

- `tour.outline`: return only the JSON shape requested by the event. Inspect the diff and repository before responding. Treat this as a fresh first-time reading of the selected base-to-tip or working-tree comparison. Ignore prior chat, caller implementation history, remembered iterations, and claims about earlier changes unless that chronology is directly present in the selected comparison or repository evidence you explicitly inspect. Ground the title, summary, phase names, phase summaries, objectives, architecture summaries, and diagrams only in that evidence. Read every requested anchor from the full event: a step anchor is `{ path, side, startLine, endLine }`, not `{ path, side, line }`. Omitting `startLine` or `endLine` invalidates the step; if every step is invalid, the complete outline fails with no anchored tour phases. Treat `TourPhase` objects as ordered conceptual layers and derive their order from behavior and dependency flow, normally contract or foundation before implementation before integration or effects when supported by evidence, never from file order or remembered implementation chronology. Every phase summary must briefly state what its layer establishes and how it enables the next layer, or how the final layer reaches the observable result or verification. Use only the currently supported architecture and sequence diagram kinds when they materially clarify a layer; do not invent state-machine or ER diagram types. Tour startup may send two consecutive outline-kind prompts: the first requests the full route and candidate analysis; the second is a focused, authoritative finding audit and requests only final finding JSON. Complete and acknowledge both in order.
- `tour.explanation`: return the conversational explanation for that stop, using only the selected comparison and repository evidence you explicitly inspect. Keep the same first-read boundary: unsupported references to corrections, iterations, follow-ups, or a prior/current state must not enter the explanation. The preceding tour step is allowed as generated route context because it is part of this selected comparison, not caller history.
- `tour.explanation`: return the conversational explanation for that stop, using only the selected comparison and repository evidence you explicitly inspect. Keep the same first-read boundary: unsupported references to corrections, iterations, follow-ups, or a prior/current state must not enter the explanation. The preceding tour step is allowed as generated route context because it is part of this selected comparison, not caller history. Generated route context is not independent evidence; correct an unsupported premise rather than repeating it. Apply the behavioral evidence check below before returning.
- `review.question`: answer the reviewer using the current step, selected-line context, and preceding reviewer conversation. Lead with the direct answer and default to a concise response unless the reviewer asks for depth. Use that context to resolve references such as "this" or "these comments" instead of discarding information already present in the conversation. Do not narrate tool limitations or offer unrelated follow-up work. Ask a clarifying question only when multiple plausible interpretations would produce materially different changes. Once the reviewer clarifies the intended scope, follow that clarification without substituting a preferred implementation or relitigating the choice. When the event prompt supplies an isolated proposal workspace, modify files only inside that exact workspace and only when the reviewer explicitly requests a concrete change. Never edit the original reviewed repository, stage, or commit. Run proportionate checks inside the proposal workspace and write a concise UTF-8 verification summary to a temporary file. Include commands and outcomes, or state why a relevant check was not run. Then call `diffcompass bridge refresh --url URL --token-file TOKEN_FILE --event EVENT_ID --lease LEASE_ID --verification-file VERIFICATION_FILE` before responding. The refresh captures the isolated edits as a pending bundle; it does not apply them to the reviewed working tree. Delete the verification file after the bridge accepts the refresh. Tell the reviewer that the proposal is ready for Apply or Reject rather than claiming it is already applied. This event-bound refresh is what authorizes the question's answer to cross that comparison update.

## Finish
Expand All @@ -155,6 +155,25 @@ diffcompass bridge ack --url URL --token-file TOKEN_FILE --event EVENT_ID --leas

Never invent feedback not present in the event. Every repository change after review closure must be directly required to resolve the submitted note or comments. Preserve unrelated work and report independently discovered issues separately instead of including them in the feedback implementation. If implementation needs authority beyond the review scope, stop and ask the user before making that change.

### Behavioral evidence for tour prose

Apply this check to outline objectives and summaries as well as stop explanations.
Trace behavioral claims through the trigger or caller, guards, state writes, and
consumers in the captured source, including relevant unchanged code. A type or
renderer branch alone does not establish a reachable transition. A payload-free
`Retrying` case, for example, does not imply `Shown` ever enters it or loses its
content; check retry and refresh entry points first.

Review questions are optional, never a quota. Their premises need the same
evidence as factual prose. First try to answer the question from the implementation,
callers, tests, and repository contracts. Explain an already-settled answer instead
of asking the reviewer to investigate it. Ask only about a remaining decision or
uncertainty with an established premise, briefly naming the supporting symbol or
guard. Omit unsupported concerns instead of dressing them as questions. If the
state writer is not implemented, distinguish planned transitions from implemented
rendering; neither a plan nor an expected test proves runtime behavior. Keep this
check internal and return only the event's requested output.

### Narrative Briefing prompts

The initial outline prompt may request `briefingProposal` alongside the tour. Use the outcome-first approach: the title alone should make the concrete change understandable. Name what becomes possible, changes, or is prevented; internal changes should name their technical outcome without inventing user benefits or unsupported guarantees. Open with a short prose paragraph explaining that outcome and the relevant before-to-after difference. Establish why it matters from supplied author intent; otherwise describe observed behavior and explicitly label inferred rationale.
Expand Down
12 changes: 8 additions & 4 deletions src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BRIEFING_AUDIT_PROMPT, BriefingAuditValidationError, briefingSequenceFl
import { BRIEFING_SOURCE_PROMPT, BriefingSourceValidationError, briefingSourceContext, captureBriefingSourceDecisions, missingBriefingSources } from "./briefing-sources.ts";
import { createHash } from "node:crypto";
import { syntaxContextForTourStep } from "./semantic-diff-tour.ts";
import { TOUR_BEHAVIOR_GROUNDING } from "./tour-grounding.ts";
import { FILE_EXPLANATION_PROMPT, fileSummarySource, usableFileExplanation } from "./file-explanations.ts";
import { ReviewError } from "./errors.ts";
import { MAP_NODE_ROLE_PROMPT } from "./map-node-roles.ts";
Expand Down Expand Up @@ -82,7 +83,9 @@ interface GeminiStreamEvent {
response?: string;
}

const TOUR_CONTEXT_BOUNDARY = `Review-context boundary (mandatory for tour generation): Treat this as a fresh, first-time reading of the selected comparison. Ignore prior chat, caller-session context, implementation chronology, earlier iterations, previous tours, and unstated claims about what was changed before. They are not review evidence.
const TOUR_CONTEXT_BOUNDARY = `${TOUR_BEHAVIOR_GROUNDING}

Review-context boundary (mandatory for tour generation): Treat this as a fresh, first-time reading of the selected comparison. Ignore prior chat, caller-session context, implementation chronology, earlier iterations, previous tours, and unstated claims about what was changed before. They are not review evidence.

Ground tour framing only in the selected base-to-tip or working-tree comparison and repository files or symbols you explicitly inspect for this comparison. This rule applies to the tour title, summary, phase names and summaries, objectives, architecture titles and summaries, diagrams, and explanations. Do not describe a change as a correction, iteration, follow-up, or current state relative to earlier work unless that relationship is directly supported by the selected comparison or inspected repository evidence. If it is unsupported, use neutral first-read language.

Expand Down Expand Up @@ -1618,20 +1621,21 @@ For a detailed walkthrough, use up to 24 steps and expand only intermediate valu
Tour: ${outline.title}
Tour purpose: ${outline.summary}
Current phase: ${phase ? `${phase.title} — ${phase.summary}` : "Unknown"}
${previousStep ? `Previously established: ${previousStep.title} — ${previousStep.objective}` : "This is the first stop, so establish the input, trigger, or prerequisite."}
${previousStep ? `Previous stop (generated route context, not evidence): ${previousStep.title} — ${previousStep.objective}` : "This is the first stop, so establish the input, trigger, or prerequisite."}

Tour step ${index + 1} of ${total}: ${step.title}
Anchor: ${step.anchor.path}:${step.anchor.startLine}-${step.anchor.endLine} (${step.anchor.side} side)
Objective: ${step.objective}
Comparison fingerprint: ${this.comparisonFingerprint}
Captured source: before ${comparison?.mergeBase ?? "unavailable"}, after ${comparison?.sourceTree ?? "unavailable"}. For supporting source outside the patch, inspect these exact Git objects with git show, respecting old/new rename paths. Never substitute current workspace files. If captured source is unavailable, narrow the explanation to the supplied evidence.

${syntaxContext}

Use the narrative context above silently. Explain this anchor as the next link in the tour's causal journey. If this is not the first stop, connect its mechanism to the fact established immediately before it. Introduce one new idea and explain the mechanism visible in the selected code, not only the resulting behavior.
Use the narrative context above silently, after checking its premises against the captured source. Explain this anchor as the next link in the tour's causal journey. If this is not the first stop, connect its mechanism to the preceding stop only where the inspected evidence supports that connection. Introduce one new idea and explain the mechanism visible in the selected code, not only the resulting behavior.

Return only the finished reviewer-facing explanation. Do not include progress updates, plans to inspect code, skill announcements, or tool-use commentary in the answer. Keep any work-in-progress messages separate from the final response.

Use plain technical language for a reviewer who knows programming fundamentals. Keep it under 150 words in two or three short paragraphs. Use short active sentences, one idea per sentence, and one consistent term for each concept. Avoid metaphors and idioms. Avoid project-specific jargon unless the code requires it; explain any necessary term in a few words. Say why the mechanism matters and what the reviewer should check. Do not repeat the title, objective, or file path. Do not predict, name, or summarize the next step; the route may change independently of this explanation. End with a short instruction to select Next when ready. Do not modify files. Do not use prior chat or implementation chronology to explain this stop. Use only the selected comparison and repository evidence you explicitly inspect.`;
Use plain technical language for a reviewer who knows programming fundamentals. Keep it under 150 words in two or three short paragraphs. Use short active sentences, one idea per sentence, and one consistent term for each concept. Avoid metaphors and idioms. Avoid project-specific jargon unless the code requires it; explain any necessary term in a few words. Say why the mechanism matters. Include a review question only when it passes the behavioral evidence requirements above; a complete explanation needs no question. Do not repeat the title, objective, or file path. Do not predict, name, or summarize the next step; the route may change independently of this explanation. End with a short instruction to select Next when ready. Do not modify files. Do not use prior chat or implementation chronology to explain this stop. Use only the selected comparison and repository evidence you explicitly inspect.`;
const needsAttachment = this.adapterId === "gemini" || this.adapterId === "copilot";
const promptWithDiff = needsAttachment ? prompt : `${prompt}\n\n<diff>\n${comparison?.patch ?? ""}\n</diff>`;
const attachment = needsAttachment && comparison ? { name: "review.diff", content: comparison.patch } : undefined;
Expand Down
Loading
Loading