34 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 21 | Split src/index.ts (1,983 lines) into focused modules |
Split src/index.ts into focused modules
Problem Statement
src/index.ts is ~1,973 lines and houses at least seven distinct concerns beyond the extension factory that is its actual responsibility.
AGENTS.md requires "one concern per file in src/" — index.ts is the last major outlier.
Goals
- Mechanically extract cohesive groups of functions into new focused modules.
- Reduce
src/index.tsline count by moving all extractable module-scope functions out. - Preserve all existing behavior — no observable change from the test suite.
- Add unit tests for the newly extracted modules and the pre-existing focused modules that lack them, using dependency injection and vitest mocks to test each module in isolation.
Non-Goals
- Behavior changes of any kind.
- Restructuring the
piPermissionSystemExtensionfactory or its closure-scoped helpers (they are inherently coupled to extension lifecycle state).
Background
Dependency status
| Issue | Title | Status | Relevance |
|---|---|---|---|
| #10 | Consolidate config layout | Closed/implemented | Was a prerequisite — no longer blocks. |
| #20 | Delete permission-request event channel | Closed/implemented | Removed emitPermissionRequestEvent and related types, reducing index.ts slightly. |
What was in src/index.ts (~1,973 lines)
The file contained these function/constant groups beyond the extension factory:
- Active-agent detection (~50 lines):
ACTIVE_AGENT_TAG_REGEX,normalizeAgentName,getActiveAgentName,getActiveAgentNameFromSystemPrompt. - External-directory / path utilities (~70 lines):
PATH_BEARING_TOOLS,normalizePathForComparison,isPathWithinDirectory,getPathBearingToolPath,isPathOutsideWorkingDirectory. - Permission prompt formatting (~250 lines):
formatMissingToolNameReason,formatUnknownToolReason,formatPermissionHardStopHint,formatDenyReason,formatUserDeniedReason,formatAskPrompt,formatSkillAskPrompt,formatSkillPathAskPrompt,formatSkillPathDenyReason,formatExternalDirectoryHardStopHint,formatExternalDirectoryAskPrompt,formatExternalDirectoryDenyReason,formatExternalDirectoryUserDeniedReason. - Tool-input preview / text utilities (~120 lines):
TOOL_INPUT_PREVIEW_MAX_LENGTH,TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,TOOL_TEXT_SUMMARY_MAX_LENGTH,truncateInlineText,sanitizeInlineText,countTextLines,formatCount,getPromptPath,formatEditInputForPrompt,formatWriteInputForPrompt,formatReadInputForPrompt,formatSearchInputForPrompt,serializeToolInputPreview,formatJsonInputForPrompt,formatToolInputForPrompt,formatGenericToolInputForLog,getToolInputPreviewForLog,getPermissionLogContext. - Subagent context (~25 lines):
normalizeFilesystemPath,isSubagentExecutionContext. - Forwarded-permission file IO (~180 lines):
sleep,formatUnknownErrorMessage,isErrnoCode,logPermissionForwardingWarning,logPermissionForwardingError,ensureDirectoryExists,getPermissionForwardingLocationForSession,ensurePermissionForwardingLocation,getExistingPermissionForwardingLocation,tryRemoveDirectoryIfEmpty,cleanupPermissionForwardingLocationIfEmpty,safeDeleteFile,writeJsonFileAtomic,readForwardedPermissionRequest,readForwardedPermissionResponse. - Forwarded-permission polling + confirmation (~180 lines):
formatForwardedPermissionPrompt,waitForForwardedPermissionApproval,processForwardedPermissionRequests,confirmPermission. - Misc helpers (~30 lines):
extractSkillNameFromInput,getEventToolName,getEventInput,getContextSystemPrompt,getSessionId,canRequestPermissionConfirmation,derivePiProjectPaths,createPermissionManagerForCwd.
Permission surfaces affected
None — pure refactor.
Test coverage gap
Before this issue, the entire src/index.ts — and every pre-existing focused module that was already extracted (bash-filter.ts, wildcard-matcher.ts, system-prompt-sanitizer.ts, skill-prompt-sanitizer.ts, permission-manager.ts, etc.) — was tested exclusively through 2 integration test files:
| File | Tests | Lines |
|---|---|---|
tests/permission-system.test.ts |
68 | 2,490 |
tests/session-start.test.ts |
2 | 114 |
These are flat lists of test() calls (no describe() grouping) that exercise the full piPermissionSystemExtension factory end-to-end via a mock ExtensionAPI.
The modules listed below have no dedicated unit test file at all:
src/active-agent.ts(58 lines) — newly extractedsrc/bash-filter.ts(51 lines) — pre-existingsrc/before-agent-start-cache.ts(44 lines) — pre-existingsrc/common.ts(88 lines) — pre-existingsrc/external-directory.ts(113 lines) — newly extractedsrc/logging.ts(118 lines) — pre-existingsrc/permission-dialog.ts(89 lines) — pre-existingsrc/permission-forwarding.ts(126 lines) — pre-existingsrc/permission-manager.ts(941 lines) — pre-existingsrc/permission-prompts.ts(131 lines) — newly extractedsrc/skill-prompt-sanitizer.ts(344 lines) — pre-existingsrc/status.ts(35 lines) — pre-existingsrc/subagent-context.ts(52 lines) — newly extractedsrc/system-prompt-sanitizer.ts(210 lines) — pre-existingsrc/tool-input-preview.ts(206 lines) — newly extractedsrc/tool-registry.ts(139 lines) — pre-existingsrc/wildcard-matcher.ts(84 lines) — pre-existingsrc/yolo-mode.ts(29 lines) — pre-existingsrc/forwarded-permissions/io.ts(328 lines) — newly extractedsrc/forwarded-permissions/polling.ts(334 lines) — newly extracted
Design challenges still in index.ts
After extraction, src/index.ts is ~970 lines.
The factory function itself is ~740 lines because it owns:
-
Module-scope mutable state —
extensionConfig,extensionLogger,loggingWarningReporter, andreportedLoggingWarningslive outside the factory. The factory writes to them viasetExtensionConfig/setLoggingWarningReporterand every extracted module that needs logging receives it via setter injection (setForwardedPermissionLogger). This works but creates hidden temporal coupling: callers must call the setter before any logging function is invoked. -
Module-scope constants derived from
getAgentDir()—PI_AGENT_DIR,SESSIONS_DIR,SUBAGENT_SESSIONS_DIR,PERMISSION_FORWARDING_DIR, andGLOBAL_LOGS_DIRare all computed at import time, which violates the AGENTS.md rule "Do not cachegetAgentDir()at module scope." They happen to work becausegetAgentDir()returns a stable value in production, but they make the module difficult to test in isolation (tests setPI_CODING_AGENT_DIRafter import). -
Closure-scoped helpers that could be pure —
refreshExtensionConfig,saveExtensionConfig,resolveAgentName,shouldExposeTool,logResolvedConfigPaths,reviewPermissionDecision,promptPermission,startForwardedPermissionPolling, andstopForwardedPermissionPollingare all closures overpermissionManager,runtimeContext,extensionConfig, and the forwarding timer. Some of these (e.g.,resolveAgentName,shouldExposeTool) could be pure functions if given their dependencies as parameters; others (e.g.,startForwardedPermissionPolling) genuinely need mutable timer state. -
Six event handlers inline —
session_start,resources_discover,session_shutdown,before_agent_start,input, andtool_callare defined inline as lambdas inside the factory. Thetool_callhandler alone is ~250 lines. These could be separate named functions that receive a context object with the shared state.
Addressing these is out of scope for this issue (pure refactor) but would be the next step toward a testable, sub-300-line index.ts.
Design Overview
New module layout (implemented)
| New file | Concern | Actual lines |
|---|---|---|
src/active-agent.ts |
Agent name extraction from session metadata and system prompt | 58 |
src/external-directory.ts |
Path normalization, outside-cwd detection, PATH_BEARING_TOOLS, external-directory format helpers |
113 |
src/permission-prompts.ts |
All format* helpers for ask/deny/user-denied prompts |
131 |
src/tool-input-preview.ts |
Text utilities and tool-input formatting for prompts and logs | 206 |
src/subagent-context.ts |
isSubagentExecutionContext, normalizeFilesystemPath |
52 |
src/forwarded-permissions/io.ts |
Atomic JSON write, request/response read, directory ensure/cleanup, error helpers, logger setter | 328 |
src/forwarded-permissions/polling.ts |
waitForForwardedPermissionApproval, processForwardedPermissionRequests, confirmPermission, getSessionId, getContextSystemPrompt, formatForwardedPermissionPrompt, PermissionForwardingDeps |
334 |
src/index.ts retains (~970 lines):
- Imports from the new modules and existing ones.
- Module-scope logging state and helpers (
extensionConfig,extensionLogger,writeDebugLog,writeReviewLog, etc.). - Module-scope constants (
PI_AGENT_DIR,SESSIONS_DIR, etc.). - Small helpers tightly coupled to module state (
extractSkillNameFromInput,getEventToolName,getEventInput,canRequestPermissionConfirmation,derivePiProjectPaths,createPermissionManagerForCwd). piPermissionSystemExtensionfactory (the default export) with closure-scoped helpers and six event handlers.
Module dependency direction
index.ts
├── active-agent.ts
├── external-directory.ts
├── permission-prompts.ts
│ └── tool-input-preview.ts
├── subagent-context.ts
└── forwarded-permissions/
├── io.ts
└── polling.ts (imports io.ts, active-agent.ts, subagent-context.ts)
No new module imports from index.ts — dependency flows one way (index → modules).
Dependency injection patterns used
isSubagentExecutionContext(ctx, subagentSessionsDir)— takes the directory as a parameter instead of reading a module-scope constant.getToolInputPreviewForLog(result, input, pathBearingTools)/getPermissionLogContext(result, input, pathBearingTools)— receive thePATH_BEARING_TOOLSset as a parameter.ensurePermissionForwardingLocation(forwardingDir, sessionId)/getExistingPermissionForwardingLocation(forwardingDir, sessionId)— receive the forwarding directory as a parameter.setForwardedPermissionLogger({ writeReviewLog, writeDebugLog })— setter injection for the IO module's logger.PermissionForwardingDeps— context object passed toconfirmPermission,processForwardedPermissionRequests, andwaitForForwardedPermissionApprovalcarryingforwardingDir,subagentSessionsDir,writeReviewLog,requestPermissionDecisionFromUi, andshouldAutoApprove.
Testing strategy: dependency injection and vitest mocks
Every module has collaborators — other modules it imports and calls.
Unit tests should verify each module in isolation by mocking its collaborators with vi.mock() and, where needed, vi.fn() / vi.spyOn().
This ensures tests exercise the module's own logic and boundary conditions without coupling to the real behavior of dependencies.
Guiding principles:
- Mock collaborators, not the module under test.
If
permission-prompts.tsimportsformatToolInputForPromptfromtool-input-preview.ts, the permission-prompts tests mocktool-input-preview.tsand verify that the prompts module calls it with the right arguments and uses its return value correctly. - Use
vi.mock()for module-level imports. Vitest hoistsvi.mock()calls so the module under test receives mocked versions of its dependencies at import time. - Use
vi.fn()for injected function dependencies. When a function takes a callback or deps object (e.g.,PermissionForwardingDeps), passvi.fn()stubs and assert they were called correctly. - Mock
ExtensionContextas a plain object. The PiExtensionContextis an interface — tests construct minimal objects satisfying only the properties the module actually reads (e.g.,{ sessionManager: { getEntries: vi.fn() } }). - Mock filesystem operations.
Modules that use
node:fs(forwarded-permissions/io.ts) should havenode:fsmocked viavi.mock("node:fs")so tests never touch the real filesystem. - Restore mocks between tests.
Use
afterEach(() => { vi.restoreAllMocks(); })to prevent test pollution.
Example pattern for a module with collaborators:
import { describe, expect, test, vi, afterEach } from "vitest";
// Mock the collaborator module before importing the module under test.
vi.mock("../tool-input-preview.js", () => ({
formatToolInputForPrompt: vi.fn(() => "mocked preview"),
}));
import { formatAskPrompt } from "../src/permission-prompts.js";
import { formatToolInputForPrompt } from "../src/tool-input-preview.js";
afterEach(() => {
vi.restoreAllMocks();
});
describe("formatAskPrompt", () => {
test("includes tool input preview for non-bash tools", () => {
const result = formatAskPrompt(
{ toolName: "read", state: "ask", source: "tool" },
"my-agent",
{ path: "/foo" },
);
expect(formatToolInputForPrompt).toHaveBeenCalledWith("read", { path: "/foo" });
expect(result).toContain("mocked preview");
});
});
Export strategy
Each new module exports only the functions and constants that index.ts (or sibling modules) actually reference.
PermissionReviewSource stays in index.ts as it is only used there.
Module-scope constant rule
Per AGENTS.md, getAgentDir() must not be cached at module scope.
The extracted modules receive directory values as parameters; index.ts calls getAgentDir() at invocation time inside closures (no change from current behavior).
Constants like ACTIVE_AGENT_TAG_REGEX, PATH_BEARING_TOOLS, and length limits are safe at module scope since they do not depend on the environment.
Module-Level Changes
Added
| File | Content |
|---|---|
src/active-agent.ts |
ACTIVE_AGENT_TAG_REGEX, normalizeAgentName, getActiveAgentName, getActiveAgentNameFromSystemPrompt |
src/external-directory.ts |
PATH_BEARING_TOOLS, normalizePathForComparison, isPathWithinDirectory, getPathBearingToolPath, isPathOutsideWorkingDirectory, formatExternalDirectoryHardStopHint, formatExternalDirectoryAskPrompt, formatExternalDirectoryDenyReason, formatExternalDirectoryUserDeniedReason |
src/permission-prompts.ts |
formatMissingToolNameReason, formatUnknownToolReason, formatPermissionHardStopHint, formatDenyReason, formatUserDeniedReason, formatAskPrompt, formatSkillAskPrompt, formatSkillPathAskPrompt, formatSkillPathDenyReason |
src/tool-input-preview.ts |
truncateInlineText, sanitizeInlineText, countTextLines, formatCount, getPromptPath, formatEditInputForPrompt, formatWriteInputForPrompt, formatReadInputForPrompt, formatSearchInputForPrompt, serializeToolInputPreview, formatJsonInputForPrompt, formatToolInputForPrompt, formatGenericToolInputForLog, getToolInputPreviewForLog, getPermissionLogContext, length constants |
src/subagent-context.ts |
normalizeFilesystemPath, isSubagentExecutionContext |
src/forwarded-permissions/io.ts |
sleep, formatUnknownErrorMessage, isErrnoCode, logPermissionForwardingWarning, logPermissionForwardingError, ensureDirectoryExists, getPermissionForwardingLocationForSession, ensurePermissionForwardingLocation, getExistingPermissionForwardingLocation, tryRemoveDirectoryIfEmpty, cleanupPermissionForwardingLocationIfEmpty, safeDeleteFile, writeJsonFileAtomic, readForwardedPermissionRequest, readForwardedPermissionResponse, listRequestFiles, setForwardedPermissionLogger, ForwardedPermissionLogger |
src/forwarded-permissions/polling.ts |
getSessionId, getContextSystemPrompt, formatForwardedPermissionPrompt, waitForForwardedPermissionApproval, processForwardedPermissionRequests, confirmPermission, PermissionForwardingDeps |
Changed
| File | Change |
|---|---|
src/index.ts |
Removed extracted functions/constants; added imports from new modules; reduced from ~1,973 to ~970 lines |
Unchanged
All pre-existing modules (src/permission-manager.ts, src/bash-filter.ts, src/wildcard-matcher.ts, src/system-prompt-sanitizer.ts, src/skill-prompt-sanitizer.ts, src/extension-config.ts, etc.) and test files are not modified.
TDD Order
Phase 1: Extract modules (steps 1–8, completed)
Since this was a pure mechanical refactor with no behavior change, the cycle was extract → verify → commit. Existing tests passed after each step without modification.
-
✅ Extract
src/active-agent.ts— move agent-name detection functions. Commit:refactor: extract active-agent detection into src/active-agent.ts (#21) -
✅ Extract
src/subagent-context.ts— move subagent detection helpers. Commit:refactor: extract subagent context into src/subagent-context.ts (#21) -
✅ Extract
src/tool-input-preview.ts— move text utilities and tool-input formatters. Commit:refactor: extract tool-input preview into src/tool-input-preview.ts (#21) -
✅ Extract
src/external-directory.ts— move path utilities and external-directory format helpers. Commit:refactor: extract external-directory logic into src/external-directory.ts (#21) -
✅ Extract
src/permission-prompts.ts— move ask/deny/user-denied prompt formatters (importstool-input-preview.ts). Commit:refactor: extract permission prompts into src/permission-prompts.ts (#21) -
✅ Extract
src/forwarded-permissions/io.ts— move file IO, directory management, and error helpers. Commit:refactor: extract forwarded-permission IO into src/forwarded-permissions/io.ts (#21) -
✅ Extract
src/forwarded-permissions/polling.ts— move polling loop andconfirmPermission. Commit:refactor: extract forwarded-permission polling into src/forwarded-permissions/polling.ts (#21) -
✅ Final cleanup — remove dead imports, fix lint warnings. Commit:
refactor: finalize index.ts split (#21)
Phase 2: Add unit tests (steps 9–20)
Each step adds a dedicated test file for a module that currently has no unit tests.
The goal is direct coverage of each module's exported functions in isolation — using vi.mock() to replace collaborator modules and vi.fn() for injected dependencies.
Tests exercise edge cases and boundary conditions that are hard to reach through the end-to-end factory tests.
Every test file should use describe() blocks to group tests by exported function, and afterEach(() => { vi.restoreAllMocks(); }) to prevent cross-test pollution.
-
tests/wildcard-matcher.test.ts— testcompileWildcardPatternEntries,findCompiledWildcardMatch,findCompiledWildcardMatchForNames. No collaborators to mock (pure algorithm). Cover: empty patterns, exact match, glob*matching, last-match-wins precedence, multi-name lookup, no-match returns null. Commit:test: add unit tests for wildcard-matcher (#21) -
tests/common.test.ts— testtoRecord,getNonEmptyString,isPermissionState,extractFrontmatter,parseSimpleYamlMap. No collaborators to mock (pure functions). Cover: non-object inputs totoRecord, whitespace-only strings, all three permission states, malformed frontmatter delimiters, empty YAML map, multi-line values. Commit:test: add unit tests for common (#21) -
tests/bash-filter.test.ts— testBashFilter.check. Mock:vi.mock("./wildcard-matcher.js")to verifyBashFilterdelegates pattern matching to the wildcard-matcher and applies the default fallback correctly. Cover: exact match, glob patterns, last-match-wins, default fallback for unmatched commands, empty command, whitespace normalization. Commit:test: add unit tests for bash-filter (#21) -
tests/yolo-mode.test.ts— testshouldAutoApprovePermissionState,canResolveAskPermissionRequest. No collaborators to mock (pure functions taking config/flags). Cover: yolo on/off × ask/allow/deny, subagent with no UI and yolo off, subagent with no UI and yolo on. Commit:test: add unit tests for yolo-mode (#21) -
tests/tool-input-preview.test.ts— test all exported formatters andgetPermissionLogContext. Mock:vi.mock("./logging.js")sosafeJsonStringifyreturns controlled output — verifies the module delegates serialization to its collaborator and handles the result. Cover: truncation at exact boundary, multi-line content, empty input, edit with multiple replacements, path-bearing vs non-path-bearing tools ingetPermissionLogContext. Commit:test: add unit tests for tool-input-preview (#21) -
tests/external-directory.test.ts— testnormalizePathForComparison,isPathWithinDirectory,isPathOutsideWorkingDirectory,getPathBearingToolPath, and format helpers. Mock:vi.mock("node:os", () => ({ homedir: vi.fn(() => "/mock/home") }))so tilde-expansion tests are deterministic and platform-independent. Cover: tilde expansion, relative paths, path-bearing vs non-path-bearing tools, empty strings, quoted paths,@-prefixed paths, format helpers with/without agent name. Commit:test: add unit tests for external-directory (#21) -
tests/permission-prompts.test.ts— test allformat*exported functions. Mock:vi.mock("./tool-input-preview.js")soformatToolInputForPromptreturns controlled strings — verifiesformatAskPromptcalls the collaborator with the right tool name and input and incorporates the preview into the prompt string. Cover: with/without agent name, MCP target, bash command with/without matched pattern, denial reason, skill path deny/ask. Commit:test: add unit tests for permission-prompts (#21) -
tests/active-agent.test.ts— testnormalizeAgentName,getActiveAgentName,getActiveAgentNameFromSystemPrompt. MockExtensionContextas a plain object:{ sessionManager: { getEntries: vi.fn(() => [...]) } }. Cover: whitespace-only name, null, tag variations in system prompt, missing tag, session entries withactive_agentcustom type, last-entry-wins when multiple entries exist, entry withname: nullresets. Commit:test: add unit tests for active-agent (#21) -
tests/subagent-context.test.ts— testisSubagentExecutionContext,normalizeFilesystemPath. MockExtensionContextas a plain object:{ sessionManager: { getSessionDir: vi.fn() } }. Usevi.stubEnv()/vi.unstubAllEnvs()to controlSUBAGENT_ENV_HINT_KEYSwithout leaking across tests. Cover: env variable detection (each of the 3 hint keys), session dir within/outside subagent root, missing session dir, empty env values. Commit:test: add unit tests for subagent-context (#21) -
tests/tool-registry.test.ts— testcheckRequestedToolRegistration,getToolNameFromValue. Mock:vi.mock("./common.js")to controlgetNonEmptyString/toRecordreturn values — verifies the registry delegates input parsing to its collaborators. Cover: registered tool, unregistered tool, missing tool name, event withinputvsarguments, empty tool list. Commit:test: add unit tests for tool-registry (#21) -
tests/system-prompt-sanitizer.test.ts— testsanitizeAvailableToolsSection. No collaborators to mock (pure string transformation). Cover: removing denied tools, preserving allowed tools, multi-section prompts, missing Available tools section, tool guidance blocks for inactive tools, empty allowed-tools list. Commit:test: add unit tests for system-prompt-sanitizer (#21) -
tests/skill-prompt-sanitizer.test.ts— testresolveSkillPromptEntries,findSkillPathMatch. MockPermissionManageras a plain object:{ checkPermission: vi.fn() }— verifies the sanitizer delegates permission checks to the manager and uses the returned state to decide whether to strip, keep, or mark skill blocks. Cover: skill allow/deny/ask, path matching within/outside skill directories, multi-skill prompts, no skill blocks in prompt. Commit:test: add unit tests for skill-prompt-sanitizer (#21)
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Could this silently weaken a permission? | No — pure refactor moves functions without changing logic. Every step verifies npm test passes, and the test suite covers tool/bash/mcp/skill/special/external-directory permission decisions. |
| Circular dependency between new modules | Dependency flows one way (index → modules → shared types). No module imports from index.ts. tool-input-preview.ts is imported by permission-prompts.ts only. |
Module-scope caching of getAgentDir() |
Extracted modules receive directory paths as parameters. getAgentDir() is called only in index.ts closures at invocation time, matching the existing pattern and the AGENTS.md rule. Note: index.ts itself still caches PI_AGENT_DIR at module scope — this is a pre-existing violation, not introduced by this change. |
| Import path breaks in tests | Only two test files import from index.ts (tests/permission-system.test.ts, tests/session-start.test.ts), both importing only the default export piPermissionSystemExtension, which remains in index.ts. |
| Forwarded-permission closures depend on logger state | forwarded-permissions/io.ts uses a setter-injected logger (setForwardedPermissionLogger). The setter must be called before any IO function that logs. This is wired up in the factory before refreshExtensionConfig(). |
| Mocked unit tests could drift from real module contracts | Unit tests verify each module's contract with its collaborators (correct arguments passed, return values used). Integration tests in permission-system.test.ts continue to verify end-to-end wiring with real collaborators. Both must pass — mocks catch contract violations early, integration tests catch wiring mistakes. |
| Over-mocking hides real bugs | Mock only direct collaborators (one level deep). Never mock the module under test. If a test needs to mock more than 2–3 collaborators, that is a signal the module has too many responsibilities and should be split further. |
Open Questions
- Should
PermissionReviewSourcemove tosrc/types.ts? Currently only used inindex.ts. Defer until a second module needs it. - Should
extractSkillNameFromInputmove tosrc/skill-prompt-sanitizer.ts? It's closely related but currently only called in theinputevent handler. Defer to keep this change mechanical. - Should
permission-manager.ts(941 lines) get its own unit test file? It has complex logic (MCP target resolution, policy merge, caching) that would benefit from direct tests withvi.mock("node:fs")to control config file reads. Deferred — it would be a large effort and the integration tests cover the main paths. Consider as a separate issue. - Should the module-scope
PI_AGENT_DIRconstants be moved inside the factory? This would fix the AGENTS.md rule violation but requires threading the values through more call sites. Consider as part of a future factory restructuring issue. - Should
forwarded-permissions/io.tsreplace setter injection with parameter injection? The currentsetForwardedPermissionLoggerpattern creates temporal coupling. An alternative is to pass the logger as a parameter to each function that logs (matching thePermissionForwardingDepspattern inpolling.ts). This would make the module fully stateless and easier to test without calling a setter first. Consider for a future cleanup.