10 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 508 | fix: bash external_directory gate misses Windows drive-letter absolute paths |
Retro: #508 — fix: bash external_directory gate misses Windows drive-letter absolute paths
Stage: Planning (2026-06-27T00:00:00Z)
Session summary
Planned the fix for the bash external_directory gate missing Windows drive-letter absolute paths (C:/… and C:\…).
The operator's questions drove the scope progressively deeper: from "which classifier(s)" → "detect the platform?"
→ "what makes this change easy (Kent Beck)?"
→ "is there an architectural root cause?".
The investigation surfaced that the package has a half-threaded, ambient platform dependency (process.platform and the host-bound node:path import read in ~6 interior modules), which is the real root cause behind the recurring Windows-path bugs.
Filed that as a separate architecture issue, #510, and rebased #508's plan to land on #510's clean seam.
Observations
- Scope decisions (via
ask_user). Confirmed: recognize both separator forms in both classifiers (not just the strict one); detect drive paths by a drive-letter prefix regex (/^[a-zA-Z]:[/\\]/), not "any backslash"; keep gating drive-shaped tokens on POSIX (do not warn/drop — dropping would reduce POSIXpath-surface coverage of the real in-CWD./C:/fooaccess). - No
process.platformbranch. The platform-sensitive decision ("isC:/fooabsolute?") already lives innode:path(path.win32vspath.posix). Shape recognition is platform-independent string matching; absoluteness delegates to the injected flavor. This satisfies the package's owncode-designrule against readingprocess.platformin utility functions. - Architectural root cause → #510.
path-utils.tsisPathWithinDirectoryandrule.tsalready use an injectableplatform: NodeJS.Platform = process.platformparameter (with the testability rationale in the doc comment), but the seam was never threaded end-to-end:normalizePathForComparisonhard-codesprocess.platforminline,canonicalizePath/AccessPath.forPathtake no flavor, andcwd-projection.tsimports host-boundnode:pathand hand-rollsstartsWith("/"). Threading an injectedPathSemantics/platform flavor through the pipeline is the structural correction that makes #382/#345/#418/#508 and future Windows forms (UNC,\\?\, drive-relative) a single-home change. - Sequencing (operator choice).
Refactor first (#510), then rebase #508 onto the clean seam.
On that seam #508 collapses to: add the drive-letter shape to the classifiers; the
isRelativeCandidate→ injectedisAbsoluteconversion and the win32 testability both belong to #510 (novi.mock("node:path")). - Tidy-first insight.
The naive classifier-only fix would briefly over-flag an inside-CWD drive path under an unknown
cdbase (a Windows-absolute path mislabeled "relative" by the hand-rolled check). #510'sisAbsolutedelegation eliminates that window, so #508 needs no transient cleanup. #509is the sibling issue (bare-filenamepath-surface bypass) split from the same parent #494 — explicitly out of scope here.
Stage: Planning refresh — post-#510/#511 (2026-06-28T00:00:00Z)
Session summary
510 and #511 are closed; the platform/path-semantics seam landed as PathNormalizer (src/path-normalizer.ts), and the old cwd-projection.ts is now the class-based bash-path-resolver.ts holding this.normalizer
Refreshed the #508 plan from its abstract "injected flavor" wording to the concrete API and confirmed the scope reduction the plan anticipated.
Observations
- #510 deferred the
isRelativeCandidateconversion, exactly the contingency the original plan flagged.foldCdalready delegates tothis.normalizer.isAbsolute, but the module-level free functionisRelativeCandidate(bash-path-resolver.ts, two call sites:projectExternalPaths,buildRuleCandidatePath) still hand-rolls!startsWith("/") && !startsWith("~"). #508 folds in converting it to a private method delegating tothis.normalizer.isAbsolute— load-bearing, since the projection's relative/unknown decision (not justfoldCd) gates the over-flag. - Testability is clean as predicted.
BashProgram.parse(command, normalizer)andextractExternalPathsFromBashCommand(command, normalizer)take aPathNormalizer;bash-external-directory.test.tsalready buildsnew PathNormalizer(process.platform, cwd). #508's Windows assertions constructnew PathNormalizer("win32", cwd)— novi.mock("node:path"). - #508 now reduces to two production edits + docs: drive-shape recognition in both classifiers (
token-classification.ts, unchanged by #510) and theisRelativeCandidateconversion (bash-path-resolver.ts). Singlefix:commit +docs:commit. - Next step:
/tdd-plan.
Stage: Implementation — TDD (2026-06-28T20:15:00Z)
Session summary
Completed 2 TDD cycles in a single session: (1) the core fix: commit adding WINDOWS_DRIVE_PATH_PATTERN to both classifiers and converting isRelativeCandidate to a private method delegating to this.normalizer.isAbsolute; (2) the docs: commit updating architecture.md and SKILL.md.
Test count went from 2195 to 2211 (+16 tests: 8 new classifier unit tests and 4 new win32 end-to-end assertions across 2 test files).
Pre-completion reviewer returned PASS.
Observations
isRelativeCandidateedit required three partial attempts due to the decorator rule line (──) inbash-path-resolver.ts— the Unicode box-drawing characters miscount when anchoringoldText, causing atomic batch rejections. Resolved by anchoring on adjacent unique code lines rather than the rule itself (as AGENTS.md warns), and by splitting the class-closing brace into a separate edit from the rule line.- The unknown-base win32 test passed green before the implementation (tokens were dropped → empty result), so it was not a red test in the strict sense.
After the fix it stays green for the correct reason (inside-CWD check fires).
This is expected for the "over-flag prevention" path — the red test for the over-flag scenario would require a Windows absolute path that is outside CWD under an unknown base, but the plan test chose inside-CWD to exercise the conversion specifically.
No deviation: the test does cover the
isRelativeCandidaterouting change as documented in the test comment. c://xURL rejection confirmed: theURL_PATTERN(/^[a-z][a-z0-9+.-]*:\/\//i) fires before the drive pattern for single-letter schemes with//, so there is no ambiguity between the URL guard and the new drive pattern.- Pre-completion reviewer verdict: PASS. All deterministic checks, invariants, and doc surfaces clean.
Stage: Final Retrospective (2026-06-29T01:00:00Z)
Session summary
Shipped #508 cleanly: pushed the two commits, CI green, closed the issue, and merged release-please PR #514 to cut pi-permission-system-v17.1.1.
This closes a four-stage arc (Planning → Planning refresh → TDD → Ship) in which operator-driven Socratic questioning during planning turned a one-line hand-rolled fix into a clean fix layered on a properly-threaded PathNormalizer seam (#510), with #511 as a follow-up.
Observations
What went well
- Operator-driven architectural depth (planning).
The progressive questioning — "which classifier?"
→ "detect the platform?"
→ "what makes this change easy?"
→ "is there an architectural root cause?"
— surfaced the half-threaded ambient-platform dependency and produced #510 / #511.
Without it, #508 would have shipped a hand-rolled
startsWith("/")check that drifts fromnode:pathand a fragilevi.mock("node:path")test. Strategic judgment applied at exactly the right moment. - Plan contingency foresight paid off.
The original plan flagged "if #510 defers the
isRelativeCandidateconversion, fold it into #508." #510 did defer it; the Planning-refresh stage confirmed the contingency and folded it in with zero rework. - Ship-flow
IN_PROGRESS-check handling worked as designed. Release-please PR #514 returnedUNSTABLEwith a CI check stillIN_PROGRESS. The session polledstatusCheckRollupuntilSUCCESSand then merged viarelease_pr_merge, rather than falling back togh pr mergewhile a check was running — exactly the/ship-issuestep-6.4 refinement. The refined instruction prevented a premature merge. - Clean TDD execution. Two cycles, +16 tests, pre-completion PASS, no follow-up fixups.
What caused friction (agent side)
other— theisRelativeCandidateedit needed three attempts becauseoldTextspanned the──decorator rule line inbash-path-resolver.ts; the Unicode box-drawing run miscounts and rejected the atomic batch. Impact: two extraEditcalls, no rework — resolved by anchoring on adjacent code lines, which is exactly what AGENTS.md already prescribes. The rule exists and is crisp; this was a first-attempt-application slip, not a missing rule.other— the unknown-base win32 test passed green before implementation (the token was dropped upstream → empty result), so it was not a strict red test for the over-flag path. Impact: none — documented in the test comment and the TDD stage notes; a true red test for over-flag would need an outside-CWD drive path under an unknown base. This is a manifestation of the existing package-SKILL rule ("trace the token through the classifier first"), already covered.
What caused friction (user side)
- None.
Operator involvement was strategic (planning depth, sequencing choice, scope confirmation via
ask_user), not mechanical oversight.
Considered but not proposed
- No
AGENTS.mdchange for the decorator-line edit trap — the rule already exists and is crisp; adding salience text to an already-dense file would not improve first-attempt recall. - No
testing-skill change for the green-before-red test — the package SKILL's "trace the token through the classifier first" already covers the underlying principle. - No
/ship-issuechange — theIN_PROGRESSrefinement worked; nothing to add.
Changes made
- Added this Final Retrospective stage entry to
packages/pi-permission-system/docs/retro/0508-bash-external-directory-windows-drive-letter.md. - No
AGENTS.md, prompt, or skill changes — operator confirmed retro-only; existing rules covered every friction point.