8.1 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 502 | pi-permission-system: migrate the per-tool path-bearing gate onto AccessPath (Phase 7 Step 1) |
Retro: #502 — Migrate the per-tool path-bearing gate onto AccessPath (Phase 7 Step 1)
Stage: Planning (2026-06-29T00:00:00Z)
Session summary
Planned Phase 7 Step 1: route the per-tool path-bearing gate (read/write/edit/grep/find/ls) onto AccessPath so per-tool rules match lexical ∪ canonical, closing the symlink-evasion asymmetry against the path surface.
The change is mechanically parallel to [#486] (the path-surface migration): the resolver already unwraps access-path → path-values and PATH_SURFACES already routes the per-tool surfaces through evaluateAnyValue, so the only behavior change is the canonical alias joining the match set.
Produced a three-step plan (breaking feat!: behavior change, a refactor: accessor removal, then docs) at docs/plans/0502-per-tool-gate-access-path.md.
Observations
getPlatform()removal is forced, not optional, and resolves [#513]. [#511] is already merged, so [#502] is the second of the twogetPlatform()consumers to fold. Once the per-tool gate stops readingplatform(it threaded it only to feedderiveSuggestionValue'snormalizePathForComparison), the pipeline'sgetPlatform()read is dead and the accessor would trip thepnpm fallow dead-codeCI gate. So Step 2 removesgetPlatform()fromToolCallGateInputs+PermissionSession+makeGateInputs, and [#513] should be closed at ship with a pointer to the [#502] SHA.- Scope discriminator: keyed the
access-pathbranch offgetPathBearingToolPath(...) !== null(built-in six with a presentinput.path). This deliberately keeps the missing-path case on thetoolintent so thenormalizeInput["*"]fallback is preserved, and keeps MCP/extension tools ontool(their path is already symlink-resistant via the cross-cuttingpathgate since [#486]). - Suggestion value is provably unchanged:
accessPath.value()equals the oldnormalizePathForComparison(path, tcc.cwd, platform)because the pipeline normalizer is built from the same sessioncwd+platform. Flagged the two [#438] cwd-bounding tests as the invariants to keep green (now passing an injectedAccessPathbuilt vianew PathNormalizer(...)). - Structural win: the change removes a parameter relay —
platformthreaded session → pipeline →describeToolGatesolely to feed one derivation theAccessPaththe gate already builds now owns (Tell-Don't-Ask viavalue()). - Release: Step 1 of batch "symlink-resistant-path-matching" (tail = Step 3, [#504]); mid-batch → defer.
The breaking
feat!:lands onmainand auto-batches; the major-bump release cuts when Step 3 lands. - Skipped the
ask_usergate: operator-authored issue, unambiguous proposal, and the only scope addition (getPlatform()removal) is forced by the dead-code gate + [#513], not a genuine design choice.
Stage: Implementation — TDD (2026-06-29T10:15:00Z)
Session summary
Implemented all three planned TDD steps: the breaking feat!: per-tool gate migration to access-path, the refactor: removal of the dead getPlatform() accessor (resolving [#513]), and the docs: updates marking Phase 7 Step 1 complete.
Four new tests added (suite 2211 → 2215); full suite, tsc, root lint, and pnpm fallow dead-code all green.
Pre-completion reviewer returned PASS.
Observations
- One unplanned deviation — a stale fallow suppression.
Adding the explicit
bashProgram: BashProgram | nullannotation in the newresolvePerToolCheckhelper gave fallow a resolvable receiver forBashProgram.commands(), which retired the long-standingunused-class-memberfalse-positive suppression inprogram.ts. Removed it as a focused fourthrefactor:commit (not in the plan; the plan only listed the touched files). This is whyfallow dead-codemust be run — the baseline-green checks (check/lint/test) do not catch a now-stale suppression. describeToolGatesignature change was structurally improved, not just mechanical. Swapping theplatform: NodeJS.Platformparameter for an optionalaccessPath?: AccessPathremoved a three-layer parameter relay (Tell-Don't-Ask viavalue()), exactly as the plan predicted.- The
tool.test.tsred was weak; the pipeline test carried the real red. Passing anAccessPathwhere the old code expected aplatformstring coincidentally behaved like posix (an object!== "win32"), sotool.test.tspassed against old code. The meaningful behavioral red (per-tool gate emitsaccess-path; symlink-canonical match blocks) lived intool-call-gate-pipeline.test.ts, which used the establishednode:fsrealpathSyncmock frompath.test.ts. - Pre-completion reviewer: PASS — no warnings; all cross-step invariants ([#486], [#438], [#510], missing-path fallback) verified preserved by their pinning tests.
- Remaining for ship: close [#513] with a pointer to the [#502] SHA (its
getPlatform()removal is folded into this work); confirm the mid-batch release deferral (batch "symlink-resistant-path-matching", tail = Step 3 [#504]).
Stage: Final Retrospective (2026-06-29T14:26:40Z)
Session summary
Shipped Phase 7 Step 1 across plan → TDD → ship in one continuous session: a breaking feat!: per-tool gate migration to access-path, the folded-in [#513] getPlatform() removal, an unplanned stale-suppression cleanup, and docs.
The operator deferred the release (mid-batch); commits landed on main, CI passed, and [#513] was closed with a pointer to the [#502] SHA while [#502] stays open until the batch tail (Step 3, [#504]) ships.
A notably clean run — no rework, no rabbit-holes, pre-completion PASS.
Observations
What went well
- Reading the [#486] plan as a template made planning fast and accurate.
[#502] was "mechanically parallel to [#486]", so loading the prior plan and the already-migrated
path.ts/path.test.tsgave a ready-made design (theaccess-pathintent shape, thenode:fsrealpathSyncmock convention) and a correct prediction of every invariant at risk. - Verifying related-issue state caught the [#513] fold-in.
Checking that [#511] was already
CLOSEDmade [#502] the secondgetPlatform()consumer to fold, so the dead-code gate forced the accessor removal into this issue — a scope point that would have surfaced as a CI failure if planned around instead. - The fallow gate did its job.
The post-step
pnpm fallow dead-coderun flagged a now-stale suppression that the baseline check/lint/test triad cannot see; one focusedrefactor:commit cleared it.
What caused friction (agent side)
other(weak red) — the Step 1tool.test.tschanges passed against the olddescribeToolGate: anAccessPathpassed where the old signature expected aplatformstring flowed through esbuild untypechecked and coincidentally behaved like posix (an object!== "win32"), so the unit test gave a hollow red. Impact: no rework — the genuine behavioral red lived intool-call-gate-pipeline.test.ts(access-path emission + symlink-canonical block), which was noticed and relied on at the time. But thetool.test.tsred phase was not truly validating the change.missing-context(minor) — the plan's Module-Level Changes did not anticipate that adding thebashProgram: BashProgram | nullannotation inresolvePerToolCheckwould give fallow a resolvable receiver and retire theprogram.tssuppression. Impact: one extra unplannedrefactor:commit; self-identified by the gate, no rework.
What caused friction (user side)
- None.
The single decision point (mid-batch release deferral) was surfaced early from the plan's
**Release:**marker viaask_userand answered cleanly — the intended handshake.
Changes made
.pi/skills/testing/SKILL.md— added a rule under### Interface and type changes: a TDD step that changes a parameter's type can produce a hollow red (esbuild does not typecheck, so the new-typed argument may coincidentally satisfy the old runtime path); confirm the red exercises the new behavior, not just the new signature.