11 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 307 | Project a running effective working directory across cd's onto bash path candidates |
Retro: #307 — Project a running effective working directory across cd's onto bash path candidates
Stage: Planning (2026-06-01T00:00:00Z)
Session summary
Produced the implementation plan for projecting a stateful effective working directory onto bash external-directory path candidates, retiring the single leadingCdTarget model.
The plan lands in three feat: TDD steps (Tier 1 sequential current-shell cd fold, Tier 2 subshell / brace-group / pipeline / background scoping, conservative unknown-base bail) plus a docs step.
Two disposable web-tree-sitter AST probes de-risked every descent decision before any plan text was committed, per the #306 / #308 retro lesson.
Observations
- Key load-bearing insight: the strict
classifyTokenAsPathCandidateonly admits absolute,~/, and..-containing tokens, and absolute /~/tokens are base-independent. So the effective-cwd projection only ever changes resolution of..-relative candidates, andpathTokens()(which never resolves against a base) is provably unaffected. This narrowed the whole behavior surface — and the test surface — dramatically. - Two genuine design decisions were surfaced via
ask_userand answered by the owner: scope = Tier 1 + Tier 2 together (not Tier 1 only), and unknown-base policy = conservative (flag relative candidates) rather than today's fall-back-to-cwd. Both choices push toward least-privilege and shaped the plan structure. - Deliberate deviation from the #308 forward note: that plan speculated #307 would add
pathCandidates/effectiveCwdfields toBashCommand. The plan instead keeps the path-candidate walk as its own derivation of the shared single parse, because the cwd-frame grouping descends into brace groups and substitution interiors and foldscdstate, whereascommands()emits brace groups whole and nested commands as separate rule units — different descent semantics that would force a discriminator (the wrong abstraction). This still honors #308's one-parse anti-drift goal. - AST probe findings that shaped the walk:
listnests left-associatively (must recurse children in source order), and background&is an anonymous operator token after the command it backgrounds (distinguishable from&&/||/;for the fold guard). - The escape-to-
cwdfallback incomputeEffectiveResolveBaseis dropped in favor of faithful tracking; this is what closes the missed-escape example (cd nested/deep && cd .. && cat ../../etc/passwd). Twoleading cd prefixcharacterization tests assert the retired model in their comments but pass by coincidence on looselength > 0assertions — the plan re-frames and strengthens them in step 1. - All changes are private to
bash-program.tsplus its two test files and one architecture-doc line; no gate signatures, facades, config, or schema change.
Stage: Implementation — TDD (2026-06-01T21:40:00Z)
Session summary
Implemented all four plan steps in four commits (three feat: + one docs:): Tier 1 sequential current-shell cd fold, Tier 2 subshell frame / brace-group persistence, conservative unknown-base bail, and the architecture-doc update.
The package suite grew from 1716 to 1731 tests (+15: 14 new externalPaths projection cases plus 2 re-framed bash-external-directory.test.ts cases, minus a couple folded names).
Pre-completion reviewer verdict: PASS (one non-blocking WARN).
Observations
- Two deliberate deviations from the plan, both sanctioned and confirmed by the reviewer:
- Command/process-substitution interiors do NOT fold their internal
cds (they inherit the enclosing base) — the explicit fallback offered by the plan's Open Question. Subshell( … )(frame stack) and brace-group{ … }(persistence) ARE implemented because they are statement-level nodes the walk reaches directly; substitution interiors are collected insidecollectCommandTokens(flat), so folding them would require refactoring the leaf collectors to emitPathCandidate[]. The deferral is conservative (over-flags, never under-flags) and is documented with a code comment and theconservatively flags a relative path inside a command substitutiontest. - The plan said to keep
// fallow-ignore-next-line unused-class-memberonpathTokens/externalPaths; in reality onlycommands()ever carried that suppression, so none was added andpnpm fallow dead-codestays clean.
- Command/process-substitution interiors do NOT fold their internal
- The load-bearing planning insight held up exactly: because the strict classifier only admits absolute /
~//..tokens, almost every step-3 unknown-base test passed under step-1 behavior already (a non-literalcdleft the base atcwd, which resolves escaping relatives the same way). Only the within-cwd relative case (cd "$DIR" && cat src/../within.txt) genuinely required theunknownvariant — it was the single Red in step 3. - The two re-framed
bash-external-directory.test.tscharacterization tests passed by coincidence on looselength > 0assertions; strengthening them to exact resolved paths (/projects/outside.txt,/etc/hosts+/tmp) turned the coincidence into documentation of the faithful-tracking behavior. - Two minor lint nits during step 1 (
@typescript-eslint/prefer-optional-chainonnext !== null && !next.isNamedand on!child || !child.isNamed) — resolved by an early-return guard and!child?.isNamedrespectively. - Reviewer warnings: WARN —
bash-program.tsis now ~975 lines and carries two descent strategies (base-threadingwalkForCandidatesand flatcollectPathCandidateTokens) that share leaf collectors;collectPathCandidateTokensis dual-used (subordinate helper inside leaf collectors AND thedefault:branch strategy). Accurate but mitigated by JSDoc; no structural change required. A future cleanup could fold substitution-internal scoping in and unify the two walks (the plan's Open Question convergence).
Stage: Final Retrospective (2026-06-02T00:00:00Z)
Session summary
Across planning → TDD → ship, issue #307 landed in four commits and released as pi-permission-system v9.2.0 via release-please PR #311.
The plan's load-bearing insight — that the strict classifyTokenAsPathCandidate only resolves ..-relative tokens against a base — made the TDD steps nearly surprise-free, and the plan's pre-authorized Open Question fallback let me defer substitution-internal cd folding without a new design question.
CI passed on the first push; the only agent-side friction was a recurring heredoc slip and two trivial lint/portability nits, none causing rework.
Observations
What went well
- Planning pinned the true behavior surface, which front-loaded the surprises: because the strict classifier only resolves
..-relative candidates against a base, almost every step-3 unknown-base test already passed under step-1 behavior, and only the within-cwd relative case (cd "$DIR" && cat src/../within.txt) genuinely needed theunknownvariant. A plan that identifies the real behavior surface shrinks the test surface and makes each Red predictable. - The plan's Open Question pre-authorized deferring substitution-internal
cdfolding; when implementation hit the leaf-collector-refactor cost, I took the documented fallback without re-asking. Pre-deciding the fallback at plan time removed a mid-implementation decision boundary. - Verification ran incrementally throughout:
pnpm run checkplus the targetedvitestfile after each step, then the full suite,eslint, andpnpm fallow dead-code(from the repo root) before each commit and again pre-push — no end-of-session verification pile-up.
What caused friction (agent side)
instruction-violation— appended the TDD stage notes with a shell heredoc (cat >> … << 'EOF'), whichAGENTS.md, themarkdown-conventionsskill, AND.pi/prompts/tdd-plan.mdline 165 all forbid. Self-identified immediately after; verified the Unicode (em-dashes,…) rendered correctly, so zero rework. Notable because the #308 retro ADDED that exacttdd-plan.mdline and it still did not prevent the slip — the reminder sits at the end of a long prompt and lost to heredoc habit. The one multi-stage prompt that lacks the reminder is.pi/prompts/retro.md.other(environment) —git log | grep -oPfailed in the ship stage because macOS BSDgreplacks-P; recovered in one retry withgrep -Eo. Same friction the #306 retro noted and explicitly rejected as a process change (environment-specific). One round-trip, no rework.other(mechanical) — two@typescript-eslint/prefer-optional-chainnits in step 1 (next !== null && !next.isNamed,!child || !child.isNamed), fixed with an early-return guard and!child?.isNamed. Caught by packageeslintbefore the commit; routine.
What caused friction (user side)
- None.
The user ran all four stages back-to-back with no strategic redirection; involvement was mechanical oversight plus the two planning
ask_userdesign decisions (Tier 1 + Tier 2 scope; conservative unknown-base), both genuine owner-judgment calls posed at the right moment.
Follow-up (not for this session)
- The pre-completion reviewer's WARN stands as a real but substantive cleanup:
bash-program.ts(~975 lines) carries two descent strategies sharing leaf collectors, withcollectPathCandidateTokensdual-used. Folding substitution-internal scoping in and unifying the two walks (the plan's Open Question convergence) is a multi-file refactor — worth its own issue and/plan-issue, not a retro-scoped edit.
Diagnostic details
- Escalation-delay tracking — no
rabbit-hole; every friction point resolved in ≤2 tool calls. - Feedback-loop gap analysis — no gap; verification ran after every step and fully before every commit and pre-push.
- Unused-tool detection — no gap;
colgrepwas loaded but correctly unused — every search was exact-symbol (leadingCdTarget,externalPaths,rawTokens), sogrepwas the right tool. - Model-performance correlation — the only subagent dispatch was the
pre-completion-revieweron judgment-heavy read-only review; it returned a thorough PASS with one accurate WARN, an appropriate match. No quality mismatch surfaced from the parent-session model switches.
Changes made
.pi/prompts/retro.md— added a one-line reminder to Step 3 ("Author and append the retro file with theEdit/Writetools, not a shell heredoc"), for parity with.pi/prompts/tdd-plan.mdand.pi/prompts/build-plan.md, closing the one multi-stage prompt that lacked it.packages/pi-permission-system/docs/retro/0307-effective-working-directory-projection.md— appended this Final Retrospective stage entry. Candidates considered and rejected: a BSDgrep -Pportability note (already rejected in the #306 retro as environment-specific), aprefer-optional-chainrule (routine lint), and escalating the heredoc rule inAGENTS.md/themarkdown-conventionsskill (already present in three places — the gap wasretro.mdonly). The walk-unification cleanup (reviewer WARN) is recorded above as a follow-up for its own issue, not a retro edit.