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
97 changes: 97 additions & 0 deletions dashboard/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,63 @@ const stageNames = [
"pr"
];

function summaryText(value) {
const result = String(value ?? "").replace(/\s+/gu, " ").trim().slice(0, 1000);
if (/(?:password|passwd|secret|token|authorization|bearer|cookie|connection.?string|private.?key)/iu.test(result)) {
return "[Sensitive detail omitted]";
}
return result.replace(/https?:\/\/\S+/giu, "[Link omitted]") || "Not verified";
}

function jobSummarySections(job) {
const gates = job.validation?.gates ?? [];
const unresolved = stageNames.filter((name) => job.stages?.[name]?.status !== "passed");
const evidence = job.testEvidence;
const interim = ["running", "queued", "holding"].includes(job.status);
const stage = (name) => {
const recorded = job.stages?.[name];
return `${name}: ${summaryText(recorded?.status)} - ${summaryText(recorded?.message)}`;
};
return [
{ heading: interim ? "Interim outcome" : "Recorded outcome", lines: [
`Job: ${summaryText(job.id)} | Status: ${summaryText(job.status)}`,
`Request: ${summaryText(job.request)}`,
`Environment: ${summaryText(job.targetEnvironment)} | Mode: ${summaryText(job.mode)}`,
"Job completion is not a claim of complete regression coverage."
] },
{ heading: "Investigation and changes (agent-reported)", lines: [
stage("intake"), stage("diagnosis"), stage("reproduce"), stage("fix"), stage("review"),
"Exact changed-file and commit inventory: Not verified by this summary."
] },
{ heading: "Tests and regression", lines: [
...gates.map((gate) => `${summaryText(gate.id)}: ${summaryText(gate.status)}${gate.required ? " (required)" : " (optional or not applicable)"}${Number.isFinite(gate.durationMs) ? ` | ${gate.durationMs} ms` : ""}${gate.summary ? ` | ${summaryText(gate.summary)}` : ""}`),
...(gates.length ? [] : ["No executor-backed gate results recorded."]),
`Gate scope: ${summaryText(job.validation?.scope)}. Only the listed configured gates are accounted for.`,
`Agent-reported scenario: ${evidence?.reported ? summaryText(evidence.scenario) : "Not verified"}`,
`Data source: ${summaryText(evidence?.source)} | Mutation mode: ${summaryText(evidence?.mutationMode)}`,
stage("local-stack"), stage("live-test"),
"Agent stage/scenario messages are not independent runner proof. Synthetic or intercepted tests do not prove real backend writes."
] },
{ heading: "Findings and must-know", lines: [
...(job.bugs ?? []).map((bug) => `${summaryText(bug.id)}: ${summaryText(bug.outcome)} | Owner: ${summaryText(bug.owner)} | ${summaryText(bug.summary)}`),
...((job.bugs ?? []).length ? [] : ["Per-bug findings: Not verified."]),
`Publication: ${stage("pr")}`,
`PR readiness: ${summaryText(job.pullRequestReadiness?.message)}`,
"Deployed-system checks do not verify unpublished local code. Publication does not establish merge or deployment."
] },
{ heading: "Workarounds and maintenance traps", lines: [
"Validated workaround, tradeoffs and fix-induced side effects: Not verified unless explicitly recorded in the findings above.",
"Do not infer that no risks exist merely because no finding was recorded."
] },
{ heading: "Remaining gaps and next actions", lines: [
...unresolved.map(stage),
...gates.filter((gate) => gate.status !== "passed").map((gate) => `Check ${summaryText(gate.id)}: ${summaryText(gate.status)}${gate.reason ? ` | ${summaryText(gate.reason)}` : ""}`),
...(job.error ? [`Recorded error: ${summaryText(job.error)}`] : []),
"Review unverified scenarios, missing evidence and recorded blockers before claiming full coverage. No checks were run by opening this summary."
] }
];
}

const form = document.querySelector("#job-form");
const startButton = document.querySelector("#start-button");
const formError = document.querySelector("#form-error");
Expand Down Expand Up @@ -930,6 +987,44 @@ let selectedJobId = null;
let pendingRetryAfterConnectJobId = null;
let playwrightAuthenticationRunning = false;
const observedJobs = new Map();
const summaryButton = document.querySelector("#job-summary-open");
const summaryDialog = document.querySelector("#job-summary-dialog");
const summaryContent = document.querySelector("#job-summary-content");
let summarySelection = null;

function renderSummary() {
summaryContent.replaceChildren();
const job = summarySelection?.instanceId === dashboardInstanceId
? observedJobs.get(summarySelection.jobId) : null;
if (!job) {
summaryContent.textContent = "This job's evidence is unavailable or the dashboard changed. Close and reopen the summary for the selected job.";
return;
}
for (const section of jobSummarySections(job)) {
const heading = document.createElement("h3");
heading.textContent = section.heading;
const list = document.createElement("ul");
for (const line of section.lines) {
const item = document.createElement("li");
item.textContent = line;
list.append(item);
}
summaryContent.append(heading, list);
}
}

summaryButton.addEventListener("click", () => {
if (draftingConversation || !observedJobs.has(selectedJobId)) {return;}
summarySelection = { instanceId: dashboardInstanceId, jobId: selectedJobId };
renderSummary();
summaryDialog.showModal();
});
document.querySelector("#job-summary-close").addEventListener("click", () => summaryDialog.close());
summaryDialog.addEventListener("close", () => {
summarySelection = null;
summaryContent.replaceChildren();
summaryButton.focus();
});
const expandedJobIds = new Set();
let renderedEvidenceJobId = null;
let playwrightEvidenceRequestId = 0;
Expand Down Expand Up @@ -1364,6 +1459,8 @@ function inferActiveStageFromLogs(logs = []) {
}

function renderJob(job, currentActiveJob = job) {
summaryButton.disabled = !job;
if (summaryDialog.open) {renderSummary();}
currentJobFullscreen.disabled = !currentActiveJob;
currentJobFullscreen.hidden = !currentActiveJob;
const currentJobTitle = document.querySelector("#current-job-dialog-title");
Expand Down
9 changes: 9 additions & 0 deletions dashboard/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ <h1 id="workspace-title">FixLab</h1>
</div>
<div id="readiness" class="readiness">Checking repository…</div>
<button id="current-job-fullscreen" type="button" disabled>Current job full screen</button>
<button id="job-summary-open" type="button" disabled>View summary</button>
</header>

<div class="conversation-column" id="chat-view" tabindex="-1">
Expand Down Expand Up @@ -541,6 +542,14 @@ <h2>Raw logs</h2>
</section>
</main>
</div>
<dialog id="job-summary-dialog" aria-labelledby="job-summary-title">
<div class="section-heading">
<h2 id="job-summary-title">Job summary</h2>
<button id="job-summary-close" type="button">Close summary</button>
</div>
<p class="guidance">Recorded evidence for the selected job, refreshed while open. No AI call, test execution, file creation or job changes. Missing findings remain unverified.</p>
<article id="job-summary-content"></article>
</dialog>
<dialog id="current-job-dialog" aria-labelledby="current-job-dialog-title">
<div class="section-heading">
<h2 id="current-job-dialog-title">Current job details and evidence</h2>
Expand Down
20 changes: 19 additions & 1 deletion dashboard/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,23 @@ pre {
animation: none;
}
}
#job-summary-dialog {
width: min(850px, calc(100vw - 2rem));
max-height: calc(100dvh - 2rem);
box-sizing: border-box;
overflow: auto;
overflow-wrap: anywhere;
padding: 1.5rem;
border: 1px solid #d8d6e8;
border-radius: 12px;
color: #282638;
background: #f7f7fc;
}

#job-summary-content li {
margin-bottom: 0.5rem;
}

#current-job-dialog {
width: 100vw;
max-width: 100vw;
Expand All @@ -1380,7 +1397,8 @@ pre {
background: #f7f7fc;
}

#current-job-dialog::backdrop {
#current-job-dialog::backdrop,
#job-summary-dialog::backdrop {
background: rgb(20 18 40 / 45%);
}

Expand Down
2 changes: 1 addition & 1 deletion dashboard/repository-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:p
import { canonicalExecutionRepository } from "./executor-lease.js";

export const SETUP_PROFILE_PATH = join(".github", "fixlab", "repository-profile.json");
export const REPOSITORY_PROMPT_NAMES = ["bugfix", "intake", "diagnose", "reproduce", "fix", "validate", "live-test", "pr"]
export const REPOSITORY_PROMPT_NAMES = ["bugfix", "intake", "diagnose", "reproduce", "fix", "validate", "live-test", "pr", "summary"]
.map((name) => `fixlab.${name}.prompt.md`);

export function localRepository(value) {
Expand Down
24 changes: 24 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,35 @@ overwriting repository-owned versions.
| `/fixlab.validate` | Run focused tests, lint, type-check, and builds | Every required check passes or is explicitly skipped with risk |
| `/fixlab.live-test` | Start owned applications and run the browser journey | The running application proves the expected behavior |
| `/fixlab.pr` | Prepare the evidence-backed pull-request outcome | The PR contains the exact change, evidence, and remaining risks |
| `/fixlab.summary` | Optionally summarize an existing job in the conversation | Actual outcomes, regression gaps, findings, workarounds and must-know items are explicit; no execution or file changes |

The commands read `.github/fixlab/repository-profile.json` and repository
instructions before acting. They never replace repository-owned commands,
environment controls, or approval requirements.

For an optional engineering handoff, run `/fixlab.summary` in the VS Code
conversation containing the job evidence. It is never added to the automatic
bug-fix lifecycle. The prompt returns Markdown without rerunning checks,
changing job status, creating files or publishing changes. Active-job summaries
are interim; unavailable results are marked "Not verified". It distinguishes
runner-confirmed results from agent claims, local from deployed validation, and
known findings from suspected side effects. A folder or ZIP is a separate request.

Existing repositories can rerun `fixlab init` after updating FixLab to add the
missing prompt without replacing their profile, agents or customized prompts.
Other conversation hosts can paste the packaged
[`fixlab.summary.prompt.md`](../prompts/fixlab.summary.prompt.md) into the
same evidence-bearing conversation; this is a prompt file, not a new CLI
subcommand or a dashboard export button.

In the dashboard, select a current or saved job and click **View summary** in
the header. This separate read-only view assembles recorded investigation,
changes, gate results, scenario claims, findings and remaining gaps without an
AI call or job mutation. It refreshes that same job's evidence while open.
Missing workarounds, commit inventory and side-effect findings stay unverified;
it does not infer them from a passed status or parse raw logs. For an AI-written
handoff with richer context, use the optional prompt in its original conversation.

## Executor-backed dashboard checks

`fixlab gate --port <port> --job <id> --gate <id> --repository <worktree>`
Expand Down
69 changes: 69 additions & 0 deletions e2e/dashboard-readiness.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2347,6 +2347,75 @@ function pendingQuestionJob(question) {
return job;
}

test("job summary shows recorded findings and gaps without execution, and refreshes the same job", async ({ page }) => {
const posts = [];
page.on("request", (request) => { if (request.method() === "POST") { posts.push(request.url()); } });
const job = {
id: "summary-current", request: "Investigate a synthetic regression", requestType: "bug-fix",
mode: "fix-and-validate", status: "running", targetEnvironment: "dev",
stages: {
diagnosis: { status: "passed", message: "Confirmed route mismatch" },
fix: { status: "pending", message: "Correction not applied" },
"live-test": { status: "blocked", message: "Authentication unavailable" }
},
validation: { scope: "all", gates: [{ id: "regression-tests", status: "failed", required: true, durationMs: 25 }] },
testEvidence: { reported: true, scenario: "Check role visibility", source: "synthetic-intercepted", mutationMode: "intercepted" },
bugs: [{ id: "bug-1", outcome: "unresolved", owner: "application", summary: "Role selection remains incorrect" }],
logs: [{ stream: "stdout", message: "Raw log sentinel must not enter summary" }], activity: []
};
await mockChatWorkspace(page, { job });
await page.goto(baseUrl);
await page.getByRole("button", { name: "View summary", exact: true }).click();
const summary = page.locator("#job-summary-content");
await expect(summary).toContainText("Interim outcome");
await expect(summary).toContainText("Confirmed route mismatch");
await expect(summary).toContainText("regression-tests: failed (required)");
await expect(summary).toContainText("Role selection remains incorrect");
await expect(summary).toContainText("Synthetic or intercepted tests do not prove real backend writes");
await expect(summary).toContainText("Validated workaround, tradeoffs and fix-induced side effects: Not verified");
await expect(summary).not.toContainText("Raw log sentinel");
job.status = "passed";
job.validation.gates[0].status = "stale";
await expect(summary).toContainText("Recorded outcome");
await expect(summary).toContainText("regression-tests: stale (required)");
await expect(summary).toContainText("Job completion is not a claim of complete regression coverage");
await page.getByRole("button", { name: "Close summary" }).click();
await expect(page.locator("#job-summary-dialog")).toBeHidden();
await expect(page.getByRole("button", { name: "View summary", exact: true })).toBeFocused();
expect(posts).toEqual([]);
});

test("job summary follows historical selection, handles missing evidence and renders text safely", async ({ page }) => {
const posts = [];
page.on("request", (request) => { if (request.method() === "POST") { posts.push(request.url()); } });
const job = { id: "summary-active", request: "Active summary sentinel", requestType: "bug-fix",
mode: "fix-and-validate", status: "running", stages: {}, bugs: [], logs: [], activity: [] };
const saved = { ...job, id: "summary-history", status: "held",
request: 'Saved <img src=x onerror="window.summaryInjection=true"> https://private.example.test/path',
stages: { review: { status: "blocked", message: "authorization=private-sentinel" } } };
await mockChatWorkspace(page, { job, history: [saved] });
await page.goto(baseUrl);
await page.locator('.workspace-navigation [data-open="memory-group"]').click();
await page.locator("#repository-memory-list button").click();
await page.setViewportSize({ width: 390, height: 844 });
await page.getByRole("button", { name: "View summary", exact: true }).click();
const summary = page.locator("#job-summary-content");
await expect(summary).toContainText(saved.id);
await expect(summary).not.toContainText(job.request);
await expect(summary).toContainText("No executor-backed gate results recorded");
await expect(summary).toContainText("[Sensitive detail omitted]");
await expect(summary).not.toContainText("private.example.test");
await expect(summary).not.toContainText("private-sentinel");
await expect(summary.locator("img")).toHaveCount(0);
expect(await page.evaluate(() => window.summaryInjection)).toBeUndefined();
const overflow = await page.locator("#job-summary-dialog").evaluate((element) =>
element.scrollWidth > element.clientWidth);
expect(overflow).toBe(false);
await page.keyboard.press("Escape");
await expect(page.locator("#job-summary-dialog")).toBeHidden();
expect(posts).toEqual([]);
});

test("current-job full screen ignores historical selection, follows the live job and restores the prior view", async ({ page }) => {
const posts = [];
let historicalEvidence;
Expand Down
47 changes: 47 additions & 0 deletions prompts/fixlab.summary.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
mode: agent
description: Optionally summarize an existing FixLab job without running checks or changing files.
---

Generate an engineering handoff for the FixLab job in this conversation using
existing session evidence only. This is an optional summary, not a new fix or
validation job. If the job is ambiguous or its evidence is unavailable, ask for
the job identifier and relevant sanitized results; do not infer them from a
different job.

Do not rerun tests, install tools, start applications, access live systems,
change files, commit, publish, or alter job status. Do not invoke the bug-fix
workflow. Return Markdown in the conversation only; a folder or ZIP requires
a separate explicit request and normal permissions.

Include these sections, marking missing evidence "Not verified":

1. Outcome: original bug, expected versus actual behavior, job identifier,
repository/commit and PR references when safe, and whether the result is
fixed and verified, fixed but unverified, workaround-only, external, or
unresolved. For an active job, label the summary interim.
2. Diagnosis and changes: evidence-backed root cause, affected components,
changed files and the rationale for the correction. Explain no-change
outcomes honestly.
3. Test and regression report: a table of scenarios/assertions, exact safe
commands and working directories, recorded outcomes/durations, and evidence
references. Separate the original failing reproduction from subsequent
verification, focused tests from broad regression, and runner-confirmed
results from agent-reported claims. Preserve failed, skipped, timed-out,
blocked, pending, stale, not-run and deferred checks; never mark them passed.
4. Findings and must-know: verified tradeoffs, compatibility concerns,
maintenance traps and prerequisites. Separate pre-existing defects from
confirmed fix-induced regressions; label suspected side effects as
hypotheses, not findings.
5. Workarounds: only recorded workarounds, their validation, limitations,
conditions for removal, and known ownership. Do not invent one.
6. Remaining risks and next actions: untested scenarios, missing data/access,
evidence gaps and concrete follow-up. A complete summary does not imply
complete regression coverage.

Distinguish local changed-code validation from deployed-system checks,
synthetic/intercepted tests from real integration, and draft publication from
ready-for-review or merge. Reference existing reviewed evidence instead of
pasting raw logs or transcripts. Exclude credentials, authorization headers,
cookies, browser state, private endpoints, personal/customer data, unreviewed
screenshots and model-cost/token metrics. Omit or redact sensitive references.
6 changes: 5 additions & 1 deletion test/dashboard-repository-settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test("repository onboarding previews without writes and creates only missing rev
assert.equal(localRepository(repository), repository);
const preview = plan(repository);
assert.equal(existsSync(join(repository, ".github")), false);
assert.equal(preview.files.length, 10);
assert.equal(preview.files.length, 11);
const applied = applyRepositorySettings(preview, "synthetic-settings-owner");
assert.equal(applied.saved, true);
const profile = JSON.parse(readFileSync(join(repository, SETUP_PROFILE_PATH), "utf8"));
Expand All @@ -46,6 +46,10 @@ test("repository onboarding previews without writes and creates only missing rev
assert.equal(configuredTestSynthesis(profile, "", undefined, "sit").source, "synthetic-intercepted");
assert.equal(validateLiveTestProfile(profile).ok, true);
assert.equal(existsSync(join(repository, ".github", "agents", "fixlab-autofix.agent.md")), true);
assert.equal(
readFileSync(join(repository, ".github", "prompts", "fixlab.summary.prompt.md"), "utf8"),
readFileSync(join(packageRoot, "prompts", "fixlab.summary.prompt.md"), "utf8")
);
assert.equal(existsSync(canonicalExecutionRepository(repository).directory), false);
assert.equal(plan(repository).assets.length, 0);
} finally {
Expand Down
Loading
Loading