7.0 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 454 | Bash external_directory gate: cd-fold projection drops the running directory across a redirect-then-pipe, causing false external-path prompts |
Retro: #454 — Recover bash operator precedence so a cd fold persists across a redirect-then-pipe
Stage: Planning (2026-06-21T00:00:00Z)
Session summary
Planned the fix for BashProgram.externalPaths over-prompting when a leading current-shell cd precedes a redirect-then-pipe statement (pnpm x 2>&1 | tail).
Confirmed the root cause by dumping the tree-sitter-bash AST and verifying real bash semantics with bash -c: the parser mis-groups cd a/b && pnpm x 2>&1 | tail as (cd a/b && pnpm x 2>&1) | tail, burying the current-shell cd inside a pipeline node that the walker's default case treats as non-folding.
The plan adds a pipeline case to walkForCandidates that folds the first stage's leading current-shell commands while keeping the terminal piped command and downstream stages as non-folding subshells.
Observations
- The author is
gotgenes(the operator), and the expected behavior is dictated by real bash precedence (|binds tighter than&&), so theask-usergate was skipped — there is no operator-facing ambiguity. - Verified the fail-closed boundary empirically:
bash -c 'cd a/b && cd c 2>&1 | tail; pwd'ends ina/b, nota/b/c— the terminalcdis the real pipe stage (subshell) and must not fold. Folding it would under-flag a later escape (a fail-open regression), so the plan treats the terminal command of the first stage specially (foldListExceptTerminal) and pins it with a dedicated test. - Classified as a non-breaking
fix:— it removes false-positive external-directory prompts; no default, config, or output shape changes. - Not part of any architecture-roadmap batch → ship independently.
- The change is internal to
bash-program.ts:externalPaths(cwd): string[]is unchanged, so no consumer, test import, orSKILL.mdreference moves. The design-review checklist found no structural smell (no shared-interface widening, no new threaded parameter); the three new helpers each return anEffectiveBase(real behavior, not procedure-splitting). - The #452 A3 never-weaker invariant pins the bash command gate (
commands()), a different slice thanexternalPaths, so the metamorphic test is untouched; the #307/#418 fail-closed projection invariant is the one at risk and is guarded by the new terminal-cdtest.
Stage: Implementation — TDD (2026-06-21T00:00:00Z)
Session summary
Implemented the redirect-then-pipe cd-fold recovery in two TDD cycles: a fix: cycle adding four externalPaths projection tests plus walkPipeline / foldPipelineFirstStage / foldListExceptTerminal and a pipeline case in walkForCandidates, then a docs: cycle updating the bash-program.ts line in docs/architecture/architecture.md.
Test count rose 2065 → 2069 (+4); full suite, tsc, root lint, and pnpm fallow dead-code all green.
Observations
- Deviated from the plan on test 4: used
cat ../foo(escaping under the pre-cd base, inside under the folded base) instead ofcat foo, so the downstream-stage test is a genuine red test rather than green pre-fix; documented in the commit body. - Pre-fix failure values matched the traced
path.resolvepredictions exactly (test 1/projects/b, test 3/x→/projects/x, test 4/projects/foo), confirming the base-reset diagnosis. - The fail-closed terminal-
cdtest (cd a && cd b 2>&1 | tail ; cat ../../x⇒/projects/x) is the load-bearing guard: without it, a naive "fold the whole first stage" fix would silently fold the pipe-stagecd band under-flag a later escape (fail-open). - ESLint's pre-commit auto-fix removed a non-null assertion (
namedChildren[i]!→namedChildren[i]);tscstayed clean, so the assertion was unnecessary — array indexing returnsTSNodehere. - The architecture tree-listing line uses bare
#N(parenthesized) for inline issue refs, not[#N]reference links; rumdl MD053 does not recognize[#454]inside that line, so the doc update uses bare#454to match the local convention and avoid the unused-definition error. - Pre-completion reviewer: PASS — all deterministic checks green, the three at-risk invariants (#452 A3 never-weaker, #307/#418 fail-closed subshell + terminal-pipe-stage) verified, and the test-4 deviation accepted as a quality improvement.
Stage: Final Retrospective (2026-06-21T00:00:00Z)
Session summary
Landed the #454 worktree branch onto linear main via /land-worktree: ff-merged issue-454-bash-external-directory-gate-cd-fold-pro (66351220..98e856bc), pushed, verified CI (run 27915785330), closed the issue, released pi-permission-system@16.0.1, and tore down the worktree.
The flow ran end-to-end with one friction point — deriving the previous-release tag for the close-comment range.
Observations
What went well
- The
UNSTABLE-no-checks release-please PR (#458, emptystatusCheckRollup— theGITHUB_TOKENcase) was handled exactly per the documented fallback:gh pr merge 458 --rebasethengit pull --ff-only, no attempt to merge a genuinely blocked PR. This is the documented path, not a novel win — noted only to confirm the guardrail held. - Inspected the full PR body (
gh pr view 458 --json body -q .body) before merging and confirmed it bumped onlypi-permission-system— no sibling-package bump slipped through.
What caused friction (agent side)
wrong-abstraction— to find the previous-release tag for the close-comment range I rangit tag --sort=-version:refname | head -1, which sorts lexically across all package tags and returnedpi-subagents-worktrees-v0.2.3(an unrelated package).git log --oneline pi-subagents-worktrees-v0.2.3..HEADthen dumped ~400 commits instead of the ~5 in the #454 range. Impact: added friction (large log to scan) but no rework — the #454 commits sat at the top ofHEAD, so I filtered them manually. The package-scoped tag (git tag --list 'pi-permission-system-v*' --sort=-creatordate | head -1⇒pi-permission-system-v16.0.0) gives the tight, correct range.
What caused friction (user side)
- None — the user invoked
/land-worktree 454and let it run; the flow needed no strategic intervention.
Diagnostic details
- All four diagnostic lenses found nothing notable: no subagents were dispatched (the land flow is deterministic tool calls), no
rabbit-holesequences occurred, no unused tool would have helped, and CI was the verification gate (run correctly on the pushed SHA before the issue was closed or released).
Changes made
.pi/prompts/land-worktree.md§5 — replaced the<previous-tag-or-base>placeholder with package-scoped tag derivation guidance (git tag --list '<pkg>-v*' --sort=-creatordate | head -1), naming the lexical-sort anti-pattern..pi/prompts/ship-issue.md§5 — mirrored the same package-scoped tag derivation on thegit log --onelineblock and the "other issues" range check (two occurrences).