--- issue: 562 issue_title: "Leaf path modules re-derive the win32 path flavor from a raw platform parameter" --- # PathFlavor: pass the platform's path language, not the raw discriminator ## Release Recommendation **Release:** ship independently This is Step 3 of the pi-permission-system Phase 10 roadmap, tagged `Release: independent` (a member of no batch). It is a behavior-preserving refactor, so every implementation commit is `refactor:` (a hidden changelog type) — the work lands on `main` and auto-batches into the next `feat:`/`fix:` release rather than cutting one of its own (Refs [#479]). The closing documentation commit touches only release-excluded paths (`docs/architecture`, `.pi/`), so it does not cut a release either. ## Problem Statement The win32-vs-POSIX path flavor is re-derived from a raw `platform: NodeJS.Platform` parameter at 13 sites across the package. Six leaf functions open with the identical `const impl = platform === "win32" ? winPath : posixPath` ternary; the win32 case fold (`toLowerCase()`), the separator pick, and the `{ caseInsensitive: true, windowsSeparators: true }` match-options literal are each independently re-derived at further sites. The variant set is closed (win32 vs. POSIX), so the risk is not variant growth — it is **connascence of algorithm**: every site must re-derive the same mapping identically, and in a permission system a leaf that misses the case fold or separator fold is a silent bypass (the [#382]/[#508] bug class). The [#505]/[#510] seam made the leaves pure platform-parameterized functions behind the `PathNormalizer` facade — it fixed *where the platform is read* but threaded the raw discriminator rather than the resolved product, so each leaf still re-interprets it. ## Goals - Introduce `PathFlavor`, a value object that is the resolved product of the single `platform === "win32"` decision, and thread it into the leaves in place of the raw `platform` string. - Make the [#382]/[#508] must-agree bug class **structurally impossible**: the case fold, the containment geometry, and the match-options each have exactly one implementation, owned by the flavor. - Remove `NodeJS.Platform` from every domain signature: after this change, no `src/` module outside `index.ts` and the `PathFlavor` factory names `platform`. - Preserve behavior exactly — identical decisions on every input, on both platforms. This is not a breaking change. - Advance the roadmap's structural metrics: `platform === "win32"` sites 13 → 1; `caseInsensitive` derivations → 1; flat `src/` root 62 → 59 (three leaves relocate into `src/path/`). ## Non-Goals - Unifying `subagent-context`'s bespoke prefix-containment onto `PathFlavor.isWithin`. That check (`isPathWithinDirectoryForSubagent`) uses a different algorithm than `path.relative`-based containment and diverges on `..` segments and cross-root paths, so collapsing it is **behavior-affecting**, not a pure refactor. Filed as a follow-up: [#571]. - Splitting a separate `BashDialect` collaborator out of the win32 flavor. Pi core guarantees bash-on-win32 is always Git Bash (no cmd/PowerShell branch in core's `shell.ts`), so the win32⇔MSYS pairing is fixed; a second object would be polymorphism over an axis that cannot vary independently. Track-and-watch: extract it only if core ever un-fixes the pairing (WSL, PowerShell). - Any change to config, schema, the permission model, or user-facing docs — this is an internal structural refactor with no observable surface change. ## Background The relevant modules and their current platform coupling: - `src/path-normalizer.ts` — the facade constructed at the session edge with `(platform, cwd)`. Selects `this.impl` once, but also carries two `this.platform !== "win32"` guards in `forBashToken` / `interpretBashCdTarget` for Git Bash/MSYS semantics, and a `usesWindowsSeparators()` accessor. - `src/access-intent/path-normalization.ts` — `normalizePathForComparison` (resolve + normalize + fold), `canonicalNormalizePathForComparison` (+ realpath + fold), `getPathPolicyValues` / `getAbsolutePathPolicyValues` / `getCwdRelativePathPolicyValues`. Called by `AccessPath.forPath`. - `src/path-containment.ts` — `isPathWithinDirectory` (the `path.relative` containment geometry) and `isPathOutsideWorkingDirectory` (geometry + `isSafeSystemPath` exclusion). - `src/canonicalize-path.ts` — `canonicalizePath` (best-effort `realpathSync` walk using `impl.parse`/`impl.sep`/`impl.join`). - `src/pi-infrastructure-read.ts` — `isPiInfrastructureRead` (re-derives the win32 match-options literal, calls `isPathWithinDirectory`). - `src/authority/subagent-context.ts` — `normalizeFilesystemPath` (normalize + fold) and `isSubagentExecutionContext`, plus a private prefix-containment helper. - `src/rule.ts` — `pathMatchOptions` (re-derives the match-options literal, gated by `PATH_SURFACES`) and the `evaluate`/`evaluateFirst`/`evaluateAnyValue`/`evaluateMostRestrictive`/`ruleMatches` family, all threaded `platform`. - `src/permission-manager.ts` — holds `this.platform`, relays it to the `rule.ts` family. Consumes `ResolvedAccessIntent` and **must not import `AccessPath`** (ADR-0002, enforced by a `no-restricted-imports` rule scoped to this file). `PathFlavor` is a plain value object, not `AccessPath`, so it is safe to consume here. - `src/access-intent/bash/token-classification.ts` — `classifyTokenAsRuleCandidate` takes a `windowsSeparators` boolean derived from `PathNormalizer.usesWindowsSeparators()` by `bash-path-resolver.ts` ([#520]). - `src/index.ts` — the sole `process.platform` reader (the ESLint `no-restricted-syntax` guard exempts only this file); threads `hostPlatform` into `PermissionManager`, `PermissionSession` (→ `PathNormalizer`), and `SubagentDetection`. Constraints from AGENTS.md and the package skill that apply: - Platform handling has a single home (`PathNormalizer`); no `src/` module reads `process.platform` (ESLint-guarded). `PathFlavor` becomes a second platform-semantics home, still fed exclusively from `index.ts`'s one read. - To test Windows behavior on POSIX CI, inject a win32 flavor — never `vi.mock("node:path")`. The documented `new PathNormalizer("win32", cwd)` idiom becomes `new PathNormalizer(win32PathFlavor, cwd)`. - `permission-manager.ts` stays string-based (ADR-0002); a `PathFlavor` import is allowed (the guard bans only `access-intent/access-path`). ## Design Overview ### The `PathFlavor` value object `PathFlavor` is the platform's **path language** expressed as one object: syntax recognition, token semantics, and an equivalence relation — the three things the leaves currently re-derive. It is pure (no filesystem access) and immutable; two cached singletons are selected by a factory that holds the package's one remaining `=== "win32"` comparison. ```typescript // src/path/path-flavor.ts import type { PlatformPath } from "node:path"; import type { BashTokenShape } from "#src/access-intent/bash/msys-bash-tokens"; import type { WildcardMatchOptions } from "#src/wildcard-matcher"; export interface PathFlavor { /** Node's own platform strategy (path.win32 | path.posix). Path-domain primitives use it directly. */ readonly impl: PlatformPath; /** Win32 { caseInsensitive, windowsSeparators } | undefined — the match-options product for the wildcard engine. */ readonly matchOptions: WildcardMatchOptions | undefined; /** Comparison case fold: win32 => value.toLowerCase(), posix => value. */ fold(value: string): string; /** resolve + normalize + fold against a base — the #382 invariant's single home. */ comparable(pathValue: string, base: string): string; /** path.relative-based containment geometry. */ isWithin(pathValue: string, directory: string): boolean; /** True when the token contains a path separator under this platform (posix: "/"; win32: "/" or "\"). */ hasPathSeparator(token: string): boolean; /** MSYS/Git-Bash token shape on win32; always { kind: "plain" } on posix. */ bashTokenShape(token: string): BashTokenShape; } export const posixPathFlavor: PathFlavor; export const win32PathFlavor: PathFlavor; /** The one `platform === "win32"` decision in the package. */ export function pathFlavorForPlatform(platform: NodeJS.Platform): PathFlavor; ``` Ownership rule (prevents god-object drift): the flavor owns **platform semantics** (fold, geometry, token shape, match options, separator syntax). **Domain policy** stays in the functions that consume it — the lexical cleanup in `normalizePathForComparison` (trim / strip quotes / strip `@` / `expandHome`), the alias generation in `getPathPolicyValues`, the `isSafeSystemPath` exclusion in `isPathOutsideWorkingDirectory`, the infra-read rules, and `rule.ts`'s `PATH_SURFACES`-gated dispatch. `impl` is exposed rather than wrapped: post-migration its consumers are exclusively path-domain primitives, and `PlatformPath` is itself a strategy object Node maintains — wrapping it would add ~7 forwarding methods with no semantics. The doc comment states the ownership rule; sealing it later (make it private, mirror the used methods) is a two-line change if it ever itches. ### How the flavor dissolves each site `fold` / `comparable` / `matchOptions` collapse the equivalence family — the #382/#508 must-agree class. A leaf can no longer re-derive-and-diverge because it never derives at all: ```typescript // path-normalization.ts — after export function normalizePathForComparison(pathValue: string, base: string, flavor: PathFlavor): string { const cleaned = lexicalCleanup(pathValue); // domain policy stays here return cleaned ? flavor.comparable(cleaned, base) : ""; } ``` `bashTokenShape` dissolves `PathNormalizer`'s two `!== "win32"` guards into uniform dispatch — the posix flavor returns `{ kind: "plain" }` (semantically correct: every posix token is an ordinary path), so the switch runs unchanged on both platforms: ```typescript // path-normalizer.ts — after; no platform conditional remains forBashToken(token: string, options?: { resolveBase?: string }): AccessPath { const shape = this.flavor.bashTokenShape(token); switch (shape.kind) { case "device": return AccessPath.forDevice(token); case "drive-mount": return this.forPath(shape.windowsPath, options); case "posix-absolute": { /* literal-only + backslash alias, unchanged */ } case "plain": return this.forPath(token, options); } } ``` `hasPathSeparator` dissolves the ask-leak in the bash rule-candidate classifier — the resolver stops asking `usesWindowsSeparators()` and relaying a boolean through an options bag: ```typescript // token-classification.ts — after; the two includes() lines become one if (token.startsWith(".")) return token; if (flavor.hasPathSeparator(token)) return token; // was: includes("/") + (windowsSeparators && includes("\\")) if (token.includes("..")) return token; if (WINDOWS_DRIVE_PATH_PATTERN.test(token)) return token; ``` `PathNormalizer` exposes its flavor (`readonly flavor: PathFlavor`) so `bash-path-resolver.ts` passes `this.normalizer.flavor` to the classifier; `PathNormalizer.usesWindowsSeparators()` and `RuleCandidateOptions.windowsSeparators` are deleted. ### Construction and threading `index.ts` performs the one `process.platform` read, resolves it into the flavor once, and injects that collaborator into the three holders — "instantiate the right collaborator as soon as we know the platform": ```typescript // index.ts const flavor = pathFlavorForPlatform(process.platform); const permissionManager = new PermissionManager({ agentDir, flavor, isYoloEnabled }); const subagentDetection = new SubagentDetection({ subagentSessionsDir, flavor, registry }); // session ctor: new PermissionSession(..., flavor) -> new PathNormalizer(flavor, cwd) ``` `PathNormalizer` and `PermissionManager` hold `this.flavor` and drop their `platform` fields; `PermissionManager`'s constructor option becomes `flavor?: PathFlavor` (defaulting to `posixPathFlavor`, mirroring the old `platform ?? "linux"`); `SubagentDetection`'s dep becomes `flavor: PathFlavor`. ### Lift-and-shift bridge Migrating a leaf's signature from `platform` to `PathFlavor` breaks its callers at the type level. To keep every commit compiling and green, the migration is bottom-up: each leaf switches to `PathFlavor` first, and its not-yet-migrated callers bridge with an inline `pathFlavorForPlatform(platform)` at the call site. Because `pathFlavorForPlatform` returns cached singletons, the bridge is cheap and cannot diverge — the transitional state is still bypass-safe. The final threading step removes every inline bridge once the holders carry the flavor directly. ## Module-Level Changes New: - `src/path/path-flavor.ts` — the `PathFlavor` interface, `posixPathFlavor` / `win32PathFlavor` singletons, `pathFlavorForPlatform` factory. - `test/path/path-flavor.test.ts` — unit tests for every capability, on both flavors. Relocated into `src/path/` (tidy-first — these leaves reach their final home; three fewer files at the flat `src/` root, 62 → 59): - `src/path-containment.ts` → `src/path/path-containment.ts`. Delete the standalone `isPathWithinDirectory` export (geometry moves onto `flavor.isWithin`); keep `isPathOutsideWorkingDirectory(canonicalPath, canonicalCwd, flavor)`. - `src/canonicalize-path.ts` → `src/path/canonicalize-path.ts`; `platform` → `flavor` (`flavor.impl`). - `src/pi-infrastructure-read.ts` → `src/path/pi-infrastructure-read.ts`; `platform` → `flavor` (`flavor.matchOptions`, `flavor.isWithin`); delete the re-derived match-options literal. - Move each module's test to `test/path/` and update the `#src/...` import. Edited (signature/body, no relocation): - `src/access-intent/path-normalization.ts` — all five exports `platform` → `flavor`; `normalizePathForComparison` = lexical cleanup + `flavor.comparable`; `canonicalNormalizePathForComparison` = `flavor.comparable` + `canonicalizePath` + `flavor.fold`; `getCwdRelativePathPolicyValues` uses `flavor.impl.relative` + `flavor.isWithin`. - `src/access-intent/access-path.ts` — `AccessPath.forPath` option `platform` → `flavor` (its only caller is `PathNormalizer.forPath`). - `src/path-normalizer.ts` — constructor `platform` → `flavor`; hold `this.flavor`; expose `readonly flavor`; drop both `!== "win32"` guards via `flavor.bashTokenShape`; delete `usesWindowsSeparators()`; relay `this.flavor` to every leaf; import the relocated leaves from `#src/path/...`. - `src/authority/subagent-context.ts` — `normalizeFilesystemPath` and `isSubagentExecutionContext` `platform` → `flavor` (`flavor.impl`, `flavor.fold`, `flavor.impl.sep`). - `src/authority/subagent-detection.ts` — `SubagentDetectionDeps.platform` → `flavor: PathFlavor`. - `src/rule.ts` — `pathMatchOptions(surface, flavor)` returns `PATH_SURFACES.has(surface) ? flavor.matchOptions : undefined`; `ruleMatches` / `evaluate` / `evaluateFirst` / `evaluateAnyValue` / `evaluateMostRestrictive` `platform` → `flavor`. - `src/permission-manager.ts` — option `platform?` → `flavor?: PathFlavor` (default `posixPathFlavor`); hold `this.flavor`; relay to the `rule.ts` family. Import `#src/path/path-flavor` (allowed by the `no-restricted-imports` guard, which bans only `access-intent/access-path`). - `src/permission-session.ts` — constructor `platform` → `flavor`; build `new PathNormalizer(flavor, "")` and rebuild on `activate`. - `src/access-intent/bash/token-classification.ts` — `classifyTokenAsRuleCandidate(token, flavor)` via `flavor.hasPathSeparator`; delete `RuleCandidateOptions.windowsSeparators` (and the interface if it empties). - `src/access-intent/bash/bash-path-resolver.ts` — pass `this.normalizer.flavor` to the classifier; drop the `usesWindowsSeparators()` read. - `src/index.ts` — construct the flavor once; inject into `PermissionManager`, `PermissionSession`, `SubagentDetection`. `hostPlatform` stays only as the argument to `pathFlavorForPlatform`. Verify `BashTokenShape` is exported from `src/access-intent/bash/msys-bash-tokens.ts` for `path-flavor.ts` to import (it holds the discriminated union `classifyWin32BashToken` returns); export it if it is currently local. Test fixtures/harnesses: - `test/helpers/session-fixtures.ts` — `makeRealSession`'s `platform?` override → `flavor?: PathFlavor` (or accept `platform` and map to `pathFlavorForPlatform` at the boundary); it builds a real `PermissionManager` + `PermissionSession`. - `test/helpers/gate-fixtures.ts` — the `new PathNormalizer(process.platform, ...)` construction → `pathFlavorForPlatform(process.platform)`. - Every `new PathNormalizer("win32"|"linux"|process.platform, cwd)` site (~30 across `test/`) → the corresponding singleton (`win32PathFlavor` / `posixPathFlavor`) or `pathFlavorForPlatform(process.platform)`. - `test/rule.test.ts` (49 platform references) and the manager tests → flavor singletons. Documentation (final commit; all release-excluded, so no release impact): - `.pi/skills/package-pi-permission-system/SKILL.md` — rework the prose naming the removed/changed mechanisms: the leaf list ("every `path-containment` / `path-normalization` / `pi-infrastructure-read` / `canonicalize-path` / `rule.ts` / `subagent-context.ts` leaf takes an injected `platform` parameter"), the `usesWindowsSeparators()` reference in the bash `external_directory` note, and the "pass `platform: 'win32'`" test idiom → "pass `win32PathFlavor`". These are reworded-prose updates carrying no removed symbol, so grep the skill for each mechanism name. - `packages/pi-permission-system/docs/architecture/architecture.md` — mark Step 3 complete (`✅` on the step heading and the `S3` Mermaid node); update the health-metric rows (`platform === "win32"` 13 → 1, flat `src/` root 62 → 59, `caseInsensitive` derivations → 1); refresh any module-layout listing or narrative that names the relocated files or the `platform` threading. `architecture.md` inline-copies the `rule.ts` `Rule`/`RuleOrigin`/`Ruleset` types — those are unchanged (only function signatures change), so that listing needs no edit; confirm during the pass. - `packages/pi-permission-system/docs/decisions/0002-path-values-string-boundary.md` — confirm it stays accurate; the manager now consumes a `PathFlavor` but still not `AccessPath`, so the boundary holds. Add a clarifying sentence only if the ADR's wording implies the manager holds a raw `platform`. ## Test Impact Analysis 1. New unit tests the extraction enables (previously impractical): `path-flavor.test.ts` directly exercises `fold`, `comparable`, `isWithin`, `hasPathSeparator`, `bashTokenShape`, and `matchOptions` on both singletons — the win32 case/separator semantics that were previously only reachable transitively through `AccessPath` / gate tests now have a focused home. 2. Tests that become redundant: the win32-specific assertions scattered in `path-containment.test.ts` (containment geometry) and portions of `path-normalization.test.ts` (fold behavior) overlap with the new flavor tests. Keep them for now — they exercise the leaf functions' domain-policy wrapping (lexical cleanup, alias generation), not the flavor's raw geometry — but simplify any assertion that only re-checks the fold once `path-flavor.test.ts` owns it. 3. Tests that must stay as-is: the `PathNormalizer` win32 tests (`path-normalizer.test.ts`), the bash MSYS token tests (`msys-bash-tokens.test.ts`, `bash-external-directory.test.ts`), and the gate acceptance tests genuinely exercise the composed behavior (token shape → AccessPath → decision) and pin the [#533]/[#520] semantics end-to-end; they only swap their construction idiom to the flavor singleton. ## Invariants at risk This change touches surfaces earlier phases refactored; each documented outcome must stay green: - [#382]/[#508] — win32 case/separator fold on path-surface matching. Pinned by the win32 path-matching tests in `rule.test.ts` and `permission-manager-unified.test.ts` (`new PathNormalizer("win32", ...)`). The fold moving onto `flavor.fold`/`flavor.matchOptions` must not change any decision — these tests are the guard. - [#533] — Git Bash/MSYS bash-token semantics (safe devices preserved, `/c/` mounts translated, other POSIX absolutes literal-only). Pinned by `bash-external-directory.test.ts` and `msys-bash-tokens.test.ts`; `bashTokenShape` dispatch must reproduce them exactly. - [#520] — win32 backslash-relative token recognized as path-shaped. Pinned by the backslash-token cases in the bash path tests; `hasPathSeparator` replacing the `windowsSeparators` flag must keep the same classification. - [#510]/[#505] — `PathNormalizer` is the single platform home fed from one `process.platform` read. Pinned by the ESLint `no-restricted-syntax` guard (still exempting only `index.ts`) and the `test/composition-root.test.ts` wiring tests. No new test is needed — each invariant already lives in a test, not only prose. ## TDD Order Every step below is behavior-preserving; each keeps the suite green and compiling. Commit type is `refactor:` throughout (the closing docs commit is `docs:`), so nothing cuts a release on its own. `pnpm fallow dead-code` is a final-state gate — the flavor's methods are consumed incrementally across steps 2–9 and are all live by step 8; do not expect a clean fallow run mid-sequence. 1. **Add `PathFlavor` (pure addition).** Red: `test/path/path-flavor.test.ts` asserts `fold` / `comparable` / `isWithin` / `hasPathSeparator` / `bashTokenShape` / `matchOptions` / `impl` on `win32PathFlavor` and `posixPathFlavor`, plus `pathFlavorForPlatform` selection. Green: implement `src/path/path-flavor.ts`; export `BashTokenShape` from `msys-bash-tokens.ts` if needed. `refactor(pi-permission-system): add PathFlavor value object`. 2. **Relocate + migrate `canonicalize-path`.** Move to `src/path/`, signature `platform` → `flavor`; bridge its one caller (`path-normalization`) inline; move its test; update importers. `refactor(pi-permission-system): thread PathFlavor through canonicalize-path`. 3. **Relocate + migrate `path-containment`; move geometry onto `flavor.isWithin`.** Delete standalone `isPathWithinDirectory`; migrate all callers (`path-normalization`, `pi-infrastructure-read`, `path-normalizer`) to `flavor.isWithin` (inline bridge where the holder still has `platform`); keep `isPathOutsideWorkingDirectory(..., flavor)`; move test. `refactor(pi-permission-system): move containment geometry onto PathFlavor`. 4. **Migrate `path-normalization` + `access-path.forPath`.** All five `path-normalization` exports → `flavor` (`comparable`/`fold`/`impl`); `AccessPath.forPath` option `platform` → `flavor`; bridge the caller (`PathNormalizer.forPath`); update `path-normalization` tests. `refactor(pi-permission-system): thread PathFlavor through path normalization`. 5. **Relocate + migrate `pi-infrastructure-read`.** Move to `src/path/`, `platform` → `flavor` (`matchOptions`, `isWithin`); delete the re-derived match-options literal; bridge the caller; move test. `refactor(pi-permission-system): thread PathFlavor through infrastructure-read`. 6. **Migrate `subagent-context` + `subagent-detection`.** Leaves → `flavor` (`impl`, `fold`, `impl.sep`); `SubagentDetectionDeps.platform` → `flavor`; bridge `index.ts`'s construction inline; update subagent tests. `refactor(pi-permission-system): thread PathFlavor through subagent detection`. 7. **Migrate `rule.ts` + `permission-manager`.** `pathMatchOptions` and the `evaluate` family `platform` → `flavor`; manager option `platform?` → `flavor?` (default `posixPathFlavor`); migrate `rule.test.ts` (49 sites) and the manager tests to the singletons via a lift-and-shift within the step. `refactor(pi-permission-system): thread PathFlavor through rule evaluation`. 8. **Thread the flavor from `index.ts`; dissolve `PathNormalizer`'s platform conditionals.** `index.ts` constructs the flavor once and injects it into `PermissionManager` / `PermissionSession` / `SubagentDetection`; `PermissionSession` + `PathNormalizer` constructors `platform` → `flavor`; `PathNormalizer` exposes `readonly flavor`, drops both `!== "win32"` guards via `bashTokenShape`; remove every inline `pathFlavorForPlatform(platform)` bridge from steps 2–7; update all ~30 `PathNormalizer` test constructors, `session-fixtures`, `gate-fixtures`, and `composition-root.test.ts`. `refactor(pi-permission-system): inject PathFlavor from the composition root`. 9. **Migrate the bash rule-candidate classifier; delete `usesWindowsSeparators`.** `classifyTokenAsRuleCandidate(token, flavor)` via `flavor.hasPathSeparator`; `bash-path-resolver` passes `this.normalizer.flavor`; delete `usesWindowsSeparators()` and `RuleCandidateOptions.windowsSeparators`; update classifier + resolver tests. `refactor(pi-permission-system): answer path-separator syntax on PathFlavor`. 10. **Documentation.** Update the package skill's reworded prose and the architecture roadmap (Step 3 `✅` + Mermaid node + health metrics); confirm ADR-0002 accuracy. `docs(pi-permission-system): record PathFlavor and complete roadmap Step 3`. ## Risks and Mitigations - **Silent decision drift (the exact bug class this fixes).** Mitigation: behavior-preserving throughout, guarded by the [#382]/[#508]/[#533]/[#520] invariant tests listed above; each step keeps the suite green before committing. - **Large mechanical test migration (`rule.test.ts`, ~30 `PathNormalizer` constructors).** Mitigation: lift-and-shift with the cached-singleton bridge so no step rewrites a whole test file at once; the singleton swap is a construction-idiom change, not a behavior change. - **Transitional dead code failing a mid-sequence `fallow` run.** Mitigation: treat fallow as a final-state gate (documented above); the pre-completion reviewer runs it once at the end when every flavor method is live. - **Intermediary inline `pathFlavorForPlatform(platform)` bridges left behind.** Mitigation: step 8 explicitly removes every bridge; a grep for `pathFlavorForPlatform(` outside `index.ts`, the factory, and tests must return nothing at the end. - **`permission-manager.ts` accidentally importing `AccessPath` while adding the `PathFlavor` import.** Mitigation: the `no-restricted-imports` rule already bans it; `PathFlavor` lives in `src/path/`, a different module, so the guard is untouched. ## Open Questions - None blocking. The `subagent-context` containment unification is deferred by design to [#571]; the `BashDialect` split is track-and-watch (Non-Goals). [#382]: https://github.com/gotgenes/pi-packages/issues/382 [#479]: https://github.com/gotgenes/pi-packages/issues/479 [#505]: https://github.com/gotgenes/pi-packages/issues/505 [#508]: https://github.com/gotgenes/pi-packages/issues/508 [#510]: https://github.com/gotgenes/pi-packages/issues/510 [#520]: https://github.com/gotgenes/pi-packages/issues/520 [#533]: https://github.com/gotgenes/pi-packages/issues/533 [#571]: https://github.com/gotgenes/pi-packages/issues/571