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
26 changes: 22 additions & 4 deletions dashboard/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const stageNames = [
];

function summaryText(value) {
const result = String(value ?? "").replace(/\s+/gu, " ").trim().slice(0, 1000);
const result = String(value ?? "").replace(/\s+/gu, " ").trim().slice(0, 2000);
if (/(?:password|passwd|secret|token|authorization|bearer|cookie|connection.?string|private.?key)/iu.test(result)) {
return "[Sensitive detail omitted]";
}
Expand All @@ -23,6 +23,7 @@ function jobSummarySections(job) {
const gates = job.validation?.gates ?? [];
const unresolved = stageNames.filter((name) => job.stages?.[name]?.status !== "passed");
const evidence = job.testEvidence;
const detail = job.engineeringSummary;
const interim = ["running", "queued", "holding"].includes(job.status);
const stage = (name) => {
const recorded = job.stages?.[name];
Expand All @@ -32,14 +33,27 @@ function jobSummarySections(job) {
{ heading: interim ? "Interim outcome" : "Recorded outcome", lines: [
`Job: ${summaryText(job.id)} | Status: ${summaryText(job.status)}`,
`Request: ${summaryText(job.request)}`,
...(detail ? [
`Bug: ${summaryText(detail.problem)}`,
`Expected behavior: ${summaryText(detail.expectedBehavior)}`,
`Actual behavior: ${summaryText(detail.actualBehavior)}`
] : []),
`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: [
{ heading: detail ? "Bug-to-code-fix explanation" : "Investigation and changes (agent-reported)", lines: [
...(detail ? [
`Explanation source: ${summaryText(job.engineeringSummaryOrigin)}`,
`Root cause: ${summaryText(detail.rootCause)}`,
`Code fix: ${summaryText(detail.codeFix)}`,
...(detail.changedFiles ?? []).map((file) => `Changed file: ${summaryText(file)}`),
`Commit reference: ${summaryText(detail.commit)}`
] : ["Detailed code-change explanation was not retained for this job. Stage messages below are not a substitute for that evidence."]),
stage("intake"), stage("diagnosis"), stage("reproduce"), stage("fix"), stage("review"),
"Exact changed-file and commit inventory: Not verified by this summary."
"Code references above are recorded explanations, not newly executed verification."
] },
{ heading: "Tests and regression", lines: [
...(detail?.regression ?? []).map((item) => `Recorded regression detail: ${summaryText(item)}`),
...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.`,
Expand All @@ -56,7 +70,11 @@ function jobSummarySections(job) {
"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.",
...(detail?.workarounds ?? []).map((item) => `Workaround: ${summaryText(item)}`),
...(detail?.risks ?? []).map((item) => `Risk / must-know: ${summaryText(item)}`),
...((detail?.workarounds?.length || detail?.risks?.length) ? [] : [
"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: [
Expand Down
152 changes: 148 additions & 4 deletions dashboard/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { createSessionRegistry } from "./sessions.js";
import { inspectBrowserProfileOwnership } from "./browser-ownership.js";
import { canonicalExecutionRepository, createExecutorLease } from "./executor-lease.js";
import { spawnWindowsExecutor } from "./windows-executor.js";
import { createRuntimeRequestBridge, MAX_RUNTIME_FRAME_BYTES, validateRuntimeUsage, validateToolApprovalMode } from "./runtime-requests.js";
import { createRuntimeRequestBridge, MAX_RUNTIME_FRAME_BYTES, safeRuntimeText, validateRuntimeUsage, validateToolApprovalMode } from "./runtime-requests.js";
import { validateJobFileAccess } from "./runtime-permission-policy.js";
import { configuredWorkflow, efficientWorkflowGuidance, allowsDraftIntegrationDeferral, allowsUnverifiedDraft, UNVERIFIED_DRAFT_GUIDANCE, OPTIONAL_INTEGRATION_GUIDANCE, AUTONOMOUS_RECOVERY_GUIDANCE, DRAFT_INTEGRATION_GUIDANCE, COPILOT_EXECUTION_GUIDANCE, RUNTIME_RECOVERY_GUIDANCE, READ_ONLY_POST_GUIDANCE, EXISTING_APPLICATION_GUIDANCE, API_ONLY_PLAYWRIGHT_GUIDANCE, UNIT_TEST_ONLY_GUIDANCE, TRANSIENT_PLAYWRIGHT_GUIDANCE, CONCISE_PR_GUIDANCE, PR_LEARNING_GUIDANCE } from "./workflow-policy.js";
import { componentRetrievalGuidance } from "./affected-surface.js";
Expand Down Expand Up @@ -585,6 +585,8 @@ function publicQueue(jobs) {
position: index + 1,
request: safeSummary(job.request),
requestType: job.requestType,
engineeringSummary: job.engineeringSummary ?? null,
engineeringSummaryOrigin: job.engineeringSummaryOrigin ?? null,
model: job.model,
runtime: job.runtime,
pullRequestStrategy: job.pullRequestStrategy,
Expand Down Expand Up @@ -646,6 +648,51 @@ function safePersistedSummary(value) {
return safeSummary(value).replace(/https?:\/\/\S+/giu, "[omitted URL]");
}

export function normalizeEngineeringSummary(value) {
const strings = ["problem", "expectedBehavior", "actualBehavior", "rootCause", "codeFix", "commit"];
const lists = ["changedFiles", "regression", "risks", "workarounds"];
if (!value || typeof value !== "object" || Array.isArray(value) ||
Object.keys(value).some((key) => ![...strings, ...lists].includes(key))) {
throw new Error("Engineering summary must contain only the documented bug, fix and evidence fields.");
}
const result = {};
const sanitize = (field, limit) => safeRuntimeText(field, limit)
? safeSummary(field, limit).replace(/https?:\/\/\S+/giu, "[omitted URL]")
: "[Sensitive detail omitted]";
for (const key of strings) {
const field = value[key] ?? "";
if (typeof field !== "string" || field.length > 2000 ||
(["problem", "rootCause", "codeFix"].includes(key) && !field.trim())) {
throw new Error(`Engineering summary ${key} must be a bounded string; problem, rootCause and codeFix are required.`);
}
if (key === "commit" && field && !/^[a-f0-9]{7,64}$/u.test(field)) {
throw new Error("Engineering summary commit must be a hexadecimal commit reference.");
}
result[key] = sanitize(field, 2000);
}
for (const key of lists) {
const fields = value[key] ?? [];
if (!Array.isArray(fields) || fields.length > 20 ||
fields.some((field) => typeof field !== "string" || !field.trim() || field.length > 500)) {
throw new Error(`Engineering summary ${key} must contain at most 20 bounded strings.`);
}
if (key === "changedFiles" && fields.some((field) =>
/^[\\/]/u.test(field.trim()) || /[:*?"\0\r\n]/u.test(field) ||
field.trim().replace(/\\/gu, "/").split("/").some((part) =>
!part.trim() || part.trim() === "." || part.trim() === ".."))) {
throw new Error("Engineering summary files must be relative paths without traversal.");
}
result[key] = fields.map((field) => sanitize(field, 500));
}
if (Buffer.byteLength(JSON.stringify(result)) > 16384) {
throw new Error("Engineering summary exceeds 16 KiB.");
}
return result;
}

const ENGINEERING_SUMMARY_GUIDANCE =
'- At the reviewed fix checkpoint and before finishing, emit FIXLAB_SUMMARY| followed by one-line JSON: problem, expectedBehavior, actualBehavior, rootCause, codeFix (explain the actual change and why), changedFiles[], regression[], risks[], workarounds[], commit (hex or empty). Required: problem, rootCause, codeFix. Use relative file paths, bounded strings, no secrets/private URLs. Preserve the original bug-to-fix explanation across retries; update only for new evidence. Separate historical, skipped and unverified tests from actual passes; report no-change outcomes honestly.';

function dashboardSnapshot(job) {
return {
id: job.id,
Expand Down Expand Up @@ -735,6 +782,8 @@ function dashboardSnapshot(job) {
owner: safePersistedSummary(bug.owner),
summary: safePersistedSummary(bug.summary)
})),
engineeringSummary: job.engineeringSummary ?? null,
engineeringSummaryOrigin: job.engineeringSummaryOrigin ?? null,
Comment thread
MonaDevAI marked this conversation as resolved.
Resolved
canResume: false,
canComment: false
};
Expand Down Expand Up @@ -1347,6 +1396,7 @@ ${videoGuidance}
${manualLocalhostGuidance}
${readOnly ? "" : branchNamingGuidance}
- ${readOnly ? "Do not edit files other than job-owned transient Playwright journeys and runner evidence. Do not create commits, push branches, create pull requests, or update pull requests." : "Make the smallest complete change that resolves the request. Make no code change when the evidence shows none is required."}
${readOnly ? "" : ENGINEERING_SUMMARY_GUIDANCE}
${readOnly ? TRANSIENT_PLAYWRIGHT_GUIDANCE : ""}
- Autonomously complete the lifecycle without asking the user to direct routine engineering steps.
${readOnly ? "" : `- Saved delivery workflow: ${unverifiedDraft ? "explicit unverified draft; local and live checks are deferred, not passed" : efficient ? `efficient; focused checks before the ${workflow.publication === "ready" ? "ready-for-review" : "draft"} PR, broader checks only by saved preference or explicit selection` : "strict; all applicable configured gates remain required for full validation and ready publication"}. This recorded job policy takes precedence over the product default.`}
Expand Down Expand Up @@ -1436,8 +1486,8 @@ function runGit(repository, args) {
return result.status === 0 ? result.stdout.trim() : null;
}

function safeSummary(value) {
const text = String(value ?? "").replace(/\s+/g, " ").trim().slice(0, 300);
function safeSummary(value, maximumLength = 300) {
const text = String(value ?? "").replace(/\s+/g, " ").trim().slice(0, maximumLength);
if (
/(?:password|passwd|secret|token|authorization|bearer|cookie|connection.?string|private.?key|client.?secret|dashboard[-\\/]+artifacts)/i.test(
text
Expand Down Expand Up @@ -2331,6 +2381,8 @@ function publicJob(job) {
id: job.id,
request: job.request,
requestType: job.requestType,
engineeringSummary: job.engineeringSummary ?? null,
engineeringSummaryOrigin: job.engineeringSummaryOrigin ?? null,
Comment thread
MonaDevAI marked this conversation as resolved.
Resolved
deferValidation: job.deferValidation === true,
skipLiveTests: job.skipLiveTests === true,
useDeployedBackend: job.useDeployedBackend === true,
Expand Down Expand Up @@ -2619,15 +2671,65 @@ function appendOutput(job, stream, text) {
}

function splitRenderedMarkers(line) {
const summaryIndex = line.indexOf("FIXLAB_SUMMARY|");
if (summaryIndex > 0 && /^FIXLAB_(?:STAGE|ACTIVITY|BUG|TEST)(?:\||\s)/u.test(line)) {
return [...splitRenderedMarkers(line.slice(0, summaryIndex)), ...splitRenderedMarkers(line.slice(summaryIndex))];
}
if (summaryIndex === 0) {
if (!line.slice("FIXLAB_SUMMARY|".length).trimStart().startsWith("{")) {
return [line];
}
let depth = 0;
let quoted = false;
let escaped = false;
for (let index = "FIXLAB_SUMMARY|".length; index < line.length; index += 1) {
const character = line[index];
if (quoted) {
if (escaped) {escaped = false;}
else if (character === "\\") {escaped = true;}
else if (character === '"') {quoted = false;}
} else if (character === '"') {quoted = true;}
else if (character === "{") {depth += 1;}
else if (character === "}") {
depth -= 1;
if (depth === 0) {
try {
JSON.parse(line.slice("FIXLAB_SUMMARY|".length, index + 1));
} catch {
return [line];
}
const remainder = line.slice(index + 1).trim();
if (remainder && !/^FIXLAB_(?:STAGE|ACTIVITY|BUG|TEST|SUMMARY)(?:\||\s)/u.test(remainder)) {
return [line];
}
return [line.slice(0, index + 1), ...(remainder ? splitRenderedMarkers(remainder) : [])];
}
}
}
return [line];
}
if (!/^FIXLAB_(?:STAGE|ACTIVITY|BUG|TEST)(?:\||\s)/u.test(line)) {
return [line];
}
return line
.split(/(?=FIXLAB_(?:STAGE|ACTIVITY|BUG|TEST)(?:\||\s))/u)
.split(/(?=FIXLAB_(?:STAGE|ACTIVITY|BUG|TEST|SUMMARY)(?:\||\s))/u)
.filter(Boolean);
}

function appendLine(job, stream, line) {
if (line.startsWith("FIXLAB_SUMMARY|")) {
try {
if (job.permissionRequest || Buffer.byteLength(line) > 16384) {
throw new Error("Summary is blocked by pending permission review or exceeds 16 KiB.");
}
job.engineeringSummary = normalizeEngineeringSummary(JSON.parse(line.slice("FIXLAB_SUMMARY|".length)));
job.engineeringSummaryOrigin = "agent-reported";
} catch (error) {
pushLog(job, { index: job.nextLogIndex, timestamp: new Date().toISOString(),
stream: "dashboard", message: `Engineering summary rejected: ${error instanceof SyntaxError ? "Invalid JSON." : error.message}` });
}
return;
}
const usage = parseUsageLine(line);
if (usage) {
if (job.runtime !== "copilot") {
Expand Down Expand Up @@ -3025,8 +3127,12 @@ User input:
${details}
${manualResult}
${transientGuidance}
${job.mode === "fix-and-validate" && job.engineeringSummary
? `Retained bug-to-code-fix explanation (recorded, not new verification):\n${JSON.stringify(job.engineeringSummary)}`
: ""}

Resume requirements:
${job.mode === "fix-and-validate" ? ENGINEERING_SUMMARY_GUIDANCE : ""}
- Reuse the completed diagnosis, current worktree, validation evidence, branch, and pull request from this session.
- Re-read repository instructions and the effective diff only when the new input changes them.
- Continue from the blocked or failed stage, or treat this as a focused addition to the completed job.
Expand Down Expand Up @@ -3844,6 +3950,8 @@ export function createDashboardServer({
workspaceStash: null,
validationBaseline: null,
validationHistory: { complete: true, gates: [] },
engineeringSummary: null,
engineeringSummaryOrigin: null,
finishedAt: null,
lastActivityAt: status === "running" ? createdAt : null,
error: null,
Expand Down Expand Up @@ -4850,6 +4958,42 @@ export function createDashboardServer({
return;
}

if (request.method === "POST" && requestUrl.pathname === "/api/job/engineering-summary") {
try {
const expectedHost = `${DASHBOARD_HOST}:${server.address().port}`;
if (request.headers.host !== expectedHost || request.headers.origin ||
!request.headers["content-type"]?.startsWith("application/json")) {
throw new Error("Engineering notes require a local JSON client without a browser Origin.");
}
const body = await readJsonBody(request, 16384);
if (body.instanceId !== sessions.id || body.repository !== resolvedRepository ||
body.confirmed !== true || typeof body.jobId !== "string") {
throw new Error("Engineering notes require the exact instance, repository, job and explicit confirmation.");
}
if (!dashboardHistoryPath) {
throw new Error("Engineering notes require persistable repository history.");
}
const job = [currentJob, ...heldJobs, ...completedJobs, ...persistedJobs]
.find((candidate) => candidate?.id === body.jobId);
if (closing || !job || job.executorStopping || (job === currentJob && activeHandle) ||
!["passed", "failed", "blocked", "held", "cancelled"].includes(job.status)) {
throw new Error("Engineering notes require an existing stopped job with a settled executor.");
}
const summary = normalizeEngineeringSummary(body.summary);
job.engineeringSummary = summary;
job.engineeringSummaryOrigin = "reviewed-note";
persistDashboardState();
if (dashboardHistoryWarning || !persistedJobs.some((snapshot) => snapshot.id === job.id)) {
sendJson(response, 503, { error: "Engineering notes are retained in memory but could not be persisted." });
return;
}
sendJson(response, 200, { jobId: job.id, engineeringSummary: summary, origin: "reviewed-note" });
} catch (error) {
sendJson(response, 400, { error: error.message });
}
return;
}

if (request.method === "POST" && requestUrl.pathname === "/api/job/publication-context") {
try {
if (request.headers.host !== `${DASHBOARD_HOST}:${server.address().port}` ||
Expand Down
16 changes: 16 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ Validation-only prompts retain authentication, permissions, data-safety and
evidence requirements without instructions to prepare or publish code changes.
Focused tests bound the generated prompt sizes and check those retained contracts.

Code jobs emit a bounded `FIXLAB_SUMMARY|<one-line JSON>` engineering explanation
at their reviewed fix checkpoint and completion. Required fields are `problem`,
`rootCause` and `codeFix`; optional fields capture expected/actual behavior,
relative changed files, a commit reference, regression details, risks and
workarounds. The dashboard sanitizes and preserves this record independently of
short stage messages through retries, history and restart. Invalid/oversized
records are explicitly rejected; neither records nor reviewed notes can mark
tests passed or grant permissions.

For an older stopped job, an explicitly confirmed local JSON client can submit
reviewed notes to `/api/job/engineering-summary`, binding the exact instance,
repository and job. It rejects browser Origins, active/unknown jobs and unsafe
paths. Notes are labeled `reviewed-note`, not runner proof; agent records are
labeled `agent-reported`. Missing details are not invented from current source
or a different job. The summary view does not execute tests or fetch source.

FixLab is distributed and versioned as its own product. It supports Agency
Copilot as the default compatibility runtime and GitHub Copilot CLI as an
optional direct runtime. Both provide agent execution, authentication, plugin
Expand Down
10 changes: 8 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,14 @@ 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
Detailed engineering records show the original bug, expected/actual behavior,
root cause, actual code correction, changed files, commit, regression details
and risks. They survive retries and restart instead of being replaced by a
"retained fix" stage message. Older jobs without a record explicitly identify
the missing explanation; reviewed notes can be imported by a confirmed local
client bound to that stopped job. Neither agent explanations nor reviewed notes
are new runner proof. Missing facts stay unverified; the view 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
Expand Down
Loading
Loading