Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1d0e9f9
docs(spec): app-own-layout design — make the demo dashboard disposabl…
agjs Jul 31, 2026
fbc2217
docs(spec): app-own-layout v2 — address panel BLOCK (two axes, opt-in…
agjs Jul 31, 2026
c78a1ce
docs(spec): app-own-layout v3 — additive app shell (approach A), reso…
agjs Jul 31, 2026
9c8b0f2
docs(spec): app-own-layout v4 — nav-as-data + real cross-nav + honest…
agjs Jul 31, 2026
33237f0
docs(plan): core<->adapter seam reclamation — grounded in the couplin…
agjs Jul 31, 2026
f2ed87b
refactor(core): WS1a — conventions via injected IConventionProvider s…
agjs Jul 31, 2026
c967f89
refactor(core): WS1a hardening (panel r1 advisories)
agjs Jul 31, 2026
c434bde
test(core): WS1a — independently verify both halves of the convention…
agjs Jul 31, 2026
efad0fa
refactor(core): WS1b — migrate reactive push + pull_conventions tool …
agjs Jul 31, 2026
ad748be
refactor(core): WS1b — drop dead isTopic, honest docblock, push-no-pr…
agjs Jul 31, 2026
6735a75
docs(core): correct IToolContext.conventions JSDoc (drop removed isTo…
agjs Jul 31, 2026
fb5e29f
test(core): WS1b — prove Session threads cfg.conventions into the rea…
agjs Jul 31, 2026
67f60b1
fix(policy)+test(core): classify pull_conventions as read_file; prove…
agjs Jul 31, 2026
36a83d1
fix(core)+test: close WS1b r4 findings — withheld-capability gate + c…
agjs Jul 31, 2026
22a2b64
refactor(core): drop hardcoded topic enum from pull_conventions tool
agjs Jul 31, 2026
bc9a0f1
refactor(core): build pull_conventions topic enum from injected provi…
agjs Jul 31, 2026
f1da038
test(core): observe Session-advertised pull_conventions schema + drop…
agjs Jul 31, 2026
600da07
refactor(core): offer pull_conventions only with a provider + relocat…
agjs Jul 31, 2026
0442e27
fix(core): keep pull_conventions prompt inventory in lockstep with th…
agjs Jul 31, 2026
5a6b521
refactor(core): resolve the stack via a generic IStackAdapter seam (WS2)
agjs Jul 31, 2026
c23a4f0
test(core): cover the REPL stack-adapter dispatch decision + wiring (…
agjs Jul 31, 2026
40e3257
test(core): source-guard that the REPL line handler is wired to the g…
agjs Jul 31, 2026
3592553
refactor(core): extract greenfieldOrSend so the interception branch i…
agjs Jul 31, 2026
227c97b
test(core): exactly-once cardinality + single-statement-bound greenfi…
agjs Jul 31, 2026
21e8d79
test(core): drop the as-cast from the greenfield spy + remove overcla…
agjs Jul 31, 2026
57871ba
test(core): strengthen the greenfield source guard to close both plan…
agjs Jul 31, 2026
de6cab1
test(core): pin greenfield wiring shape + commit the negative bypass …
agjs Jul 31, 2026
ae3db82
test(core): pin the ENTIRE greenfieldOrSend call shape (close the cal…
agjs Aug 1, 2026
24d2c88
test(core): negatives isolate their bypass + anchor the callee (WS2 r…
agjs Aug 1, 2026
2e315de
test(core): anchor greenfield guard to 'await ' + strip template lite…
agjs Aug 1, 2026
4ceeee2
test(core): replace regex wiring guard with an ast-grep STRUCTURAL match
agjs Aug 1, 2026
bc215ea
test(core): strict ast-grep wiring guard — exact shape + terminal che…
agjs Aug 1, 2026
77fa3a1
test(core): anchor handler-terminal check to the runLine arrow (close…
agjs Aug 1, 2026
d07ec57
test(core): anchor greenfield guard to the runLine arrow's LAST state…
agjs Aug 1, 2026
7699344
test(core): scope the greenfield guard's claim to what it enforces (W…
agjs Aug 1, 2026
86a39ea
docs(test): attribute the preceding-send limit accurately (not to the…
agjs Aug 1, 2026
930ede0
refactor(core): reclaim the web-shaped plan spine behind a generic IP…
agjs Aug 1, 2026
131bc1e
refactor(core): harden the WS3 plan-schema seam (panel r1 findings)
agjs Aug 1, 2026
eed4b4a
test(core): prove the WS3 plan-schema seam (panel r2 findings)
agjs Aug 1, 2026
8528d14
test(core): close WS3 plan-seam panel r2 minors (no-hidden-rule + arg…
agjs Aug 1, 2026
1f5a0a6
feat(core): make the core↔adapter boundary MECHANICAL (WS4 — seam rec…
agjs Aug 1, 2026
51a9552
test(core): harden the WS4 boundary test + fix stale eslint comment (…
agjs Aug 1, 2026
310ead4
test(core): make the WS4 negative control prove it was linted + full …
agjs Aug 1, 2026
1f92d18
feat(core): close the dynamic-import escape hatch in the core↔adapter…
agjs Aug 1, 2026
644da0f
feat(core): close all statically-analyzable dynamic-import evasions +…
agjs Aug 1, 2026
23d03b3
feat(core): close the immediately-invoked createRequire loader path +…
agjs Aug 1, 2026
f572f8c
feat(core): unify dynamic-loader boundary selectors + cover every for…
agjs Aug 1, 2026
294e83a
test(core): cross-platform basename + document safe over-matches (pan…
agjs Aug 1, 2026
2bc4931
feat(core): orphan-safe boundary fixtures + adapter-exemption test + …
agjs Aug 1, 2026
929f87d
test(core): scope the fixture ignore + prove it works (panel r9 minors)
agjs Aug 1, 2026
9e42b93
refactor(core): WS5 — relocate the acceptance subsystem into the Bori…
agjs Aug 1, 2026
47edfe4
fix(review): #63 — stop the panel pre-validate false-BLOCK + green-ou…
agjs Aug 1, 2026
424b67f
revert: back out the #63 pre-validate changes (memory caught a panel-…
agjs Aug 1, 2026
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
37 changes: 37 additions & 0 deletions docs/superpowers/plans/2026-07-31-core-adapter-seam-reclamation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Core ↔ Adapter Seam Reclamation — Plan

**Why:** tsforge is a general, TypeScript-specialized, local-model-optimized build harness; BoringStack is its FIRST adapter (Phaser next). Over a month of BoringStack focus, adapter concerns leaked into the core. This plan reclaims a clean seam so the core is stack-agnostic and BoringStack (craft, planning, boilerplate, layout) lives entirely in the adapter. See memory `tsforge-north-star` for the governing laws. **Harness-first, BoringStack-second. Every step panel-gated on the diff. Do not break the working harness.**

## Good news from the audit (branch `feat/app-own-layout`)
The **build-loop seam is already correct**: BoringStack (`runBoringstackBuild`) calls INTO the core `runGreenfield` loop via clean injected interfaces — `IBoringstackHost` (setScope/setGate/setExpertRescueTarget/captureMetaBaseline/send), `IPlanConstraints`, `Exec`, `Session.setGate/setScope`. Core loop has ZERO imports from `loop/boringstack/`. These injection patterns are the MODEL to mirror for every fix below.

## The real leaks (grounded, ranked)
1. **Conventions content in core.** `loop/conventions.ts` is 100% BoringStack (React/Elysia/shadcn/Drizzle) but is core-located and consumed by core: `session.ts:632` (`buildConventionGuides`), `turn.ts:28/229` (`unseenGuidesForErrors` + `PULL_CONVENTIONS_TOOL` offered when `offerConventions`), `tools/pull-conventions.ts` (`conventionGuide`/`conventionTopics`/`isConventionTopic`), and the `PULL_CONVENTIONS_TOOL` topic enum in `agent/agent.constants.ts`. So EVERY build (a future Phaser game) gets React idioms.
2. **Planner constraints in core.** `loop/planning/boringstack-planning.ts` (BoringStack-specific) is core-located; `cli/repl.ts:47-49,184,883` imports `isBoringstackProject`/`boringstackPlanConstraints` directly — core/CLI depending on the adapter.
3. **Web concepts in the core plan schema.** `loop/planning/plan-types.ts`: `IUiIntent` (`screens: list|detail|form|dashboard`, `nav`, `shows`, `layout`, `home`), `LAYOUT_ARCHETYPES`, `IMPLEMENTED_LAYOUT_ARCHETYPES` — all web/BoringStack, sitting in the core plan.
4. Low: BoringStack-only comments in `greenfield/run.ts:244`; `scaffold/boringstack-manifest.ts` naming.

## Workstreams (staged, each independently landable + panel-gated)

### WS1 — Conventions → adapter (via injected provider)
Mirror the `IBoringstackHost`/`cfg`-injection pattern.
- Define a generic `IConventionProvider` in core: `buildGuides(): string`, `unseenForErrors(errors, seen): {topic,guide}[]`, `guide(topic): string | null`, `topics(): string[]`, `isTopic(s): boolean`.
- `ISessionConfig` gains `conventions?: IConventionProvider`. Core consumers read from it: `session.ts:632` → `cfg.conventions?.buildGuides() ?? ""`; `turn.ts` reactive push + tool-offer → `cfg.conventions`; `tools/pull-conventions.ts` → the injected provider (validate topic via `provider.isTopic`); drop/generalize the static topic enum in `agent.constants.ts` (topics come from the provider).
- BoringStack `build-session.ts` constructs the provider from the (relocated) convention library and sets `cfg.conventions`. Absent provider → NO conventions (a generic build gets no React idioms — the whole point).
- **Step A (behavior-preserving):** introduce the provider + injection; BoringStack injects the same guides → identical behavior; generic builds get none. **Step B:** physically move `loop/conventions.ts` → `loop/boringstack/conventions.ts`.
- Keep the `convention-index.test.ts` enum-sync guarantee; update it to the provider.

### WS2 — Planner constraints → adapter
Move `loop/planning/boringstack-planning.ts` → `loop/boringstack/planning.ts`. `cli/repl.ts` must not import it directly — resolve stack detection + `IPlanConstraints` via a small generic seam (a stack-adapter lookup), so the CLI asks "which adapter for this dir?" and the adapter supplies constraints. `proposePlan` already takes generic `IPlanConstraints` — keep that; only the IMPLEMENTATION location + the CLI import move.

### WS3 — Reclaim the plan spine (biggest/riskiest; last)
Core `IProductPlan` keeps the general spine: `product`, the domain model (`IEntitySpec` entities/relations), `IVerificationContract`. The web UI (`IUiIntent` screens/nav/shows/layout/home, `LAYOUT_ARCHETYPES`) moves into a **BoringStack plan-extension** validated by the adapter. Mechanism: the slice carries an adapter-typed `presentation`/`ui` extension the core treats opaquely and the adapter validates (mirrors `IPlanConstraints`' generic-in-core / filled-by-adapter split). `parsePlan`/`plan-store` validation splits into a core spine validator + an adapter validator. This unblocks the layout work cleanly (it becomes an adapter concern that can't touch core). **This supersedes the app-own-layout spec's core placement of `shellLayout` — layout is adapter-only.**

### WS4 — Mechanical boundary (the finish line)
After WS1-3, add a dependency rule (eslint boundaries / dependency-cruiser / custom): **core (`loop/**` except `loop/boringstack/**`, `planning/**` spine, `cli/**`, `agent/**`) MUST NOT import from `loop/boringstack/**`** (and no BoringStack string content in core). A leak becomes a build failure. **This rule passing = the reclamation is done.**

## Finish line
The boundary rule passes + the core plan is a general spine + a generic build (no adapter) carries no BoringStack conventions/UI concepts. Then: core is stack-agnostic, BoringStack lives fully in the adapter, and the deferred layout/craft/planning/boilerplate work all happens inside the adapter — and Phaser later slots in as a second adapter.

## Constraints
Harness-first. Don't break the working BoringStack build (it currently reaches full green). Each WS panel-gated on the diff (4-model `harness-review`, reviewers ok ≥ 2). No full end-to-end builds as the driver — unit tests + panel; a build only as an occasional spot-check when a WS completes.
86 changes: 86 additions & 0 deletions docs/superpowers/specs/2026-07-31-app-own-layout-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Scaffold ANY app in its OWN layout (BoringStack demo dashboard becomes disposable) — Design Spec v3

**Status:** approved by the user (2026-07-31), approach (A) chosen. v1 + v2 were BLOCKED by the tsforge 4-model panel. v3 switches to an **additive** architecture that resolves the blocking findings by construction (real scope enforcement, the demo shell + its 11 routes untouched, the `dashboard` path literally unchanged, no per-slice nav-role axis). Re-submitted to the panel for the review of record before implementation.

## Context

The harness already builds arbitrary **domain features**, but force-wraps every one into BoringStack's demo `AppShell` + shared `AppSidebar` (a closed-typed, single-file structure) through several hardcoded coupling points. So every app comes out as links bolted onto the showcase dashboard. The dashboard is a **disposable showcase**, not the frame. Ground truth (verified against the scaffold): the shared `AppSidebar` uses a closed `IAppSidebarNavId` union + a typed icon `Record` + a count-assertion test; `routes.tsx` has **11** hardcoded `<AppShell>` wrappers; features are injected today as entries in the `APP_SIDEBAR_NAV_ITEMS` constant. v1/v2 tried to *split that shared file in-place* and the panel correctly showed it can't be scope-enforced or kept backward-compatible.

**Confirmed with the user:** keep the real account pages (billing/MFA/OAuth/team/audit/notifications) — they become the **Settings/Admin area**; nothing deleted. The requested app is primary, in its own layout, where you land. Deliver the general mechanism; prove it on several different apps.

## Approach (A): additive app nav — the demo shell/routes stay as Settings

For `shellLayout: "app-sidebar"` the harness gives the app its **own nav-set (as DATA in a new file)** and has the shared `AppSidebar`/`AppShell` render it, plus cross-navigation links between the app and the Settings area. The scaffold's **9 account/dashboard route wrappers, `APP_SIDEBAR_NAV_ITEMS`, `IAppSidebarNavId`, and the existing sidebar test are left exactly as-is** — they ARE the Settings/Admin area. The shared `AppSidebar`/`AppShell` **components** gain small additive, backward-compatible changes (below). Why (A) beats v2's in-place split:
- **Real scope enforcement** — the app nav *items* live in a NEW file (`APP_NAV_ITEMS`) at a distinct path; path-glob scope denies app features the account `AppSidebar`/`APP_SIDEBAR_NAV_ITEMS`. (v2's critical is gone: separate files, not two arrays in one.)
- **`dashboard`/legacy path = behaviorally unchanged** — when `shellLayout` is absent/`"dashboard"`, the harness emits nothing new and features go into the existing `APP_SIDEBAR_NAV_ITEMS` exactly as today; the new component props default to today's render. (Honest scope: the scaffold `AppSidebar`/`AppShell` *source* gains optional props/one conditional — a one-time template change, defaulted — so it is "behaviorally identical for dashboard builds," NOT "byte-identical scaffold source." The generated per-feature output for a dashboard build is unchanged.)
- **No per-slice nav-role** — the Settings area is the pre-existing scaffold pages, not plan features, so every plan slice is an app feature; the `navRole` axis is dropped.

### One axis only
- **`IProductPlan.shellLayout?: "dashboard" | "app-sidebar"`** — absent → `"dashboard"` = today's behavior. The **planner emits it**. Legacy `IUiIntent.layout` keeps its current meaning **only on the dashboard path**, so legacy `layout:"settings"` demotion still works exactly as today.
- **Validation:** an `app-sidebar` plan MUST have ≥ 1 slice (`plan-store.ts` rule); `isProductPlan` currently allows `slices:[]`, so this closes the "empty plan → no app home" hole below. `gate.ts` and every consumer read the **normalized/defaulted** plan from `plan-store` (never the raw file), so an absent `shellLayout` is always seen as `"dashboard"` (no false-green from `undefined`).

### Nav as DATA + additive, backward-compatible shell changes
The real `AppShell` mounts `AppSidebar` twice (desktop `<AppSidebar/>` + mobile `<AppSidebar onNavigate={closeMobileNav} className="w-full"/>`), so an opaque element prop can't carry per-mount props. Instead pass **data**:
- **`AppSidebar` gains optional props (all defaulted to today):** `navItems?` (default = today's account items from `APP_SIDEBAR_NAV_ITEMS`) rendered in BOTH mounts; and a `footerLink?: {to,label}` slot. No props → renders exactly as today (the backward-compat guarantee; test both mounts render identically with no props). Feature pages keep `AppShell`'s providers/header (`AppPageHeaderProvider`, `AccountSwitcher`, `NotificationBell`, `ThemeToggle`, sign-out) — no second provider tree, no context loss.
- **App route wrapper:** `<ProtectedRoute><AppShell brandTo={appHome}><AppSidebar navItems={APP_NAV_ITEMS} footerLink={settings→/account/profile} …/></AppShell>` (rendered internally in both mounts) — the app sidebar shows the app's features + a **Settings** footer link, so Settings is reachable (fixing the "app→Settings unreachable" critical: the stock header has no settings link).
- **`APP_NAV_ITEMS`** is a NEW open-typed data file (`{id,path,labelKey}[]`, no closed union) features append to; ships with its own co-located count test.

### Settings → app (the single additive component change, not a wrapper edit)
`AppSidebar`, when rendering the **default** (account) nav AND an app area exists (`APP_NAV_ITEMS` non-empty), shows a **"← back to app"** footer link → the app home. This is ONE additive, conditional change to the shared `AppSidebar` component (empty app nav → no link → today's render), so the 9 account route **wrappers stay untouched** and the dashboard path is unaffected. (The desktop brand is a non-link `<span>` today; back-to-app is a real nav link in the sidebar footer, not the brand.)

### Home (never lands in Settings)
`wireHomeRedirectForPlan` sets `DEFAULT_REDIRECT_TO`. Under `app-sidebar`: the `home` slice's route if marked, else the **first slice's route** (stable plan order). Combined with the ≥1-slice validation rule above, there is always an app landing — the v2 "lands in /dashboard" holes cannot occur.

### Decision-making (no new agent tools)
Planner picks `shellLayout` (user-approves the plan). Harness applies it deterministically (generate `AppNav` + shell props + wrapper + gate + home). The build agent gets **no new tools / no layout discretion** — under `app-sidebar` it just appends its link to the app-nav file the harness scoped + named; under `dashboard` it does exactly what it does today.

### `IShellDescriptor` (keyed by shellLayout, plan-level)
```ts
type ShellLayout = "dashboard" | "app-sidebar";
interface IShellDescriptor {
wrapRoute: (pageJsx: string, ctx: { brandTo: string }) => string; // route element for a feature page
navFile: string; // file a feature appends its nav link to (APP_NAV_ITEMS vs existing constants)
navGuidance: string; // refine-prompt text for where/how to add the link
navTestId: (camel: string) => string;
sidebarTestGlobs: string[]; // union the fast gate runs (account test always; APP_NAV_ITEMS test under app-sidebar)
emitsAppNav: boolean; // app-sidebar=true (harness seeds APP_NAV_ITEMS + passes navItems/footerLink); dashboard=false (today)
}
export function getShellDescriptor(shell: ShellLayout): IShellDescriptor // throws on unknown; only 2 today
```
Honest scope note: covers `{dashboard, app-sidebar}` only. `public`/`focused`/`top-nav` need more fields (auth boundary, provisioning) — explicit future extension points, not claimed-covered.

## Stages (each additive, landable, no dead code, no default-flip)

### Stage 1 — `shellLayout` schema + validation (zero behavior change)
Add `IProductPlan.shellLayout` + `plan-store.ts` validation; absent → `"dashboard"`. Legacy `layout` untouched. Planner NOT yet emitting it → every build stays dashboard. Tests: absent→dashboard; explicit values parse; legacy plans unaffected. Landable, no behavior change.

### Stage 2 — `app-sidebar` end-to-end behind the flag (additive)
`IShellDescriptor` + `getShellDescriptor`; `AppSidebar` optional `navItems`/`footerLink` props (rendered in BOTH desktop + mobile mounts; defaults = today's account items) + conditional "← back to app" footer when an app area exists; `AppShell` optional `brandTo`; the `APP_NAV_ITEMS` open-typed data file + its count test; `wireUiRouteFile` branches on the descriptor (app-sidebar → `AppShell brandTo` wrapping `AppSidebar navItems/footerLink`; dashboard → today's literal, unchanged); `scopeFor(name, shellLayout)` grants the `APP_NAV_ITEMS` file + its test under app-sidebar (not the account sidebar); `refine-prompt` app-nav guidance; `gate.ts` runs the sidebar-test union keyed on the normalized `plan.shellLayout`; `e2e-generator` app-area landing helper (both cross-nav directions); `wireHomeRedirectForPlan` first-slice default; the marker-guarded `APP_NAV_ITEMS` seeding + home wiring (plan-level, after pristine baselines + `captureMetaBaseline` + infra fail-closed, skip-if-present, resume-safe per file). Exercised by unit tests + a test plan with `shellLayout:"app-sidebar"`. The `dashboard` path is behaviorally unchanged. Landable: dashboard unchanged, app-sidebar fully working + tested, opt-in only.

### Stage 3 — Planner emits `shellLayout`
`propose-plan.ts` picks `app-sidebar` for normal apps, `dashboard` only for dashboard-shaped products (heuristic: the product's primary purpose is viewing aggregate/overview data with no primary CRUD entity → dashboard; otherwise app-sidebar; default app-sidebar). This flips NEW apps to their own layout; stored plans (no field) stay dashboard. Planner-contract tests.

### Stage 4 — Tests + live proof of GENERALITY
Per-seam tests (both descriptors) + generation/idempotency/resume + gate-union + home-default + scope-denial (real: app feature can't edit account sidebar). **Live across DIFFERENT apps:** (a) single-entity app, (b) multi-slice relational app (Company→Contact→Deal), (c) a `dashboard`-shellLayout app (legacy path unchanged). Each: `boringstack done · N/N verified` + final acceptance green; app-sidebar apps land in the app area with the real account pages reachable via the gear (e2e-covered) + back-to-app working; dashboard app byte-unchanged.

## Critical files
- **New:** `layout-descriptor.ts` (+tests); the `APP_NAV_ITEMS` open-typed data file (harness template) + its co-located count test; nav/home generation wiring (mirrors `applyHomeRedirect`).
- **Modify (harness):** `plan-types.ts`/`plan-store.ts` (shellLayout + ≥1-slice-under-app-sidebar rule + normalized read), `propose-plan.ts` (planner emits it), `wire-resource.ts` (`wireUiRouteFile` descriptor branch), `build.ts` (`scopeFor` signature + `runBoringstackBuild` generation/home), `gate.ts` (test-glob union keyed on the NORMALIZED shellLayout), `refine-prompt.ts`, `acceptance/testid-contract.ts`, `acceptance/e2e-generator.ts` (app-area helper).
- **Modify (scaffold, additive + backward-compatible):** `AppSidebar` gains optional `navItems`/`footerLink` (defaults = today's account items) rendered in BOTH desktop + mobile mounts, and a conditional "← back to app" footer when an app area exists; `AppShell` gains an optional `brandTo`. No props → identical to today. This is a one-time template change to the shared components (behaviorally identical for dashboard builds).
- **Untouched:** the **9** existing `<AppShell>` route wrappers, `APP_SIDEBAR_NAV_ITEMS`, `IAppSidebarNavId`, the existing sidebar test — the Settings area.
- **Reuse:** `homeRouteForPlan`/`wireHomeRedirectForPlan`/`applyHomeRedirect`, `AppShell` providers/header, `conventions.ts` guides.

## Risks (biggest first)
1. **Backward-compat** — absent/`dashboard` must be behaviorally unchanged; features → existing constants, and the new `AppSidebar`/`AppShell` props default to today's render. Test: both sidebar mounts render identically with no props; a dashboard build's generated feature output is unchanged.
2. **Fast-gate union (false-green)** — under app-sidebar the `APP_NAV_ITEMS` test MUST run in `gate.ts` (union with the account sidebar test), keyed on the normalized shellLayout; test asserts it does.
3. **Scope denial is now real** — app-nav data is a separate file; test that an app feature editing the account `AppSidebar`/`APP_SIDEBAR_NAV_ITEMS` is out-of-scope.
4. **Cross-nav reachability** — app→Settings via the app sidebar's Settings footer link; Settings→app via the conditional back-to-app footer. e2e covers both directions (the stock header has no settings link, so this must be explicit).
5. **Resume/ordering** — generation + home run plan-level after both pristine captures + infra fail-closed, marker-guarded, skip-if-present per file; a partial run re-completes on resume without clobbering model-added `APP_NAV_ITEMS` entries.
6. **Twice-mounted sidebar** — `navItems` is data (not an element), so both desktop and mobile mounts render the app nav + close-on-navigate works; test the mobile mount.
7. **Accepted parity-with-today (NOT regressions):** (a) a feature could rewrite a sibling's `APP_NAV_ITEMS` entry — identical add-only-shared-file discipline as today's `APP_SIDEBAR_NAV_ITEMS`; (b) the feature's scope includes its own nav count-test so it can bump the count — exactly as the existing flow scopes `AppSidebar.test.tsx` today. Both are pre-existing harness properties, not introduced here; noted, not fixed in this spec.
8. **Planner heuristic** — a wrong dashboard/app-sidebar guess is visible in the user-approved plan (human backstop); defaults to app-sidebar.

## Verification
- `bun run validate` green; unit + regression + generation + scope-denial + gate-union tests pass.
- **Panel-gate every stage's PR via the 4-model `harness-review` (reviewers ok ≥ 2)** — review of record.
- Live: the three different apps reach `N/N verified` + final acceptance green; app-sidebar apps land in their own app area with the real account pages reachable via the gear (e2e) + back-to-app; the dashboard app is byte-unchanged. Generality is the bar — one app passing is necessary, not sufficient.
Loading