Skip to content

Commit ed10bbb

Browse files
mlcyclopsclaude
andauthored
feat: P-REPORT.9 — multi-repo Engineering Report (fetch + PRs) + composer/UX fixes (ADR-0162) (#222)
* feat(report): P-REPORT.9 — multi-repo remote fetch + PR aggregation for the Engineering Report (ADR-0160) The Engineering Report was blind to anything not already on one repo's local default branch. This adds a repo picker (workspace ∪ recents ∪ tracked repos, plus add-by-path/clone-URL), a read-only `git fetch` per selected repo (never a pull — no working-tree mutation), recent-commit aggregation across branches, and opt-in GitHub PR listing via `gh` — folded into a new "Cross-repo activity" annex. - harness/brief/repo_activity.ts (+test): PURE parser/renderer; untrusted commit/ PR text escaped + fence-stripped + capped before the markdown (invariant #5). - desktop/repo_collect.ts: first-party fetch/branch/log/gh collector (fail-soft, timeouts), like cloneRepo — not the agent gate. - /api/brief POST + /api/report/repos(/add); settings reportRepos; bridge methods. - Reports panel: checkable repo picker with remote-URL verify surface + per-repo PR toggle (GitHub + gh-auth gated), and a hero Generate button with the rail-pill chasing-light border. Rail fly-outs now toggle closed on a second click. Verified: 25 unit tests + demo-P-REPORT.9 green; renderer suite 134 pass; live across public (mlcyclops/lucidagentide) + private (TechLead187/lucidagentIDEaddon) repos — commits across 8 branches each + PRs, working trees untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ui): pin composer on short windows + report repo sort (Recent/Name) Composer clipping: the .body grid's implicit row was content-sized (`auto`), so a tall column (chat+composer, or a full rail) overflowed the viewport and clipped the prompt bar / mic when the window was shrunk vertically. Pin the row with `minmax(0,1fr)` so columns shrink below content — the composer stays at the bottom and the thread just gets shorter (still scrollable). The nav rail + metrics rail now scroll instead of clipping; a short-window media query trims the composer footprint (hides the keyboard hint) so ~3 lines of thread stay visible. Verified 250–380px. Report repo sort: a Recent/Name picker in the Repositories header (persisted to localStorage). listReportRepos now returns each repo's last-commit epoch; "Recent" orders by it (desc), "Name" alphabetically. Sort affects display only — the active workspace pre-check still keys off the server order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(report): entity-escape pipes in clean() — resolve CodeQL js/incomplete-sanitization clean() escaped `|` as `\|` (backslash) without escaping backslashes — an incomplete sanitization scheme (CodeQL js/incomplete-sanitization, high). Switch to the `&#124;` HTML entity: table-safe (the markdown parser sees the entity, not a `|`), renders as `|`, runs after the &-escape so the entity's & stays literal, and introduces no backslash escaping at all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: mlcyclops <mlcyclops@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6ff55f0 commit ed10bbb

12 files changed

Lines changed: 1042 additions & 14 deletions

File tree

‎DECISIONS.md‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11514,3 +11514,75 @@ subcommand; `acp` never routes to tui; help flags exit 0 spawning nothing) via t
1151411514

1151511515
**Related.** ADR-0150 (`lucid tui`), ADR-0155/0156 (Neovim surface), ADR-0038 (launcher trust anchor),
1151611516
invariants #3/#4 (fail-closed, gate can't be omitted - both untouched).
11517+
## ADR-0162 - P-REPORT.9: multi-repo remote fetch + PR aggregation for the Engineering Report
11518+
11519+
**Date:** 2026-07-05
11520+
**Status:** Accepted. **P-REPORT.9 BUILT + tested.**
11521+
11522+
### Context
11523+
11524+
Product ask: the Engineering Report was blind to anything not already committed on ONE repo's local
11525+
default branch. It reads `DECISIONS.md`/`PROGRESS.md` from the app repo plus a local `git diff
11526+
HEAD~10..HEAD` (`gitChangeInputs`, dev.ts) - it never fetched remotes, never spanned other branches,
11527+
never saw the other repos a user tracks, and had no concept of pull requests. Users running against
11528+
several remote repos (feature branches, PRs, `master` moving upstream) got a report missing the newest
11529+
work. They asked to pick which repos to include, verify their remotes, add missing ones, pull the latest,
11530+
and fold recent commits + PRs into one comprehensive report - targeting only the repos they choose.
11531+
11532+
### Decision
11533+
11534+
1. **Fetch-only sync, never pull.** The user picked fetch-only over `git pull`: `git fetch --prune
11535+
--no-tags origin` per selected repo downloads remote commits/branches READ-ONLY - it never mutates the
11536+
working tree or current branch, so it can't conflict across many repos with uncommitted work. `pull`
11537+
and merge modes are deliberately out of scope (recorded as a follow-up, not a silent omission).
11538+
2. **A PURE renderer + a desktop collector, mirroring change_graph.ts / gitOut.**
11539+
`harness/brief/repo_activity.ts` is pure (raw git/gh strings → `RepoActivity` → a "Cross-repo activity"
11540+
annex markdown; no I/O, no Date) so it is fixture-tested. `desktop/repo_collect.ts` does the spawning:
11541+
fetch, `git for-each-ref` branch enumeration (local heads + `origin/*`, remote HEAD symref filtered,
11542+
capped at 8 by recency), `git log` per branch, the window diff for line totals, and gh for PRs.
11543+
3. **PRs via the `gh` CLI, opt-in per repo, GitHub only.** A per-row toggle, enabled only when the remote
11544+
is GitHub AND `gh auth status` succeeds (cached 60s). Non-GitHub / unauthed / not-requested each yield
11545+
an explicit `prStatus` that the annex renders as an honest "PRs skipped: <reason>" line. gh runs with
11546+
`cwd` = the repo so it reads the origin remote itself.
11547+
4. **Repo source = workspaces ∪ recents ∪ a new `reportRepos` list** (settings_store.ts). "Add repo"
11548+
takes a local path (validated `isGitRepo`) or a clone URL (reusing `cloneRepo`) and persists into
11549+
`reportRepos` WITHOUT calling `setWorkspace`/`backend.restart()` - tracking a repo for reporting must
11550+
never hijack the active omp session. The picker shows each repo's remote URL as the "verify" surface.
11551+
5. **`/api/brief` gains a POST path** `{ role, save, repos:[{path,fetch,prs}], window }`; when `repos` is
11552+
present it appends the cross-repo annex for ALL roles. GET (single-repo default) is unchanged, so a
11553+
plain Generate reproduces the prior brief exactly (back-compat). Two new endpoints: `GET
11554+
/api/report/repos` (candidates + gh-auth) and `POST /api/report/repos/add`.
11555+
11556+
### Security
11557+
11558+
- **First-party control-plane egress**, exactly like `cloneRepo` (ADR-0111): server-side `Bun.spawn` of
11559+
git/gh behind the loopback + per-launch-token gate (ADR-0022/0024), NOT the agent tool gate. Fetch is
11560+
read-only with a 25s per-repo timeout; every step is fail-soft (a failed fetch still yields local refs,
11561+
flagged; a bad repo contributes an empty labeled entry, never a throw or a blank).
11562+
- **Untrusted external content (invariant #5).** Commit subjects, PR titles, and author names are
11563+
externally authored. `clean()` collapses newlines, strips code-fence/inline-code breakout, escapes HTML,
11564+
escapes table pipes, and length-caps every field before it enters the markdown. The annex carries a
11565+
provenance line stating the text is DATA, never instructions - it holds when the brief later flows to
11566+
TTS / NotebookLM / the KG (`reportToKg`).
11567+
11568+
### Deliberate deltas / scope
11569+
11570+
The brief NARRATIVE still comes from the primary repo's DECISIONS/PROGRESS; only the additive annex is
11571+
cross-repo (broadening the narrative source is a follow-up). Non-GitHub PR providers (GitLab MRs, Azure
11572+
DevOps) are out of scope - detected and skipped with a reason, not half-supported.
11573+
11574+
### Verification
11575+
11576+
`bun test harness` 826 pass / 0 fail (+25 `repo_activity.test.ts`: URL parse GitHub-vs-not, commit/PR
11577+
parse, cross-branch dedup + totals, fetch-failure surfacing, each PR-skip reason, per-branch cap, and
11578+
untrusted-text escaping / fence-breakout). `make demo-P-REPORT.9` green. Renderer bundles clean. Live
11579+
against real repos: `listReportRepos()` resolved GitHub + GitLab + Azure-DevOps remotes and flagged
11580+
non-git folders; `collectRepoActivity` enumerated commits across feature/master/release branches with
11581+
line totals, and the annex escaped `&`. The 5 pre-existing `fs_browse.test.ts` env failures are unrelated.
11582+
11583+
### Relates to
11584+
11585+
ADR-0116/0117 (the Reports panel + brief store this extends), ADR-0072 (the Engineering Update engine),
11586+
ADR-0030 (`gitChangeInputs`, reused pattern), ADR-0111 (`cloneRepo` first-party git precedent),
11587+
ADR-0022/0024 (the loopback + token gate the endpoints sit behind), CLAUDE.md invariants #3 (fail-closed),
11588+
#5 (untrusted content delimited/as-data), #9 (stable ids).

‎Makefile‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ demo-P-FIGMA.2: ## P-FIGMA.2 (ADR-0154): after /figma import, a guided step —
463463
demo-P-SANDBOX.1: ## P-SANDBOX.1 (ADR-0157): the runtime execution boundary — sandbox seam (bwrap/noop), canNetwork/canExec caps ENFORCED at the omp spawn (suspicious-chain downgrade = real --unshare-net), managed require-isolation fail-closes, disclosed passthrough elsewhere
464464
$(BUN) run harness/scripts/demo_p_sandbox_1.ts
465465

466+
.PHONY: demo-P-REPORT.9
467+
demo-P-REPORT.9: ## P-REPORT.9 (ADR-0162): multi-repo remote fetch + PR aggregation for the Engineering Report — remote-URL parse (GitHub vs not), commits aggregated across branches (deduped) + line totals, the Cross-repo activity annex, fail-soft on a failed fetch (local refs still shown), PRs skipped with a reason on non-GitHub/unauthed remotes, and untrusted commit/PR text neutralized (no HTML/fence breakout)
468+
$(BUN) run desktop/scripts/demo_p_report_9.ts
469+
466470
.PHONY: dashboards
467471
dashboards: ## Materialize dashboard CSVs from a DuckDB into observable/docs/data (DB=path)
468472
$(BUN) run harness/scripts/materialize_dashboards.ts $(DB) observable/docs/data

‎PROGRESS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3080,3 +3080,8 @@ Roadmap phases (each its own future increment + ADR for its frozen-contract delt
30803080
- **shipped:** harness/runs/sandbox_exec.ts (SandboxBackend bwrap|noop, pure resolveBackend, wrapForProfile = the single fail-closed decision point: managed require-isolation unsatisfiable => refuse; canExec:false => refuse-exec; canNetwork:false on a passthrough => refuse, under bwrap => --unshare-net total deny - the chooseProfile suspicious downgrade is finally REAL); wired at BOTH spawns (lucid_acp execGated: refused resolution = exit 1 pre-spawn like the scanner preflight + loud sandboxDisclosure() passthrough line; acp_backend resolveSandboxPlan: managed-require w/o backend spawns with EVERY exec permission denied + audited via existing SecurityEvent - no new EventNames, P-SANDBOX.3 owns those); security.exec.requireIsolation managed knob (+ GPO ExecRequireIsolation), tighten-only. 16+4 new tests (801 green), tsc x3 clean, standalone launcher still compiles, demo-P-SANDBOX.1 green. Merge housekeeping: local marketplace ADR renumbered 0156->0158 (upstream took 0156/0157).
30813081
- **stubbed:** bwrap mount plan binds $HOME rw (fs containment stays omp --isolate's; network is the v1 boundary); seccomp deferred (needs a compiled BPF fd); agent_run.ts spawnGatedOmp not yet routed through the seam; macOS/Windows have NO isolating backend (disclosed passthrough) until P-SANDBOX.4.
30823082
- **next:** P-SANDBOX.2 - egress_proxy.ts (loopback DNS + CONNECT proxy consulting egressDecisionDetailed) for canNetwork:true profiles; the increment that directly answers the DNS-TXT exfil.
3083+
3084+
**P-REPORT.9 - multi-repo remote fetch + PR aggregation for the Engineering Report (ADR-0162)**
3085+
- **shipped:** harness/brief/repo_activity.ts (PURE: parseRemoteUrl/parseCommits/parsePrJson/buildRepoActivity + renderRepoActivityAnnex → the "Annex C - Cross-repo activity" markdown; clean() neutralizes untrusted commit/PR text - HTML escape + fence-breakout strip + pipe escape + cap, invariant #5); desktop/repo_collect.ts (fetch-only `git fetch --prune --no-tags`, for-each-ref branch enumeration capped at 8 by recency w/ origin-HEAD filtered, per-branch git log, window diff totals, gh PR list opt-in behind ghAvailable cache; listReportRepos/addReportRepo/collectRepoActivity - first-party spawn like cloneRepo, fail-soft + timeouts); /api/brief POST path + /api/report/repos(/add) endpoints; settings_store reportRepos; bridge reportRepos/addReportRepo + engineeringBrief(repos); Reports-panel repo picker (checkable rows + remote-URL verify surface + per-repo PR toggle gated on GitHub+gh-auth + Add-repo path/URL + Fetch-latest checkbox) + .rp-repo* styles. 25 tests + demo-P-REPORT.9 green; harness 826 pass/0 fail; verified live against real GitHub/GitLab/Azure-DevOps remotes.
3086+
- **stubbed:** fetch-only (no pull/merge - deliberate); brief NARRATIVE still primary-repo DECISIONS/PROGRESS (only the annex is cross-repo); PRs GitHub-only (GitLab MRs / Azure DevOps detected + skipped-with-reason, not supported); no per-fetch formal audit event yet (fetch status is surfaced in the annex, reach-out is loopback first-party).
3087+
- **next:** P-REPORT.10 candidates - GitLab/Azure PR providers; a formal SecurityEvent per fetch/PR reach-out; optional pull (ff-only) as an explicit opt-in; cross-repo narrative synthesis.

‎desktop/dev.ts‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
1616
import { buildEngineeringUpdate, renderEngineeringBrief, buildPodcastScript, renderScript, type PodcastBackend, type BriefRole } from "../harness/brief/engineering_update.ts";
1717
import { buildComplianceRows, renderPoamCsv, renderCkl } from "../harness/brief/compliance.ts"; // P-REPORT.6/.8: POA&M + CKL
1818
import { buildChangeGraph, buildSchemaChanges, renderAnnexes } from "../harness/brief/change_graph.ts"; // P-REPORT.8: report annexes
19+
import { renderRepoActivityAnnex } from "../harness/brief/repo_activity.ts"; // P-REPORT.9: cross-repo activity annex
20+
import { addReportRepo, collectRepoActivity, ghAvailable, listReportRepos, type RepoSelection } from "./repo_collect.ts"; // P-REPORT.9 (ADR-0162)
1921
import { loadChatBg, saveChatBg, type ChatBg } from "./chat_bg.ts"; // P-APPEAR.1: personalized chat background
2022
import { ingestCodeGraph, loadCodeGraph } from "./code_graph.ts"; // P-KG-CODE.1: workspace code graph
2123
import { ingestSymbolGraph, loadSymbolGraph } from "./symbol_graph.ts"; // P-KG-SYM.1: AST symbol graph
@@ -544,7 +546,10 @@ const server = Bun.serve({
544546
if (p === "/api/brief") {
545547
// P-REPORT.1 (ADR-0116): `?role=` tailors which sections lead + the framing; `?save=1` persists the
546548
// brief to the report store (so the Reports panel lists it). The goal-modal preview omits save.
547-
const roleRaw = url.searchParams.get("role");
549+
// P-REPORT.9 (ADR-0162): a POST body `{ role, save, repos, window }` additionally aggregates recent
550+
// commits + PRs across the SELECTED repos (fetched read-only) into a Cross-repo activity annex.
551+
const body = req.method === "POST" ? await readBody<{ role?: unknown; save?: unknown; repos?: unknown; window?: unknown }>(req) : {};
552+
const roleRaw = url.searchParams.get("role") ?? (body.role != null ? String(body.role) : null);
548553
const role: BriefRole | undefined = roleRaw === "developer" || roleRaw === "security" || roleRaw === "manager" || roleRaw === "executive" ? roleRaw : undefined;
549554
const repo = join(import.meta.dir, "..");
550555
const rd = (f: string) => { try { return existsSync(join(repo, f)) ? readFileSync(join(repo, f), "utf8") : ""; } catch { return ""; } };
@@ -557,9 +562,31 @@ const server = Bun.serve({
557562
const gi = gitChangeInputs(repo);
558563
brief += "\n\n" + renderAnnexes(buildChangeGraph(gi.numstat, gi.nameStatus, gi.range), buildSchemaChanges(gi.numstat, gi.nameStatus));
559564
}
560-
const savedRel = url.searchParams.get("save") === "1" ? saveBrief(Date.now().toString(36), role ?? "executive", brief) : null;
565+
// P-REPORT.9: cross-repo activity annex, only when repos were selected (POST). Fetch is read-only;
566+
// the annex is appended for ALL roles (it's the whole point of selecting extra repos).
567+
const rawRepos = Array.isArray(body.repos) ? (body.repos as unknown[]) : [];
568+
const sel: RepoSelection[] = rawRepos
569+
.map((r) => (r && typeof r === "object" ? r as Record<string, unknown> : {}))
570+
.filter((r) => typeof r.path === "string" && r.path)
571+
.map((r) => ({ path: String(r.path), fetch: r.fetch !== false, prs: r.prs === true }));
572+
if (sel.length) {
573+
const window = Number(body.window) || 10;
574+
const activities = await collectRepoActivity(sel, { fetch: true, prs: false, window });
575+
brief += "\n\n" + renderRepoActivityAnnex(activities);
576+
}
577+
const doSave = url.searchParams.get("save") === "1" || body.save === true;
578+
const savedRel = doSave ? saveBrief(Date.now().toString(36), role ?? "executive", brief) : null;
561579
return json({ ok: true, data: { brief, scriptText: renderScript(buildPodcastScript(u, role)), counts, role: role ?? "", savedRel } });
562580
}
581+
// P-REPORT.9 (ADR-0162): the candidate repos for a report (workspace ∪ recents ∪ report-only tracked)
582+
// + whether `gh` is authenticated (drives the PR toggle). Read-only; safe to poll.
583+
if (p === "/api/report/repos") return json({ ok: true, data: { repos: await listReportRepos(), ghAuth: await ghAvailable() } });
584+
// Add a report-target repo by local path or clone URL. Does NOT change the active workspace.
585+
if (p === "/api/report/repos/add" && req.method === "POST") {
586+
const b = await readBody<{ path?: unknown; url?: unknown }>(req);
587+
const r = await addReportRepo({ path: b.path != null ? String(b.path) : undefined, url: b.url != null ? String(b.url) : undefined });
588+
return json({ ok: r.ok, data: { repos: await listReportRepos(), ghAuth: await ghAvailable(), error: r.error } });
589+
}
563590
// P-REPORT.8: STIG Viewer .ckl export of the security control crosswalk (native XML checklist).
564591
if (p === "/api/brief/ckl") {
565592
const repo = join(import.meta.dir, "..");

0 commit comments

Comments
 (0)