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

15 KiB
Raw Blame History

issue, issue_title
issue issue_title
475 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.

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.tsaccess-intent/bash/program.ts and bash-token-classification.tsaccess-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).