Skip to content

Commit 67be7a7

Browse files
JonasJesus42claude
andauthored
feat(tanstack-migrator): per-round merge loop, .deco sync-mirror, deep triage (#513)
* feat(tanstack-migrator): incremental-merge loop + .deco push-mirror + investigative triage Replace the single-PR + human-merge model with an incremental per-round loop: opening_pr → reviewing (code-review agent gate) → merging (MCP squash-merge + branch delete) → deploying → then triaging (first deploy) or paritying (fix rounds); each fix round cuts a fresh migration/fix-N branch off main and loops until parity target is hit with an empty backlog. Adds mergePullRequest / deleteBranch github wrappers and reviewing/merging statuses (plain-TEXT columns, no migration). Rework the .deco sync into the proven push-mirror installed on the CLIENT repo (on:push .deco/** → rsync blocks → -tanstack repo via STOREFRONT_SYNC_TOKEN); SYNC_DECOFILE_INSTALL opens a PR adding only that one workflow file. Make triage uncapped + investigative (headless Playwright console capture + skill grep sweep) and surface the new phases in the widgets/dashboard/stepper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tanstack-migrator): wire cross-migration memory into the review phase The code-review agent now reads framework-notes.md + fixes.md so it can (a) not block a merge on a KNOWN framework bug and (b) recognize proven fix recipes; it can also append a new framework-note when a diff reveals a recurring framework bug. Triage already reads/writes all four memory files, so the new Playwright/grep sweeps contribute too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: regenerate registry.json (adds grafana MCP entry) Fixes the failing `check:registry` CI step — registry.json was out of date (missing the grafana MCP). Ran `bun run build:registry`; 115 public items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 60142f4 commit 67be7a7

23 files changed

Lines changed: 1204 additions & 377 deletions

registry.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2398,6 +2398,55 @@
23982398
]
23992399
}
24002400
},
2401+
{
2402+
"id": "deco/grafana",
2403+
"title": "Grafana",
2404+
"description": "Query self-hosted Grafana: list datasources, search/read dashboards, and run raw datasource or PromQL queries via /api/ds/query.",
2405+
"is_public": true,
2406+
"_meta": {
2407+
"mcp.mesh": {
2408+
"verified": false,
2409+
"friendly_name": "Grafana",
2410+
"short_description": "Query self-hosted Grafana datasources, dashboards, and PromQL — connect with your Grafana URL + a service-account token.",
2411+
"owner": "deco",
2412+
"has_remote": true,
2413+
"has_oauth": false,
2414+
"tags": [
2415+
"grafana",
2416+
"self-hosted",
2417+
"observability",
2418+
"monitoring",
2419+
"prometheus",
2420+
"dashboards",
2421+
"promql",
2422+
"metrics"
2423+
],
2424+
"categories": [
2425+
"Observability"
2426+
],
2427+
"readme": "The Grafana MCP connects AI agents to any self-hosted (or Cloud) Grafana instance over its HTTP API. **Key Features** — **GRAFANA_LIST_DATASOURCES** lists configured datasources (uid/name/type); **GRAFANA_SEARCH_DASHBOARDS** and **GRAFANA_GET_DASHBOARD** discover dashboards and read their full JSON (panels, targets, variables) so agents can learn the exact query a panel uses; **GRAFANA_QUERY** runs any native datasource query through POST /api/ds/query; **GRAFANA_QUERY_PROMETHEUS** is a convenience wrapper for instant or range PromQL. **Use Cases** — pull metrics into agent workflows, reproduce a dashboard panel's query, inspect cost/observability dashboards, and build automations on top of existing Grafana data. **Authentication** — provide your Grafana base URL and a service-account token (Administration → Service accounts → Add token); Viewer role is enough for querying. Ideal for teams running self-hosted Grafana who want programmatic, read-only access to metrics and dashboards without exposing Grafana broadly."
2428+
}
2429+
},
2430+
"server": {
2431+
"name": "grafana",
2432+
"title": "Grafana",
2433+
"description": "Query self-hosted Grafana: list datasources, search/read dashboards, and run raw datasource or PromQL queries via /api/ds/query.",
2434+
"icons": [
2435+
{
2436+
"src": "https://grafana.com/static/assets/img/fav32.png"
2437+
}
2438+
],
2439+
"remotes": [
2440+
{
2441+
"type": "HTTP",
2442+
"url": "https://sites-grafana.deco.site/mcp",
2443+
"name": "grafana",
2444+
"title": "Grafana",
2445+
"description": "Query self-hosted Grafana: list datasources, search/read dashboards, and run raw datasource or PromQL queries via /api/ds/query."
2446+
}
2447+
]
2448+
}
2449+
},
24012450
{
24022451
"id": "deco/grain",
24032452
"title": "Grain",

tanstack-migrator/server/constants.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ export const LIVENESS_STALL_MS = 4 * 60_000;
1717
/** Session timeouts per task kind (the reader gives up past these). */
1818
export const SESSION_TIMEOUT_MS: Record<string, number> = {
1919
migrate: 30 * 60_000,
20-
triage: 20 * 60_000,
20+
// triage is intentionally exhaustive (build + SSR + headless-browser console
21+
// capture + skill grep sweep) — give it room.
22+
triage: 40 * 60_000,
23+
review: 20 * 60_000,
2124
fix: 25 * 60_000,
2225
parity: 20 * 60_000,
2326
};
@@ -34,7 +37,11 @@ export const DEFAULT_NO_IMPROVE_LIMIT = 3;
3437
export const DEFAULT_MAX_CONCURRENT = 1;
3538
export const DEFAULT_MAX_FIX_SESSIONS = 20;
3639
export const DEFAULT_FIX_BATCH_SIZE = 3;
37-
export const DEFAULT_MAX_ISSUES_PER_TRIAGE = 15;
40+
/**
41+
* Effectively uncapped: triage should report EVERY real problem it finds. The
42+
* high bound is only a runaway safety rail (dedupe + labeling stay bounded).
43+
*/
44+
export const DEFAULT_MAX_ISSUES_PER_TRIAGE = 100;
3845
export const DEFAULT_WORK_BRANCH = "migration/tanstack";
3946

4047
/** Object-storage key prefix for parity artifacts. */

tanstack-migrator/server/db/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export type SiteStatus =
99
| "baselining"
1010
| "migrating_script"
1111
| "opening_pr"
12+
| "reviewing"
13+
| "merging"
1214
| "triaging"
1315
| "fixing"
1416
| "paritying"
@@ -47,6 +49,8 @@ export const ACTIVE_STATUSES: SiteStatus[] = [
4749
"baselining",
4850
"migrating_script",
4951
"opening_pr",
52+
"reviewing",
53+
"merging",
5054
"triaging",
5155
"fixing",
5256
"paritying",
@@ -109,6 +113,7 @@ export function isSessionStatus(status: string): boolean {
109113
status === "baselining" ||
110114
status === "migrating_script" ||
111115
status === "triaging" ||
116+
status === "reviewing" ||
112117
status === "fixing" ||
113118
status === "paritying" ||
114119
isLegacyStatus(status)
@@ -131,6 +136,7 @@ export function isActiveStatus(status: string): boolean {
131136
export type RunKind =
132137
| "migrate"
133138
| "triage"
139+
| "review"
134140
| "fix"
135141
| "parity"
136142
| "fix_iteration" // legacy (≤ v0.4.x)

tanstack-migrator/server/engine/machine.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { baselining } from "./phases/baselining.ts";
88
import { creatingRepo } from "./phases/creating-repo.ts";
99
import { deployingCf } from "./phases/deploying-cf.ts";
1010
import { fixing } from "./phases/fixing.ts";
11+
import { merging } from "./phases/merging.ts";
1112
import { migratingScript } from "./phases/migrating-script.ts";
1213
import { openingPr } from "./phases/opening-pr.ts";
1314
import { paritying } from "./phases/paritying.ts";
1415
import { provisioningSandbox } from "./phases/provisioning-sandbox.ts";
16+
import { reviewing } from "./phases/reviewing.ts";
1517
import { triaging } from "./phases/triaging.ts";
1618

1719
export interface EngineDeps {
@@ -35,6 +37,8 @@ export const PHASE_HANDLERS: Partial<Record<SiteStatus, PhaseHandler>> = {
3537
baselining,
3638
migrating_script: migratingScript,
3739
opening_pr: openingPr,
40+
reviewing,
41+
merging,
3842
triaging,
3943
fixing,
4044
paritying,

tanstack-migrator/server/engine/phases/deploying-cf.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,29 @@ import { previewRendersRealHtml } from "../../lib/preview.ts";
2222
import { getDriver, isSimulation } from "../../sandbox/client.ts";
2323
import { deployPrompt } from "../../sandbox/templates/prompts.ts";
2424

25-
/** After a successful deploy, advance to parity (measured against the CF URL). */
26-
async function advanceToParity(
25+
/**
26+
* After a successful deploy, route on whether this was the FIRST deploy:
27+
* - initial migration (no cf_deploy_url yet) → triaging (build the backlog)
28+
* - every fix round (cf_deploy_url already set) → paritying (measure vs URL)
29+
* Robust even when triage found zero issues (doesn't depend on issues_total).
30+
*/
31+
async function advanceAfterDeploy(
2732
site: SiteRow,
2833
patch: Partial<SiteRow>,
2934
): Promise<void> {
35+
const firstDeploy = !site.cf_deploy_url;
36+
const next = firstDeploy ? "triaging" : "paritying";
3037
await updateSite(site.id, {
3138
...patch,
32-
status: "paritying",
33-
phase_detail:
34-
"deploy ok — starting parity measurement against the real URL",
39+
status: next,
40+
phase_detail: firstDeploy
41+
? "deploy ok — triaging the migrated site"
42+
: "deploy ok — measuring parity against the real URL",
3543
last_progress_at: new Date().toISOString(),
3644
});
3745
await addEvent(
3846
site.id,
39-
`CF deploy ok at ${patch.cf_deploy_url ?? "?"} — advancing to parity`,
47+
`CF deploy ok at ${patch.cf_deploy_url ?? site.cf_deploy_url ?? "?"}${firstDeploy ? "triaging" : "advancing to parity"}`,
4048
);
4149
}
4250

@@ -48,7 +56,7 @@ export async function deployingCf(
4856
const workerName = parseRepo(site.target_repo).repo;
4957

5058
if (isSimulation(ctx)) {
51-
await advanceToParity(site, {
59+
await advanceAfterDeploy(site, {
5260
cf_project_name: workerName,
5361
cf_deploy_url: `https://${workerName}.deco-cx.workers.dev`,
5462
});
@@ -142,7 +150,7 @@ export async function deployingCf(
142150
);
143151
}
144152

145-
await advanceToParity(current, {
153+
await advanceAfterDeploy(current, {
146154
cf_project_name: workerName,
147155
cf_deploy_url: deployUrl,
148156
});

tanstack-migrator/server/engine/phases/fixing.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { addEvent } from "../../db/events.ts";
1111
import { createRun, finishRun } from "../../db/runs.ts";
1212
import { getSite, incrementCost, updateSite } from "../../db/sites.ts";
1313
import type { SiteRow } from "../../db/types.ts";
14-
import { ghsTokenForSite } from "../../lib/github.ts";
14+
import { ensureBranch, ghsTokenForSite, parseRepo } from "../../lib/github.ts";
1515
import {
1616
closeResolvedIssues,
1717
markBlockedIssues,
@@ -40,14 +40,24 @@ export async function fixing(
4040
ctx: WorkerCtx,
4141
deps: EngineDeps,
4242
): Promise<void> {
43-
// Terminal checks first (idempotent re-entry)
44-
if (site.parity_score !== null && site.parity_score >= site.parity_target) {
43+
// Terminal check (idempotent re-entry): parity target met AND backlog empty
44+
// means we're done — no more rounds. (Parity is measured post-merge in
45+
// paritying; deploy happens per-round via opening_pr → … → deploying.)
46+
if (
47+
site.parity_score !== null &&
48+
site.parity_score >= site.parity_target &&
49+
site.issues_open === 0
50+
) {
4551
await updateSite(site.id, {
46-
status: "deploying",
47-
phase_detail: `parity ${site.parity_score} >= ${site.parity_target}, going to deploy`,
52+
status: "done",
53+
phase_detail: `parity ${site.parity_score}% with empty backlog — done`,
54+
finished_at: new Date().toISOString(),
4855
last_progress_at: new Date().toISOString(),
4956
});
50-
await addEvent(site.id, `Parity reached ${site.parity_score}% — CF deploy`);
57+
await addEvent(
58+
site.id,
59+
`Parity ${site.parity_score}% and backlog empty — done`,
60+
);
5161
return;
5262
}
5363
if (deps.inflight.has(site.id)) return;
@@ -111,6 +121,36 @@ export async function fixing(
111121
}));
112122
}
113123

124+
// New round: cut a fresh branch off main so this batch gets its own PR that
125+
// is reviewed + merged independently. Only when starting a fresh round
126+
// (pr_number null); a review-reject keeps pr_number set → same branch.
127+
if (!isSimulation(ctx) && !site.pr_number) {
128+
if (!site.target_repo) throw new Error("target_repo not set");
129+
const ref = parseRepo(site.target_repo);
130+
const round = site.fix_sessions_done + 1;
131+
const roundBranch = `migration/fix-${round}`;
132+
if (site.work_branch !== roundBranch) {
133+
await ensureBranch(ctx, ref, roundBranch, "main");
134+
await updateSite(site.id, {
135+
work_branch: roundBranch,
136+
pr_number: null,
137+
pr_url: null,
138+
preview_ready: false,
139+
});
140+
site = {
141+
...site,
142+
work_branch: roundBranch,
143+
pr_number: null,
144+
pr_url: null,
145+
preview_ready: false,
146+
};
147+
await addEvent(
148+
site.id,
149+
`New fix round on branch ${roundBranch} (off main)`,
150+
);
151+
}
152+
}
153+
114154
const sessionNumber = site.fix_sessions_done + 1;
115155
const run = await createRun({
116156
siteId: site.id,
@@ -186,6 +226,7 @@ export async function fixing(
186226
meta: { issues: { resolved: [] } },
187227
});
188228
await updateSite(site.id, {
229+
status: "opening_pr",
189230
fix_sessions_done: sessionNumber,
190231
issues_open: current.issues_open - resolvedCount,
191232
issues_closed: current.issues_closed + resolvedCount,
@@ -226,18 +267,19 @@ export async function fixing(
226267
},
227268
});
228269
await updateSite(site.id, {
270+
status: "opening_pr",
229271
fix_sessions_done: sessionNumber,
230272
sandbox_session_id: null,
231273
phase_thread_id: null, // each batch is a fresh, narrow conversation
232274
transient_retries: 0,
233-
phase_detail: `fix ${sessionNumber}: ${resolved.length}/${batch.length} resolved, backlog ${openIssues.open}`,
275+
phase_detail: `fix ${sessionNumber}: ${resolved.length}/${batch.length} resolved, backlog ${openIssues.open} — opening PR`,
234276
last_progress_at: new Date().toISOString(),
235277
});
236278
await addEvent(
237279
site.id,
238-
`Fix session ${sessionNumber}: resolved ${resolved.length > 0 ? resolved.map((n) => `#${n}`).join(", ") : "none"}${blocked.length > 0 ? ` · blocked ${blocked.map((b) => `#${b.number}`).join(", ")}` : ""} — backlog ${openIssues.open}`,
280+
`Fix session ${sessionNumber}: resolved ${resolved.length > 0 ? resolved.map((n) => `#${n}`).join(", ") : "none"}${blocked.length > 0 ? ` · blocked ${blocked.map((b) => `#${b.number}`).join(", ")}` : ""} — backlog ${openIssues.open}, opening PR`,
239281
);
240-
// next tick re-enters: backlog empty → paritying; budget outneeds_human
282+
// advance: opening_pr → reviewing → merging → deployingparitying
241283
} catch (err) {
242284
const message = err instanceof Error ? err.message : String(err);
243285
await finishRun(run.id, { status: "failed", logsTail: message });

0 commit comments

Comments
 (0)