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
116 changes: 113 additions & 3 deletions pm-stack/aggregator/src/project-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ export interface ProjectStatusReport {
url: string;
}

/** One working repo's card on a fleet-style project page (#1126).
* The role is editorial; the open-issue count is an explicitly dated
* GitHub-search snapshot (openIssuesAsOf) — never presented as live.
* Pool items and roadmap rows are NOT stored here: the detail builder
* splits the page's live pool/roadmap sections by repo at request
* time, so those halves cannot drift from the sections above them. */
export interface ProjectRepoCard {
repo: string;
role: string;
openIssues: number;
openIssuesAsOf: string;
trackedRefs: RefKey[];
}

export interface ProjectEditorial {
done: ProgramEntry[];
next: ProgramEntry[];
Expand All @@ -59,10 +73,20 @@ export interface ProjectConfig {
trackedRefs: RefKey[];
/** Extra architecture entries (the pm-dashboard doc renders its from here). */
architecture?: ProgramEntry[];
/** Per-repo cards (fleet pages, #1126); pool/roadmap halves are
* derived live by the detail builder, split by repo. */
repoCards?: ProjectRepoCard[];
/** Sweep-health entries (#1126): editorial entries whose claims
* rest only on already-published fleet data (the orchestration
* section this aggregator serves, the fleet's tracked issues) —
* no new data pipeline feeds this section. */
sweepHealth?: ProgramEntry[];
editorial?: ProjectEditorial;
}

const CODE = "developing-today/code";
const OPENCODE = "developing-today/opencode";
const OPENCHAMBER = "developing-today/openchamber";
const NIX = "dezren39/nix";
const blob = (path: string): string => `https://github.com/${CODE}/blob/main/${path}`;
const tree = (path: string): string => `https://github.com/${CODE}/tree/main/${path}`;
Expand Down Expand Up @@ -316,6 +340,57 @@ export const PROJECT_CONFIGS: ProjectConfig[] = [
code(310),
code(311),
],
repoCards: [
{
repo: CODE,
role: "The fleet's home repo: the id train, mcpx, this PM stack / aggregator, and the repo queue/worker pilot itself.",
openIssues: 1033,
openIssuesAsOf: "2026-10-29",
trackedRefs: [code(911), code(1233), code(1130)],
},
{
repo: OPENCODE,
role: "The OpenCode fork: the agent runtime the fleet's sessions run on. Upstream of openchamber — its updates are carried into openchamber and then into code's opencode pieces.",
openIssues: 35,
openIssuesAsOf: "2026-10-29",
trackedRefs: [ref(OPENCODE, 365), ref(OPENCODE, 371)],
},
{
repo: OPENCHAMBER,
role: "The OpenChamber fork: session UI and orchestration over opencode. Carries opencode updates downstream into code.",
openIssues: 36,
openIssuesAsOf: "2026-10-29",
trackedRefs: [ref(OPENCHAMBER, 214)],
},
{
repo: NIX,
role: "Drew's Nix config: machines, packages, and the deployed builds of mcpx and the forks (mcpx fixes land here and in code as one set).",
openIssues: 15,
openIssuesAsOf: "2026-10-29",
trackedRefs: [ref(NIX, 512)],
},
],
sweepHealth: [
{
title: "Work runs through the repo queue, not ad-hoc sweeps",
detail:
"Since the 2026-10-25 pilot, producers (issue creator, PR diff review, curators) enqueue structured repo-task packets and worker lanes claim them atomically in shifts; the lifecycle labels (queue/*, work/*, user/*) on each issue mirror its queue state, and the hourly fleet monitor repairs any divergence. This section's claims rest on that published queue/label state and the fleet's tracked issues — no separate sweep-health pipeline exists.",
refs: [code(1233), code(1130), code(1131)],
},
{
title: "The orchestrator's published section is the health signal",
detail:
"The hourly orchestration job reconciles the org Project from issue/PR/session signals and publishes the orchestration section this aggregator serves; its freshness is rendered live on the program index above. A stale or absent section says so on the page rather than being smoothed over.",
refs: [code(899)],
links: [{ label: "/api/orchestration.json", url: "/api/orchestration.json" }],
},
{
title: "What the fleet watches",
detail:
"Queue reliability (claims must end in a terminal verb — complete, park, or block — never evaporate), merge-queue stalls, and per-repo backlog growth measured by the dated GitHub-search counts on the cards above. The counts are snapshots with a date, refreshed by hand when this page is next edited; the pool and roadmap sections are the live halves.",
refs: [code(911)],
},
],
editorial: {
done: [
{
Expand Down Expand Up @@ -367,9 +442,9 @@ export const PROJECT_CONFIGS: ProjectConfig[] = [
],
gaps: [
{
title: "The pool is a fleet-level slice, not the fleet backlog (2026-10-27)",
title: "The pool is a fleet-level slice, not the fleet backlog (2026-10-29)",
detail:
"As of 2026-10-27, this page's fleet-ops pool carries 4 items, while GitHub issue search counts 665 open issues across the four repos (developing-today/code 372, developing-today/opencode 51, developing-today/openchamber 64, dezren39/nix 178). Most work stays in each repo's own backlog; this page tracks the fleet-level program only.",
"As of 2026-10-29, GitHub issue search counts 1,119 open issues across the four repos (developing-today/code 1,033, developing-today/opencode 35, developing-today/openchamber 36, dezren39/nix 15), while this page's fleet-ops pool — counted live above — is a small fleet-level slice of that total. Most work stays in each repo's own backlog; this page tracks the fleet-level program only and does not claim pool completeness.",
},
{
title: "Home doc scope is CI/automation only",
Expand Down Expand Up @@ -555,6 +630,16 @@ export function validateProjectConfigs(configs: ProjectConfig[]): string[] {
errors.push(`${where}: dashboardLinks route "${l.route}" is neither a dashboard route nor an https URL`);
}
checkRefs(c.trackedRefs, errors, where);
for (const card of c.repoCards ?? []) {
if (!card.repo.includes("/")) errors.push(`${where}: repoCard repo "${card.repo}" is not owner/repo`);
if (!card.role.trim()) errors.push(`${where}: repoCard ${card.repo} role is empty`);
if (!Number.isInteger(card.openIssues) || card.openIssues < 0)
errors.push(`${where}: repoCard ${card.repo} openIssues is not a non-negative integer`);
if (!/^\d{4}-\d{2}-\d{2}$/.test(card.openIssuesAsOf))
errors.push(`${where}: repoCard ${card.repo} openIssuesAsOf "${card.openIssuesAsOf}" is not a YYYY-MM-DD date`);
checkRefs(card.trackedRefs, errors, where);
}
checkEntries(c.sweepHealth, errors, where);
checkEntries(c.architecture, errors, where);
checkEntries(c.editorial?.done, errors, where);
checkEntries(c.editorial?.next, errors, where);
Expand Down Expand Up @@ -634,13 +719,27 @@ export function unsetPoolCount(pool: ProjectPoolItem[]): number {
return pool.filter((p) => p.area === null).length;
}

export interface RepoCardDetail {
card: ProjectRepoCard;
/** This project's live pool items whose issue lives in the card's repo. */
poolItems: ProjectPoolItem[];
/** This project's live roadmap rows tied to the card's repo (by
* title or by a linked ref in that repo). */
roadmapRows: RoadmapRow[];
tracked: TrackedIssue[];
}

export interface ProjectDetail {
config: ProjectConfig;
route: string;
poolItems: ProjectPoolItem[];
poolByStatus: Count[];
roadmapRows: RoadmapRow[];
tracked: TrackedIssue[];
/** Per-repo card halves, derived live from the same pool/roadmap
* data as the sections above them (#1126). Empty when the config
* carries no repoCards. */
repoCards: RepoCardDetail[];
/** Whole-pool items with no Area (the gap note for null-area projects). */
unsetPoolCount: number;
}
Expand All @@ -657,13 +756,22 @@ export function buildProjectDetail(
: pool
.filter((p) => p.area === config.poolArea)
.sort((a, b) => (a.status ?? "").localeCompare(b.status ?? "") || a.title.localeCompare(b.title));
const roadmapRows = config.roadmapArea === null ? [] : rows.filter((r) => r.area === config.roadmapArea);
return {
config,
route: projectRoute(config.id),
poolItems,
poolByStatus: counts(poolItems.map((p) => p.status)),
roadmapRows: config.roadmapArea === null ? [] : rows.filter((r) => r.area === config.roadmapArea),
roadmapRows,
tracked: config.trackedRefs.map((ref) => ({ ref, item: items.get(ref) ?? null })),
repoCards: (config.repoCards ?? []).map((card) => ({
card,
poolItems: poolItems.filter((p) => p.repo === card.repo),
roadmapRows: roadmapRows.filter(
(r) => r.title.includes(card.repo) || r.links.some((l) => l.ref.startsWith(`${card.repo}#`)),
),
tracked: card.trackedRefs.map((ref) => ({ ref, item: items.get(ref) ?? null })),
})),
unsetPoolCount: unsetPoolCount(pool),
};
}
Expand All @@ -682,8 +790,10 @@ export function allProjectRefs(configs: ProjectConfig[] = PROJECT_CONFIGS): RefK
};
for (const c of configs) {
for (const r of c.trackedRefs) add(r);
for (const card of c.repoCards ?? []) for (const r of card.trackedRefs) add(r);
const sections = [
c.architecture ?? [],
c.sweepHealth ?? [],
c.editorial?.done ?? [],
c.editorial?.next ?? [],
c.editorial?.longTerm ?? [],
Expand Down
22 changes: 22 additions & 0 deletions pm-stack/aggregator/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4792,6 +4792,26 @@ export function renderProgramProject(
roadmapHtml = `<p class="muted">${detail.roadmapRows.length} row${detail.roadmapRows.length === 1 ? "" : "s"} with area <code>${esc(config.roadmapArea)}</code> in Grist's Program roadmap.</p>${rows}`;
}

const repoCardsHtml = detail.repoCards.length
? `<h2>Per-repo cards</h2>
<p class="muted">One card per working repo. Open-issue counts are a dated GitHub-search snapshot (date on each card), never a live claim; the pool items and roadmap rows are split live from the same sections above, by repo.</p>${detail.repoCards
.map(({ card, poolItems, roadmapRows, tracked }) => {
const refs = tracked
.map(
({ ref, item }) =>
`<a class="badge src" href="${esc(refUrl(ref))}">${esc(ref)}</a>${item ? ` <span class="badge ${item.state === "open" ? "open" : "closed"}">${esc(item.state)}</span>` : ""}`,
)
.join(" ");
return `<div class="card"><strong><a href="https://github.com/${esc(card.repo)}">${esc(card.repo)}</a></strong><br>${esc(card.role)}<br><span class="muted">Open issues (GitHub search, ${esc(card.openIssuesAsOf)}): <strong>${card.openIssues}</strong> · fleet pool items in this repo: ${poolItems.length} · roadmap rows: ${roadmapRows.length}</span>${refs ? `<br>${refs}` : ""}</div>`;
})
.join("")}`
: "";

const sweepHealthHtml = config.sweepHealth?.length
? `<h2>Sweep health — published fleet state</h2>
<p class="muted">Built only from already-published fleet data — the orchestration section this aggregator serves and the fleet's tracked issues. No new data pipeline feeds this section.</p>${programEntries(config.sweepHealth, items)}`
: "";

const ed = config.editorial;
const editorialHtml = ed
? `${ed.done.length ? `<h2>What's built</h2>${programEntries(ed.done, items)}` : ""}${ed.next.length ? `<h2>What's next</h2>${programEntries(ed.next, items)}` : ""}${ed.longTerm.length ? `<h2>Long-term roadmap</h2>${programEntries(ed.longTerm, items)}` : ""}${ed.gaps.length ? `<h2>Known gaps</h2>${programEntries(ed.gaps, items)}` : ""}`
Expand All @@ -4809,6 +4829,8 @@ ${trackedTable(detail.tracked)}
${poolHtml}
<h2>Roadmap — live</h2>
${roadmapHtml}
${repoCardsHtml}
${sweepHealthHtml}
${editorialHtml}`;
return layout(
config.title,
Expand Down
70 changes: 67 additions & 3 deletions pm-stack/aggregator/test/program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,46 @@ describe("project registry", () => {
const fleetDone = (fleet.editorial?.done ?? []).map((e) => `${e.title} ${e.detail}`).join("\n");
expect(fleetDone).toContain("closed in GitHub");
const fleetGaps = (fleet.editorial?.gaps ?? []).map((e) => `${e.title} ${e.detail}`).join("\n");
expect(fleetGaps).toContain("2026-10-27");
expect(fleetGaps).toContain("665 open issues");
expect(fleetGaps).toContain("2026-10-29");
expect(fleetGaps).toContain("1,119 open issues");

const cloud = projectConfigById("cloud-program")!;
expect(cloud.roadmapArea).toBe("Cloud program");
});

test("repo-fleet carries four per-repo cards and a sweep-health section (#1126)", () => {
const fleet = projectConfigById("repo-fleet")!;
const cards = fleet.repoCards ?? [];
expect(cards.map((c) => c.repo)).toEqual([
"developing-today/code",
"developing-today/opencode",
"developing-today/openchamber",
"dezren39/nix",
]);
for (const card of cards) {
expect(card.role.length).toBeGreaterThan(0);
// Dated snapshot counts, taken 2026-10-29 — never live claims.
expect(card.openIssuesAsOf).toBe("2026-10-29");
expect(card.openIssues).toBeGreaterThan(0);
expect(card.trackedRefs.length).toBeGreaterThan(0);
}
expect(cards.reduce((n, c) => n + c.openIssues, 0)).toBe(1119);
// Sweep health rests on published fleet data only (orchestration
// section + tracked issues), and says so.
expect((fleet.sweepHealth ?? []).length).toBeGreaterThan(0);
const sweepText = (fleet.sweepHealth ?? []).map((e) => `${e.title} ${e.detail}`).join("\n");
expect(sweepText).toContain("orchestration");
// The scope note is refreshed, dated, and claims no completeness.
const gaps = (fleet.editorial?.gaps ?? []).map((e) => `${e.title} ${e.detail}`).join("\n");
expect(gaps).toContain("2026-10-29");
expect(gaps).toContain("does not claim pool completeness");
// Card + sweep-health refs resolve through the shared ref set.
const refs = allProjectRefs();
expect(refs).toContain("developing-today/opencode#365");
expect(refs).toContain("developing-today/openchamber#214");
expect(refs).toContain("dezren39/nix#512");
});

test("validation catches duplicates and malformed entries", () => {
const base: ProjectConfig = {
id: "x-proj",
Expand Down Expand Up @@ -341,7 +374,38 @@ describe("renderProgramProject", () => {
const fleet = projectConfigById("repo-fleet")!;
const fleetHtml = renderProgramProject(buildProjectDetail(fleet, items, pool, rows), items, PROV);
expect(fleetHtml).toContain("Queue fleet reliability");
expect(fleetHtml).toContain("665 open issues");
expect(fleetHtml).toContain("1,119 open issues");
// Per-repo cards + sweep health render (#1126).
expect(fleetHtml).toContain("Per-repo cards");
for (const repo of ["developing-today/code", "developing-today/opencode", "developing-today/openchamber", "dezren39/nix"]) {
expect(fleetHtml).toContain(repo);
}
expect(fleetHtml).toContain("Open issues (GitHub search, 2026-10-29)");
expect(fleetHtml).toContain("Sweep health — published fleet state");
expect(fleetHtml).toContain("No new data pipeline feeds this section");
});

test("detail builder splits pool and roadmap by repo for the fleet cards (#1126)", () => {
const pool = [
poolItem({ area: "fleet-ops", repo: "developing-today/code", ref: "developing-today/code#1", title: "code item" }),
poolItem({ area: "fleet-ops", repo: "dezren39/nix", ref: "dezren39/nix#2", number: 2, title: "nix item" }),
poolItem({ area: "pm-dashboard", repo: "developing-today/code", ref: "developing-today/code#3", number: 3, title: "other area" }),
];
const fleet = projectConfigById("repo-fleet")!;
const detail = buildProjectDetail(fleet, new Map(), pool, FIXTURE_ROADMAP);
expect(detail.repoCards.map((c) => c.card.repo)).toEqual([
"developing-today/code",
"developing-today/opencode",
"developing-today/openchamber",
"dezren39/nix",
]);
const byRepo = new Map(detail.repoCards.map((c) => [c.card.repo, c]));
expect(byRepo.get("developing-today/code")?.poolItems.map((p) => p.title)).toEqual(["code item"]);
expect(byRepo.get("dezren39/nix")?.poolItems.map((p) => p.title)).toEqual(["nix item"]);
expect(byRepo.get("developing-today/opencode")?.poolItems).toEqual([]);
// Fixture roadmap rows name their repo in the title / links.
expect(byRepo.get("developing-today/opencode")?.roadmapRows.length).toBeGreaterThan(0);
expect(byRepo.get("developing-today/openchamber")?.roadmapRows.length).toBeGreaterThan(0);
});

test("pm-dashboard page retains the full #1046 document", async () => {
Expand Down
Loading