8.6 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 507 | fix(pi-permission-system): external-directory prompt shows the typed path, not the resolved path that triggered the gate |
Retro: #507 — external-directory prompt shows the typed path, not the resolved path that triggered the gate
Stage: Planning (2026-06-30T00:00:00Z)
Session summary
Planned a message-clarity fix that discloses the resolved (canonical, symlink-followed) path in the external-directory prompt and denial messages as (resolves to '<canonical>'), shown only when it differs from the typed path.
The plan adds an AccessPath.resolvedAlias() accessor as the single home for the lexical-vs-canonical comparison, a shared resolvesToSuffix helper plus ExternalPathDisclosure type in denial-messages.ts, and threads the resolved form through both gates into all external-directory message variants.
Filed as packages/pi-permission-system/docs/plans/0507-external-directory-resolved-path-disclosure.md; four TDD cycles, Release: ship independently.
Observations
- Scope decision (via
ask_user): the operator chose to cover all external-directory message variants (ask + deny + no-UI + user-denied), not just the four call sites the issue named — thebuildUnavailableBodyexternal_directory body carries the identical "inside path called outside working directory" contradiction, and onceresolvedPathis inDenialContextthe marginal cost is ~2 lines each. - The comparison must be
value()(lexical absolute) vsboundaryValue()(canonical absolute), not the raw typed relative string vs canonical — the typed string is relative and would always differ. Encapsulated onAccessPathasresolvedAlias(). - Both lexical and canonical forms are win32-lowercased (
path-normalization.ts), so a case-only Windows difference yields no spurious disclosure;canonicalizePathreturns its input unchanged for non-symlink/unresolvable paths, soresolvedAlias()isundefinedexactly when there is no distinct target. - Non-breaking: message builders and
DenialContextare internal (not exported fromindex.ts); gating decisions, review-log values, and session-approval patterns are unchanged. Kept all commitsfix:(including the internalresolvedAlias()enabler) so the issue ships as one patch, not a minor bump. - Type coupling drove the cycle boundaries: changing
DenialContext.bash_external_directory.externalPathstoExternalPathDisclosure[]breaks its sole producer + consumer + inline test constructions together, so the bash prompt/denial/gate/tests land in one commit. - Not a roadmap step (Phase 7 complete); surfaced while investigating #493 (closed — the bypass concern was already handled by dual-match).
Stage: Implementation — TDD (2026-06-30T20:36:00Z)
Session summary
Implemented all four TDD cycles from the plan: added AccessPath.resolvedAlias(), threaded it through the tool external_directory gate and all three denial-message bodies, then through the bash external_directory gate and its bash-list rendering, and updated architecture.md.
Test count went from 2194 to 2207 (+13) in pi-permission-system; full monorepo suite, pnpm run check, pnpm run lint, and pnpm fallow dead-code all green.
Pre-completion reviewer returned PASS.
Observations
- Caught and corrected a self-inflicted step-boundary coupling during implementation: my first edit to
external-directory-messages.tschanged bothformatExternalDirectoryAskPrompt(Step 2, tool) andformatBashExternalDirectoryAskPrompt(Step 3, bash) in one block, since they're colocated in the same file. Reverted the bash function's signature change and its downstreamDenialContext.bash_external_directory/ test literal changes back tostring[]before committing Step 2, then reapplied them cleanly in Step 3 — keeping each commit's diff scoped to its own TDD cycle per the plan's step boundaries. MD053(rumdl) flagged[#507]as an unused link reference after adding the[#507]:definition per the plan: the twoarchitecture.mdmentions are inside a fenced```textcode block (the module-layout tree listing, opened at line 648), where reference-style links don't count toward MD053 usage. Matched the file's existing convention for issues cited only within that fence (bare#476/#477/#486-style, no brackets) instead of adding a body-prose usage just to satisfy the linter — dropped the[#507]:definition. Deviation flagged by the pre-completion reviewer as cosmetic/non-blocking (PASS).- The
bash-external-directory.tsgate correctly keeps two parallel path lists:uncoveredPaths(string values) forderiveApprovalPattern/logContext.externalPaths(unchanged, invariant-critical), and a newdisclosuresarray ({ path, resolvedPath }) for the prompt/denial text only — confirmed by the reviewer as the correct separation. - Reviewer noted one test-location deviation from the plan: the bash prompt's disclosure test landed in
test/bash-external-directory.test.ts(alongside the existingformatBashExternalDirectoryAskPromptdescribe block) rather than intest/handlers/gates/external-directory-messages.test.ts; coverage is complete either way, treated as non-blocking. - An unrelated commit (
chore: upgrade to Sonnet 5) landed onmainfrom outside this session immediately after the docs commit — noted for the record, not part of this issue's changeset. - Pre-completion reviewer: PASS. All deterministic checks green; cross-step invariants (#418/#486 dual-match protection, boundary/approval/log values) confirmed untouched — only message text changed.
Stage: Final Retrospective (2026-07-01T00:59:26Z)
Session summary
One session carried #507 through its full lifecycle — plan, four TDD cycles, ship — landing pi-permission-system-v18.0.2 (three fix: commits plus a docs: architecture update).
The same session first ran /plan-issue #487 and, on finding that #487's vision was already delivered by Phase 7 + #486, closed it as completed instead of fabricating a plan.
Execution was clean: two minor, self-caught friction points and no post-ship rework.
Observations
What went well
- Incremental verification throughout TDD: each cycle ran its affected test file for red/green, then
pnpm run checkimmediately whenever a shared type changed (DenialContext, the prompt signatures), then the full package suite before committing — the shared-type break in step 3 surfaced atchecktime, not end-of-session. /plan-issue #487correctly produced a non-plan outcome: theDecidegate treated the "Proposed change" as a hypothesis, investigation showed the three scope items were already delivered or declared non-goals, and anask_userconfirmed closing over fabricating a plan for done work.- Step-boundary discipline held under a colocation hazard:
formatExternalDirectoryAskPrompt(step 2) andformatBashExternalDirectoryAskPrompt(step 3) shareexternal-directory-messages.ts, and the accidental joint edit was reverted and re-split so each commit's diff matched its TDD cycle.
What caused friction (agent side)
missing-context— the plan prescribed adding a[#507]:reference-link definition toarchitecture.md, but both mentions live inside the```textmodule-layout tree fence, where[#N]is not a live reference, soMD053rejected the orphaned definition. Self-identified via the lint gate; resolved in ~3 tool calls by switching to bare#507(matching the fence's existing#476/#486entries) and dropping the definition. Impact: minor in-session rework, no follow-up commit; root cause is a plan-time gap themarkdown-conventionsskill did not yet cover.scope-drift— the first edit toexternal-directory-messages.tschanged both the tool (step 2) and bash (step 3) prompt functions in one block. Self-identified before committing; reverted the bash signature and its downstreamDenialContext/ test changes, then reapplied them in step 3. Impact: added friction but no rework past the revert; no follow-up commit.
What caused friction (user side)
- None.
The two
ask_userdecisions (#507 message scope → all variants; #487 → close as completed) were answered decisively and early, which kept both the plan'sModule-Level Changesand the #487 outcome unambiguous from the start.
Changes made
.pi/skills/markdown-conventions/SKILL.md— added a one-sentence caveat to the "Issue references" section: a[#N]inside a fenced code block (e.g. thearchitecture.mdmodule-layout tree) is not a live reference, so cite issues there as bare#Nwith no[#N]:definition, or MD053 rejects the orphaned definition.