Files
my-pi/pi-permission-system/docs/retro/0475-extract-command-enumeration-cwd-projection.md
T

145 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 475
issue_title: "pi-permission-system: extract command enumeration and cwd projection; relocate the bash sub-domain (Phase 6 Step 3)"
---
# Retro: #475 — Extract command enumeration and cwd projection; relocate the bash sub-domain
## Stage: Planning (2026-06-25T13:35:00Z)
### Session summary
Produced a four-cycle plan for Phase 6 Step 3 (the `bash-program-decomposition` batch tail): extract command enumeration to `command-enumeration.ts`, the `cd`-fold projection to `cwd-projection.ts`, relocate the slimmed `BashProgram` to `access-intent/bash/program.ts`, and relocate `bash-token-classification.ts` to `access-intent/bash/token-classification.ts`, repointing all gate consumers and tests.
The plan embeds four rendered-and-validated Mermaid diagrams: a shared data-flow view plus three module-layout variants (A/B/C) for the facade-scope decision, written at the operator's request so they can render them in a browser before choosing.
### Observations
- The one genuine design fork is **facade thinness / `EffectiveBase` encapsulation**, surfaced via `ask_user` (operator's own issue, so the gate confirmed a real design choice rather than direction): A = strict lift-and-shift (facade keeps `externalPaths`/`pathRuleCandidates`, reads `EffectiveBase` internals across the new boundary); B = thin facade (projection moves to `cwd-projection.ts`, `EffectiveBase` fully encapsulated); C = A plus a `resolveCandidateBase()` helper.
- The operator inclined toward B but asked for the data flow and a comparison diagram before committing, then asked for the plan to be written "as is" with the diagrams embedded for browser rendering.
Plan recommends B and is written for B, with A/C deltas noted inline; the decision is flagged **pending visual review** — the one gate before TDD.
- Corrected the operator's initial framing of B: B's win is **cohesion/encapsulation** (one module owns the `cd`-projection lifecycle end to end; `EffectiveBase` never crosses a boundary), not new test surface.
The projection functions' natural input is parse output, so existing parse-driven tests stay as facade coverage rather than converting to isolated unit tests.
- `BashCommand` moves to `command-enumeration.ts` (its producer); only `bash-command.ts` imports it externally. `BashPathRuleCandidate` has no external importer (public return type only), so under B it co-locates with `cwd-projection.ts`.
- Mermaid pitfall hit during validation: `class` and `enum` are reserved flowchart node ids (`Expecting 'SPACE', got 'SQS'`); renamed to `clsf` / `enm`.
All four diagrams validated with `mmdc`; `rumdl` clean.
- Batch-tail release caveat recorded: all three steps are `refactor:`, so release-please derives no version bump from them — "ship now" means "nothing holds the batch back," not "force a release."
Folded in the [#474]-deferred architecture `Outcome:` fix ("≤ 670 LOC" vs actual 695).
- No follow-up issues filed: Step 4 ([#476], `AccessPath`) already exists; the external-directory gate collapse is tracked as Phase 6 Step 5.
[#474]: https://github.com/gotgenes/pi-packages/issues/474
[#476]: https://github.com/gotgenes/pi-packages/issues/476
## Stage: Planning — design revision (2026-06-25T14:30:00Z)
### Session summary
A Socratic design dialogue with the operator expanded the plan well past the issue's lift-and-shift framing.
Three decisions landed: (1) `BashProgram` becomes born-ready — `parse(command, cwd: string)` resolves eagerly and the slice methods become parameter-free getters; (2) the `ToolCallContext.cwd` type widening is fixed package-wide; (3) the facade-scope fork resolves to Option B because born-ready leaves no call-time orchestration for the facade to retain.
The plan and both Mermaid diagrams were rewritten to the decided design; the three A/B/C comparison diagrams were dropped (they described the superseded lazy model and would contradict born-ready during implementation).
### Observations
- **Born-ready insight** — the operator pushed on "why is `cwd` not available at parse time?"
then "why pass `cwd` to `parse()` rather than store it?"
Resolution: `parse(command, cwd)` is the async factory (constructors can't be async); `cwd` is consumed during birth to produce the resolved arrays, so it is a factory parameter, not a retained field (storing it would be dead state).
`PathCandidate` / `EffectiveBase` become fully internal to `cwd-projection.ts` — never on the instance.
- **`cwd` type widening is a real error** — verified `ExtensionContext.cwd: string` (non-optional; the same SDK interface marks `model` / `signal` as `| undefined`, so `cwd`'s presence is deliberate).
The widened `ToolCallContext.cwd: string | undefined` spawned dead `cwd`-undefined branches in **five** gates (`bash-external-directory`, `external-directory`, `skill-read`, `path`, `tool`) plus three obsolete "no CWD" tests.
This is a package-wide gate-layer cleanup, orthogonal to the bash relocation but coupled to born-ready at the pipeline seam (`parse` needs a `string`).
- **Scope decision (`ask_user`)** — operator chose **all-in #475** (relocation + born-ready + full `cwd` fix) over splitting the type fix into a prerequisite issue or deferring born-ready.
Recorded as Alternatives considered in the plan.
- **`BashProgram` is a function masquerading as a class** — acknowledged: under eager resolution the three getters return stored arrays, so the class is close to a data holder.
Deliberately **not** collapsed to a function-returning-record here — that reshape is deferred to Step 4 ([#476]), which already retypes `externalPaths` for `AccessPath`.
- TDD order grew to five cycles: cwd type fix (independent, lands first) → command enumeration → cwd projection + born-ready (largest) → relocation → docs.
Still all `refactor:` / `docs:` — no user-facing behavior change, so the batch-tail release caveat (no version bump from refactors) stands.
- Both rewritten diagrams validated with `mmdc`; `rumdl` clean.
## Stage: Implementation — TDD (2026-06-25T17:40:00Z)
### Session summary
Completed all five planned TDD cycles in one session: (1) narrowed `ToolCallContext.cwd` to `string` and removed five dead `cwd`-undefined gate branches plus two obsolete tests; (2) extracted `command-enumeration.ts` with `BashCommand` and `collectCommands`; (3) extracted `cwd-projection.ts` with the full `cd`-fold walk and born-ready projection functions, rewrote `BashProgram` with `parse(command, cwd)` and parameter-free getters, updated all callers and tests; (4) relocated `bash-program.ts``access-intent/bash/program.ts` and `bash-token-classification.ts``access-intent/bash/token-classification.ts`, repointed six source files and seven test files, deleted old locations; (5) updated `docs/architecture/architecture.md` with the new layout entries, `✅` Step 3 markers, Track A completion, and corrected health metrics.
Test count moved from 2107 (baseline) → 2105 (Step 1: 2 dead tests) → 2104 (Steps 23: 1 no-cwd test) and held at 2104 through Steps 45.
Pre-completion reviewer: PASS.
### Observations
- **No red phase**: every cycle was behavior-preserving; the suite stayed green throughout.
The only "failures" were type errors caught by `tsc` that guided the consumer update scope (four extra test files needed `parse(cmd, cwd)` beyond what the plan listed).
- **Deviation — extra consumer test files**: the plan listed `bash-program.test.ts`, `bash-external-directory.test.ts`, `bash-path.test.ts`, `bash-command-metamorphic.test.ts`, `tool-call-gate-pipeline.test.ts`, and `external-directory-symlink-acceptance.test.ts` for the born-ready `parse(cmd, cwd)` update.
`tsc` after Step 3 also surfaced `test/handlers/gates/bash-path.test.ts` and `test/handlers/gates/bash-external-directory.test.ts` parse-helper locals — both corrected in the same commit.
- **Deviation — `vi.mock` path**: the pipeline test's `vi.mock("#src/handlers/gates/bash-program", …)` assertion needed updating after the relocation (Step 4) — caught as a live test failure (the mock silently stopped intercepting), corrected immediately.
- **Step 3 was the largest**: the `bash-program.test.ts` rewrite touching ~90 call sites (`parse(cmd)``parse(cmd, cwd)`, `.externalPaths(cwd)` / `.pathRuleCandidates(cwd)` → parameter-free) was done as a full `Write` per AGENTS.md guidance; no logic changed, only call-site shape.
- **Biome `noRedeclare` / `noUnusedVariables` fires**: when `collectCommands` was imported from the new module but the old local definition was still present, Biome's pre-commit hook caught both as errors — exactly the correctness gate the plan predicted.
- **`program.ts` born-ready LOC**: 102 (plan estimated ~110); `cwd-projection.ts`: 493 (plan estimated ~420 — the difference is the projection functions' dedup and loop bodies plus fuller doc comments).
- Pre-completion reviewer: PASS — all categories clean; noted `cwd-projection.ts` at 493 LOC is intentional (Option B encapsulation); no WARNs.
## Stage: Ship (2026-06-25T18:10:00Z)
### Session summary
Shipped the `bash-program-decomposition` batch tail: pushed six commits, CI green, closed #475 plus the two stacked predecessors (#473, #474) with curated implemented-in comments, and merged release-please PR #483 (rebase) to cut `pi-permission-system-v16.0.2`.
Correctly identified that every #475 commit is inert for release purposes (all `refactor:` or `docs:` on `exclude-paths`), so the version bump came from the one releasable commit already in the unreleased range — the #473 `docs(pi-permission-system):` architecture entry that predated `docs/architecture` joining `exclude-paths`.
### Observations
- **Inert-batch analysis held**: the plan's batch-tail caveat ("all `refactor:` → no bump") matched reality exactly; the 16.0.2 patch was driven by a stranded #473 docs commit, not by #475's work.
The ship flow surfaced this rather than forcing a fake `fix:`.
- **Release-changelog wrinkle (config-timing, not agent friction)**: 16.0.2's changelog lists only the #473 architecture entry because `packages/pi-permission-system/docs/architecture` was added to `exclude-paths` after #473's commit landed but before #474/#475's architecture commits — so the later (richer) doc commits are inert while the earliest one still drove the release.
Harmless; the decomposition is internal-only and has no user-facing changelog story anyway.
- **`ci_watch` tool quirk**: the first `ci_watch` returned `aborted: cancelled by user` at 105 s without any user action; a re-run with a longer timeout streamed progress and reported `success`.
Transient tool artifact, no agent rework.
- **CI write-back working as designed**: `ab93e61d chore: advance release-please last-release-sha baseline [skip ci]` landed automatically after the release (Refs #468) — the baseline auto-advance, not a manual step.
## Stage: Final Retrospective (2026-06-25T18:25:00Z)
### Session summary
Phase 6 Step 3 ran the full multi-session lifecycle (plan → design revision → TDD → ship → retro) and shipped clean: the bash engine now lives entirely under `src/access-intent/bash/`, `BashProgram` is a 102-LOC born-ready facade, and a latent `ToolCallContext.cwd` type-widening bug (five dead gate branches) was found and fixed along the way.
Released as `pi-permission-system-v16.0.2`, completing the `bash-program-decomposition` batch (#473, #474, #475).
### Observations
#### What went well
- **The planning `ask_user` gate did real design work, not just direction confirmation** — it surfaced the facade-scope fork (A/B/C), and the ensuing Socratic dialogue expanded the change to born-ready construction *and* uncovered a latent type-widening defect (`ToolCallContext.cwd: string | undefined` vs the SDK's non-optional `ExtensionContext.cwd: string`) with five dead branches across unrelated gates.
A planning gate finding and fixing a real bug orthogonal to the issue's stated scope is the standout of this arc.
- **Born-ready emerged from operator pushback, not a correction** — "why is `cwd` not available at parse time?"
then "why store it rather than pass it to `parse()`?"
reframed the value object cleanly: `cwd` is a factory parameter consumed at birth, not a retained field (storing it would be dead state); `PathCandidate` / `EffectiveBase` never reach the instance.
- **Cross-session retro bridging worked end to end** — each stage's notes (planning → revision → TDD) carried decisions forward, so no ground was re-litigated; the design-revision stage's born-ready spec drove the five-cycle TDD order without rediscovery.
- **`tsc`-guided, behavior-preserving TDD** — the born-ready signature change is a type-level break, so `tsc` enumerated every stale call site immediately; verification ran incrementally (check + full suite per cycle), and the suite stayed green throughout with zero rework.
Pre-completion reviewer PASS, ship clean.
#### What caused friction (agent side)
- `missing-context` (minor) — the plan's Module-Level Changes listed six test files for the born-ready `parse(cmd, cwd)` update but missed two gate-test parse-helper locals (`describeGate` wrappers in `bash-path.test.ts` / `bash-external-directory.test.ts`) and the pipeline `vi.mock` factory path.
Impact: none beyond same-commit fixes — `tsc` and one live test failure caught all of them inside the commits that introduced the change; no follow-up commits, no rework.
The type-level break made the feedback loop authoritative.
#### What caused friction (user side)
- None.
The operator's interventions were strategic redirects (the born-ready and cwd-widening questions), which functioned as the design gate working as intended — the opposite of mechanical oversight.
### Diagnostic details
- **Model-performance correlation** — the only subagent dispatch, `pre-completion-reviewer`, ran on its frontmatter default (`anthropic/claude-sonnet-4-6`), appropriate for the judgment-heavy review (29 tool uses, 217 s); no mismatch.
Session-level `model_change` entries are dominated by transient menu selections with no attributed turns and were not over-counted.
- **Escalation-delay tracking** — no `rabbit-hole` friction; the lone retry (`ci_watch`) was a one-shot tool re-run, not a stuck approach.
- **Feedback-loop gap analysis** — no gap: `pnpm run check` + full suite ran after every TDD cycle, and `lint` + `fallow dead-code` after the relocation and again pre-push.
### Candidate rules considered and rejected
Two durable-rule candidates were considered and rejected as over-fitting a clean, self-correcting session (mirroring #474's retro discipline):
1. A `code-design` "born-ready construction / a parameter consumed at birth is not a field" heuristic — the operator drove this successfully via dialogue, and the pattern is already implicit in the skill's DIP and output-argument guidance; codifying it on one data point risks premature abstraction.
2. A `plan-issue` / `design-review` "scan for type-widening against SDK contracts" prompt rule — the existing SDK-contract-verification guidance already covers it; the win here was the dialogue, not a checklist gap.
Recorded here rather than promoted.
### Changes made
1. Appended the Ship and Final Retrospective stage entries to `packages/pi-permission-system/docs/retro/0475-extract-command-enumeration-cwd-projection.md` (this file).
2. No prompt or `AGENTS.md` changes — the operator confirmed retro-only; both candidate rules were considered and rejected as over-fitting a clean session (recorded above).