feat: render work candidate coding plans

This commit is contained in:
Fer Frau Roca 2026-05-04 00:26:05 +02:00 committed by Peter Steinberger
parent 0fefca282d
commit 369c634b3c
No known key found for this signature in database
4 changed files with 297 additions and 13 deletions

View File

@ -16,6 +16,8 @@ checkpoint, and status-only commits are intentionally omitted.
checked-in source of truth.
- Replaced per-lane capacity config with a single `workers.max` budget and
dynamic background lane scheduling.
- Added generated coding-plan artifacts for fresh `queue_fix_pr` work candidates
and linked them from the dashboard work-candidate tables.
- Added a generated 1200x630 social preview card plus large-image Open Graph and
Twitter metadata for the docs site.

View File

@ -16,7 +16,16 @@ Reports store the lane fields in frontmatter:
- `work_cluster_refs`, `work_validation`, and `work_likely_files`
The dashboard shows fresh `queue_fix_pr` reports whose `work_status` is
`candidate`. This is a manual promotion queue for the repair lane.
`candidate`. This is a manual promotion queue for the repair lane. For each
fresh candidate, apply/reconcile also generates
`records/<repo-slug>/plans/<number>.md` from the existing report fields. The
dashboard links both the source report and the generated coding plan so
maintainers can promote from a concise implementation view without editing the
durable report.
Plan artifacts are generated state. They are removed when the item closes,
archives, becomes stale, or is reclassified away from `queue_fix_pr`; regenerate
them from the source report instead of editing them by hand.
## Reproducible Bug Auto-Implementation

View File

@ -375,6 +375,7 @@ interface DashboardItem {
action: string;
reviewStatus: string;
reportPath: string;
planPath?: string | undefined;
workCandidate: string;
workPriority: string;
workStatus: string;
@ -911,6 +912,10 @@ function defaultClosedDir(profile = targetProfile()): string {
return join(repoRecordsDir(profile), "closed");
}
function defaultPlansDir(profile = targetProfile()): string {
return join(repoRecordsDir(profile), "plans");
}
function reportFileName(repo: string, number: number): string {
repositoryProfileFor(repo);
return `${number}.md`;
@ -4715,6 +4720,111 @@ function reportDecision(markdown: string, closeReason: CloseReason): Decision {
};
}
function workPlanPathForReport(file: string, plansDir = defaultPlansDir()): string {
return join(plansDir, basename(file));
}
function shouldRenderWorkPlanFromReport(markdown: string): boolean {
return (
frontMatterValue(markdown, "decision") === "keep_open" &&
frontMatterValue(markdown, "action_taken") === "kept_open" &&
frontMatterValue(markdown, "work_candidate") === "queue_fix_pr" &&
frontMatterValue(markdown, "work_status") === "candidate" &&
isFresh({
reviewedAt: frontMatterValue(markdown, "reviewed_at"),
reviewStatus: effectiveReviewStatus(markdown),
})
);
}
function formattedMarkdownList(
values: readonly string[],
formatter: (value: string) => string,
): string {
return values.length ? values.map((value) => `- ${formatter(value)}`).join("\n") : "- none";
}
function inlineCode(value: string): string {
return `\`${value.replaceAll("`", "\\`")}\``;
}
export function renderWorkPlanFromReport(
markdown: string,
options: { reportPath?: string } = {},
): string | null {
if (!shouldRenderWorkPlanFromReport(markdown)) return null;
const repo = markdownRepository(markdown);
const number = frontMatterValue(markdown, "number") ?? "unknown";
const title = frontMatterValue(markdown, "title") ?? "Untitled";
const reviewedAt = frontMatterValue(markdown, "reviewed_at") ?? "unknown";
const workPrompt = reviewSectionValue(markdown, "repairWorkPrompt").trim();
const likelyFiles = frontMatterStringArray(markdown, "work_likely_files");
const validation = frontMatterStringArray(markdown, "work_validation");
const clusterRefs = frontMatterStringArray(markdown, "work_cluster_refs");
const reportPath = options.reportPath ?? "unknown";
return `---
number: ${number}
repository: ${repo}
title: ${JSON.stringify(title)}
source_report: ${reportPath}
reviewed_at: ${reviewedAt}
work_candidate: ${frontMatterValue(markdown, "work_candidate") ?? "none"}
work_priority: ${frontMatterValue(markdown, "work_priority") ?? "low"}
work_confidence: ${frontMatterValue(markdown, "work_confidence") ?? "low"}
---
# Coding Plan for ${repo}#${number}: ${title}
Source report: ${reportPath === "unknown" ? "unknown" : markdownLink(reportPath, reportPath)}
## Summary
${reviewSectionValue(markdown, "summary") || "No summary provided."}
## Plan
${workPrompt || "No repair work prompt provided."}
## Likely Files
${formattedMarkdownList(likelyFiles, inlineCode)}
## Validation
${formattedMarkdownList(validation, inlineCode)}
## Cluster References
${formattedMarkdownList(clusterRefs, (value) => value)}
## Notes
- This file is generated dashboard state from the durable review report.
- Regenerate it from the source report instead of editing it by hand.
`;
}
function syncWorkPlanFromReport(options: {
markdown: string;
reportPath: string;
plansDir: string;
dryRun?: boolean;
}): boolean {
const planPath = workPlanPathForReport(options.reportPath, options.plansDir);
const plan = renderWorkPlanFromReport(options.markdown, {
reportPath: repoRelativePath(options.reportPath),
});
if (!plan) {
if (!options.dryRun && existsSync(planPath)) unlinkSync(planPath);
return false;
}
if (!options.dryRun) {
ensureDir(dirname(planPath));
writeFileSync(planPath, plan, "utf8");
}
return true;
}
function runtimeReviewText(runtime?: {
model?: string | undefined;
reasoningEffort?: string | undefined;
@ -6229,6 +6339,11 @@ function applyDecisionsCommand(args: Args): void {
if (dryRun) return;
ensureDir(closedDir);
writeFileSync(path, nextMarkdown, "utf8");
syncWorkPlanFromReport({
markdown: nextMarkdown,
reportPath: path,
plansDir: defaultPlansDir(),
});
renameSync(path, join(closedDir, file));
};
const markApplySkipped = (actionTaken: ActionTaken, reason: string): boolean => {
@ -6567,6 +6682,7 @@ function applyArtifactsCommand(args: Args): void {
const artifactDir = resolve(stringArg(args.artifact_dir, "artifacts"));
const itemsDir = resolve(stringArg(args.items_dir, defaultItemsDir()));
const closedDir = resolve(stringArg(args.closed_dir, defaultClosedDir()));
const plansDir = resolve(stringArg(args.plans_dir, defaultPlansDir()));
const skipReconcile = boolArg(args.skip_reconcile);
const replayClosedArtifacts = boolArg(args.replay_closed_artifacts);
const maxPages = numberArg(args.max_pages, 250);
@ -6600,14 +6716,21 @@ function applyArtifactsCommand(args: Args): void {
const destinationDir = destination === "closed" ? closedDir : itemsDir;
const stalePath = join(destinationDir === itemsDir ? closedDir : itemsDir, destinationFile);
if (existsSync(stalePath)) unlinkSync(stalePath);
writeFileSync(join(destinationDir, destinationFile), markdown, "utf8");
const reportPath = join(destinationDir, destinationFile);
writeFileSync(reportPath, markdown, "utf8");
if (destination === "closed") {
const planPath = workPlanPathForReport(reportPath, plansDir);
if (existsSync(planPath)) unlinkSync(planPath);
} else {
syncWorkPlanFromReport({ markdown, reportPath, plansDir });
}
appliedArtifacts += 1;
}
}
console.error(
`[apply-artifacts] applied=${appliedArtifacts} skipped_closed=${skippedClosedArtifacts}`,
);
if (!skipReconcile) reconcileFolders({ itemsDir, closedDir });
if (!skipReconcile) reconcileFolders({ itemsDir, closedDir, plansDir });
}
function artifactTargetIsOpen(number: number, openNumbers: Set<number> | null): boolean {
@ -7010,6 +7133,7 @@ function moveMarkdownFile(options: {
function reconcileFolders(options: {
itemsDir: string;
closedDir: string;
plansDir?: string;
maxPages?: number;
dryRun?: boolean;
fetchClosedAt?: boolean;
@ -7017,6 +7141,7 @@ function reconcileFolders(options: {
const maxPages = options.maxPages ?? 250;
const dryRun = options.dryRun ?? false;
const fetchClosedAt = options.fetchClosedAt ?? true;
const plansDir = options.plansDir ?? defaultPlansDir();
ensureDir(options.itemsDir);
ensureDir(options.closedDir);
const { numbers: openNumbers, pagesScanned } = fetchOpenItemNumbers(maxPages);
@ -7048,6 +7173,10 @@ function reconcileFolders(options: {
}
const markdown = markReconciledState(sourceMarkdown, "closed", { closedAt });
moveMarkdownFile({ sourcePath, destinationPath, markdown, dryRun });
if (!dryRun) {
const planPath = workPlanPathForReport(sourcePath, plansDir);
if (existsSync(planPath)) unlinkSync(planPath);
}
movedToClosed += 1;
}
@ -7065,6 +7194,7 @@ function reconcileFolders(options: {
}
const markdown = markReconciledState(sourceMarkdown, "open");
moveMarkdownFile({ sourcePath, destinationPath, markdown, dryRun });
syncWorkPlanFromReport({ markdown, reportPath: destinationPath, plansDir, dryRun });
movedToItems += 1;
}
@ -7082,10 +7212,18 @@ function reconcileCommand(args: Args): void {
repoFromArgs(args);
const itemsDir = resolve(stringArg(args.items_dir, defaultItemsDir()));
const closedDir = resolve(stringArg(args.closed_dir, defaultClosedDir()));
const plansDir = resolve(stringArg(args.plans_dir, defaultPlansDir()));
const maxPages = numberArg(args.max_pages, 250);
const dryRun = boolArg(args.dry_run);
const fetchClosedAt = !boolArg(args.skip_closed_at);
const result = reconcileFolders({ itemsDir, closedDir, maxPages, dryRun, fetchClosedAt });
const result = reconcileFolders({
itemsDir,
closedDir,
plansDir,
maxPages,
dryRun,
fetchClosedAt,
});
console.log(JSON.stringify(result, null, 2));
}
@ -7145,6 +7283,7 @@ function dashboardStats(
): DashboardStats {
const files = markdownFiles(itemsDir);
const closedFiles = markdownFiles(closedDir);
const plansDir = defaultPlansDir(profile);
const now = Date.now();
let fresh = 0;
let proposedClose = 0;
@ -7215,6 +7354,9 @@ function dashboardStats(
action,
reviewStatus,
reportPath: repoRelativePath(join(itemsDir, file)),
planPath: existsSync(join(plansDir, file))
? repoRelativePath(join(plansDir, file))
: undefined,
workCandidate,
workPriority,
workStatus,
@ -7423,9 +7565,12 @@ function formatWorkQueueRows(items: readonly DashboardItem[], limit = 10): strin
const repo = item.repo ?? targetRepo();
const title = markdownTableCell(displayTitle(item.title));
const report = markdownLink(item.reportPath, reportFileUrl(item.number, item.reportPath));
return `| ${markdownLink(`#${item.number}`, itemUrlFor(repo, item.number, item.kind))} | ${title} | ${item.workPriority} | ${item.workStatus} | ${formatTimestamp(item.reviewedAt)} | ${report} |`;
const plan = item.planPath
? markdownLink(item.planPath, reportFileUrl(item.number, item.planPath))
: "_pending_";
return `| ${markdownLink(`#${item.number}`, itemUrlFor(repo, item.number, item.kind))} | ${title} | ${item.workPriority} | ${item.workStatus} | ${formatTimestamp(item.reviewedAt)} | ${plan} | ${report} |`;
})
.join("\n") || "| _None_ | | | | | |"
.join("\n") || "| _None_ | | | | | | |"
);
}
@ -7468,9 +7613,12 @@ function formatFleetWorkQueueRows(items: readonly DashboardItem[], limit = 15):
const repo = item.repo ?? targetRepo();
const title = markdownTableCell(displayTitle(item.title));
const report = markdownLink(item.reportPath, reportFileUrl(item.number, item.reportPath));
return `| ${markdownLink(repo, repoUrlFor(repo))} | ${markdownLink(`#${item.number}`, itemUrlFor(repo, item.number, item.kind))} | ${title} | ${item.workPriority} | ${item.workStatus} | ${formatTimestamp(item.reviewedAt)} | ${report} |`;
const plan = item.planPath
? markdownLink(item.planPath, reportFileUrl(item.number, item.planPath))
: "_pending_";
return `| ${markdownLink(repo, repoUrlFor(repo))} | ${markdownLink(`#${item.number}`, itemUrlFor(repo, item.number, item.kind))} | ${title} | ${item.workPriority} | ${item.workStatus} | ${formatTimestamp(item.reviewedAt)} | ${plan} | ${report} |`;
})
.join("\n") || "| _None_ | | | | | | |"
.join("\n") || "| _None_ | | | | | | | |"
);
}
@ -7636,8 +7784,8 @@ ${formatRecentClosedRows(stats.recentClosed)}
#### Work Candidates
| Item | Title | Priority | Status | Reviewed | Report |
| --- | --- | --- | --- | --- | --- |
| Item | Title | Priority | Status | Reviewed | Plan | Report |
| --- | --- | --- | --- | --- | --- | --- |
${formatWorkQueueRows(stats.workQueue)}
#### Recently Reviewed
@ -7763,8 +7911,8 @@ ${formatFleetRecentClosedRows(recentClosed)}
### Work Candidates Across Repos
| Repository | Item | Title | Priority | Status | Reviewed | Report |
| --- | --- | --- | --- | --- | --- | --- |
| Repository | Item | Title | Priority | Status | Reviewed | Plan | Report |
| --- | --- | --- | --- | --- | --- | --- | --- |
${formatFleetWorkQueueRows(workQueue)}
<details>

View File

@ -1,7 +1,12 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
const tmpPrefix = join(tmpdir(), "clawsweeper-test-");
import {
applyDecisionPriority,
auditFromSnapshot,
@ -39,6 +44,7 @@ import {
reviewActionForDecision,
reviewPriority,
renderReviewCommentFromReport,
renderWorkPlanFromReport,
reviewPromptTelemetryForTest,
runtimeBudgetExceeded,
safeOutputTail,
@ -2100,6 +2106,125 @@ Full review comments:
assert.doesNotMatch(markers, /clawsweeper-verdict:needs-human/);
});
function workPlanCandidateReport(overrides = {}) {
const frontmatter = {
number: 321,
repository: "openclaw/clawsweeper",
type: "issue",
title: "Render work plans",
reviewed_at: new Date().toISOString(),
review_status: "complete",
local_checkout_access: "verified",
decision: "keep_open",
action_taken: "kept_open",
work_candidate: "queue_fix_pr",
work_status: "candidate",
work_priority: "medium",
work_confidence: "high",
work_likely_files: JSON.stringify(["src/clawsweeper.ts", "test/clawsweeper.test.ts"]),
work_validation: JSON.stringify(["pnpm run check"]),
work_cluster_refs: JSON.stringify(["openclaw/clawsweeper#26"]),
...overrides,
};
return `---
${Object.entries(frontmatter)
.map(([key, value]) => `${key}: ${value}`)
.join("\n")}
---
# #321: Render work plans
## Summary
The dashboard has queue_fix_pr candidates but no generated coding plan.
## Repair Work Prompt
Render generated plan markdown from existing report fields.
`;
}
test("renderWorkPlanFromReport renders dashboard plan artifacts for fresh queue_fix_pr candidates", () => {
const plan = renderWorkPlanFromReport(workPlanCandidateReport(), {
reportPath: "records/openclaw-clawsweeper/items/321.md",
});
assert.ok(plan);
assert.match(plan, /# Coding Plan for openclaw\/clawsweeper#321: Render work plans/);
assert.match(plan, /Render generated plan markdown from existing report fields\./);
assert.match(plan, /- `src\/clawsweeper\.ts`/);
assert.match(plan, /- `pnpm run check`/);
assert.match(plan, /openclaw\/clawsweeper#26/);
});
test("renderWorkPlanFromReport returns null for stale, reclassified, or non-candidate reports", () => {
assert.equal(renderWorkPlanFromReport(workPlanCandidateReport({ work_candidate: "none" })), null);
assert.equal(
renderWorkPlanFromReport(workPlanCandidateReport({ work_status: "manual_review" })),
null,
);
assert.equal(renderWorkPlanFromReport(workPlanCandidateReport({ action_taken: "closed" })), null);
assert.equal(
renderWorkPlanFromReport(workPlanCandidateReport({ reviewed_at: "2026-01-01T00:00:00.000Z" })),
null,
);
});
test("apply-artifacts writes and removes generated work plans", () => {
const root = mkdtempSync(tmpPrefix);
try {
const artifactDir = join(root, "artifacts");
const itemsDir = join(root, "items");
const closedDir = join(root, "closed");
const plansDir = join(root, "plans");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(join(artifactDir, "321.md"), workPlanCandidateReport(), "utf8");
execFileSync(process.execPath, [
"dist/clawsweeper.js",
"apply-artifacts",
"--target-repo",
"openclaw/clawsweeper",
"--artifact-dir",
artifactDir,
"--items-dir",
itemsDir,
"--closed-dir",
closedDir,
"--plans-dir",
plansDir,
"--replay-closed-artifacts",
"--skip-reconcile",
]);
const planPath = join(plansDir, "321.md");
assert.ok(existsSync(planPath));
assert.match(readFileSync(planPath, "utf8"), /## Plan\n\nRender generated plan markdown/);
writeFileSync(
join(artifactDir, "321.md"),
workPlanCandidateReport({ work_candidate: "none", work_status: "none" }),
"utf8",
);
execFileSync(process.execPath, [
"dist/clawsweeper.js",
"apply-artifacts",
"--target-repo",
"openclaw/clawsweeper",
"--artifact-dir",
artifactDir,
"--items-dir",
itemsDir,
"--closed-dir",
closedDir,
"--plans-dir",
plansDir,
"--replay-closed-artifacts",
"--skip-reconcile",
]);
assert.equal(existsSync(planPath), false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("security-needs-attention reports block unopted repair and automerge pass markers", () => {
const securitySection = `
## Security Review