Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ checkpoint, and status-only commits are intentionally omitted.

### Added

- 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.

Expand Down
11 changes: 10 additions & 1 deletion docs/work-lane.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
170 changes: 159 additions & 11 deletions src/clawsweeper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ interface DashboardItem {
action: string;
reviewStatus: string;
reportPath: string;
planPath?: string | undefined;
workCandidate: string;
workPriority: string;
workStatus: string;
Expand Down Expand Up @@ -822,6 +823,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`;
Expand Down Expand Up @@ -4223,6 +4228,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;
Expand Down Expand Up @@ -5638,6 +5748,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 => {
Expand Down Expand Up @@ -5968,6 +6083,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);
Expand Down Expand Up @@ -6001,14 +6117,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 {
Expand Down Expand Up @@ -6411,13 +6534,15 @@ function moveMarkdownFile(options: {
function reconcileFolders(options: {
itemsDir: string;
closedDir: string;
plansDir?: string;
maxPages?: number;
dryRun?: boolean;
fetchClosedAt?: boolean;
}): ReconcileResult {
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);
Expand Down Expand Up @@ -6449,6 +6574,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;
}

Expand All @@ -6466,6 +6595,7 @@ function reconcileFolders(options: {
}
const markdown = markReconciledState(sourceMarkdown, "open");
moveMarkdownFile({ sourcePath, destinationPath, markdown, dryRun });
syncWorkPlanFromReport({ markdown, reportPath: destinationPath, plansDir, dryRun });
movedToItems += 1;
}

Expand All @@ -6483,10 +6613,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));
}

Expand Down Expand Up @@ -6546,6 +6684,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;
Expand Down Expand Up @@ -6616,6 +6755,9 @@ function dashboardStats(
action,
reviewStatus,
reportPath: repoRelativePath(join(itemsDir, file)),
planPath: existsSync(join(plansDir, file))
? repoRelativePath(join(plansDir, file))
: undefined,
workCandidate,
workPriority,
workStatus,
Expand Down Expand Up @@ -6824,9 +6966,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_ | | | | | | |"
);
}

Expand Down Expand Up @@ -6869,9 +7014,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_ | | | | | | | |"
);
}

Expand Down Expand Up @@ -7037,8 +7185,8 @@ ${formatRecentClosedRows(stats.recentClosed)}

#### Work Candidates

| Item | Title | Priority | Status | Reviewed | Report |
| --- | --- | --- | --- | --- | --- |
| Item | Title | Priority | Status | Reviewed | Plan | Report |
| --- | --- | --- | --- | --- | --- | --- |
${formatWorkQueueRows(stats.workQueue)}

#### Recently Reviewed
Expand Down Expand Up @@ -7164,8 +7312,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>
Expand Down
Loading
Loading