Part of #1367 (Phase 0 — Foundation).
Problem
Buddy Bot reconstructs PR state by regex-matching its own rendered markdown. rebase and the auto-close logic re-parse package names/versions out of PR body tables, and the dashboard extracts packages from PR titles/bodies the same way:
bin/cli.ts:873— rebase reconstructs updates from the PR bodysrc/buddy.ts:1834,1883,1915— auto-close logic round-trips markdown tablessrc/dashboard/dashboard-generator.ts:428-570— package extraction from PR bodies/titles
This is fragile (source of the bugs referenced near #1360/#1361 in code comments), breaks whenever the body template changes, and blocks features like custom pullRequest.bodyTemplate (see the dead-config issue) — a custom template would silently break rebase and auto-close.
Spec
Embed a machine-readable manifest in every Buddy Bot PR body inside an HTML comment, and make all state-reading paths consume it (with the regex path kept only as a legacy fallback for pre-existing PRs):
<!-- buddy-bot:manifest v1
{ "schemaVersion": 1,
"updates": [
{ "name": "typescript", "current": "^5.8.2", "target": "^5.8.3",
"type": "patch", "ecosystem": "npm", "file": "package.json",
"dependencyType": "devDependencies" }
],
"strategy": "all", "group": "Non-Major Updates",
"branch": "buddy-bot/update-...", "generatedAt": "..." }
-->Requirements
- New module
src/pr/pr-manifest.ts:serializeManifest(updates: PackageUpdate[], meta): string,parseManifest(body: string): PRManifest | null,hasManifest(body). Zero-dependency JSON in comment; tolerant parser (returnsnullon malformed JSON, never throws). src/pr/pr-generator.tsappends the manifest to every generated body (PRs and rebases).- Consumers switch to manifest-first, regex-fallback:
- rebase (
bin/cli.tsrebasecommand) checkAndCloseSatisfiedPRs/checkAndCloseObsoletePRs(src/buddy.ts)- dashboard package extraction (
src/dashboard/dashboard-generator.ts)
- rebase (
- Manifest must survive body-length truncation: append it before release notes are truncated to
releaseNotes.maxBodyLength, or place it at the top of the body. - GitHub PR body limit is 65,536 chars — budget the manifest and truncate release notes first, never the manifest.
- Schema is versioned (
schemaVersion); parser must accept unknown future fields.
Tasks
src/pr/pr-manifest.tswith types insrc/types.ts(PRManifest,PRManifestUpdate)- Emit manifest in
pr-generator.ts(all PR types: npm, composer, actions, docker, deps-files, security) - Convert the three consumer sites to manifest-first
- Legacy fallback: keep regex parsing behind
parseLegacyBody()and log a warning when it's used - Tests: round-trip for every ecosystem, malformed manifest, legacy body without manifest, truncation interplay, body-size budget
Acceptance criteria
- Rebasing a manifest-bearing PR produces identical update sets without touching regex paths
- A PR with a fully custom
bodyTemplate(once that ships) still rebases and auto-closes correctly - Existing open PRs (no manifest) continue to work via fallback
bun testpasses; new tests cover success/failure/edge perCLAUDE.mdconventions
Dependencies
None — this is the first Phase 0 issue; #1369 (auto-merge) and #1370 (dashboard) build on it.