6.7 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 368 | Remove the `config-modal` controller reach-through |
Retro: #368 — Remove the config-modal controller reach-through
Stage: Planning (2026-06-12T00:00:00Z)
Session summary
Planned the removal of the Law-of-Demeter reach-through in the /permission-system show handler (Phase 5 Step 7, Track D).
The plan collapses the controller's permissionManager + session fields into one getActiveAgentConfigRules(): Ruleset accessor, wired as a thin adapter closure in index.ts, and retires the fallow false-positive suppression on PermissionSession.lastKnownActiveAgentName.
The change is non-breaking (package-internal type, unchanged show output) and lands as one atomic refactor commit plus a separate doc commit marking the roadmap step complete.
Observations
- Issue author is the operator (
gotgenes); the proposed change is unambiguous and passes thecode-designcheck (the accessor returns a value, so it is real encapsulation, not procedure-splitting) — skipped theask-usergate. - The retro for plan
0341was the key prior-context find:fallow's blind spot is object-literal wiring inindex.ts(config-modal receivessessionas an object-literal property, not a traced positional arg). A named-interface attempt there did not satisfyfallow. The plan's premise is that moving the read into a real arrow-function body (session.lastKnownActiveAgentNameinside the closure) makes it a directly traced property access — the one casefallowcan follow — which is what makes retiring the suppression safe. - The only empirical unknown is whether
fallow dead-codeactually stops flagging the getter; the plan carries a documented fallback (restore a single justified suppression) so the unknown does not block. - The interface change breaks
index.tswiring and all fourconfig-modal.test.tscontroller literals at the type level in the same commit, so they fold into one TDD step per the AGENTS.md rule on constructed call sites. getComposedConfigRulesalways returns aRuleset(neverundefined), so the accessor needs no optionality and the existing empty-ruleset/summarizeConfigbehavior is preserved.
Stage: Implementation — TDD (2026-06-12T08:30:00Z)
Session summary
Executed the single TDD cycle: updated all four controller literals in test/config-modal.test.ts to the getActiveAgentConfigRules shape (Red — 4 type errors confirmed), then updated src/config-modal.ts, src/index.ts, and src/permission-session.ts (Green).
All 93 test files / 1951 tests stayed green; committed as one atomic refactor: commit.
A separate docs: commit marked Phase 5 Step 7 complete in docs/architecture/architecture.md.
Observations
- The key risk —
fallowstill flagginglastKnownActiveAgentName— did not materialise. Moving the read into a real arrow-function body inindex.ts(session.lastKnownActiveAgentName) was sufficient forfallowto trace it; thefallow-ignore-next-linesuppression is fully retired. - All four controller literals in
test/config-modal.test.tswere updated atomically; TypeScript's excess-property checking atpnpm run checkcaught any that might have been missed. - The
summarizeConfigoptionalrules?parameter did not need to change — the accessor always returns a definedRuleset, and the existing behavior (empty ruleset → no rule-suffix) is preserved without optionality. - Pre-completion reviewer: PASS — no WARN findings.
Stage: Final Retrospective (2026-06-12T09:00:00Z)
Session summary
Shipped the config-modal Law-of-Demeter refactor across four stages (Planning, TDD, Ship, Retro) in one continuous session with zero rework, zero user corrections, and zero deviations from the plan.
The refactor: + docs: commits landed on main, CI passed, issue #368 closed, and no release-please PR was triggered (expected for non-feat/fix commits).
Observations
What went well
- The cross-session context bridge proved its value concretely.
The Planning stage read
docs/retro/0341-*.md, extracted the precisefallowblind-spot diagnosis (object-literal wiring is untraceable; a named-interface attempt did not satisfyfallow), and built the plan's central hypothesis on it: move the read into a real arrow-function body inindex.tssofallowtraces it directly. That hypothesis held in the TDD stage —fallow dead-codestopped flagginglastKnownActiveAgentNameand the suppression was fully retired. A prior session's retro directly shaped a later session's design and the prediction came true. - Model selection matched task type at every stage with no mismatch (see Diagnostic details).
- The TDD Red phase used
pnpm run check(type errors) as the failing signal rather than a runtime assertion — correct for a pure interface-shape refactor where Vitest does not typecheck and the four controller literals break only at the type level.
What caused friction (agent side)
- None. No rework, no follow-up fixup commits, no rabbit-holes, no instruction violations across all four stages.
What caused friction (user side)
- None.
The issue was the operator's own, unambiguous, and non-breaking, so the
ask-usergate was correctly skipped at Planning; no mid-session intervention was needed.
Diagnostic details
- Model-performance correlation — Planning ran on
claude-opus-4-8(judgment-heavy: design decision,code-designheuristic check, cross-retro synthesis); TDD and Ship ran onclaude-sonnet-4-6(mechanical execution of a well-specified plan and a deterministic ship checklist); Retro ran onclaude-opus-4-8(synthesis). Thepre-completion-reviewersubagent ran during TDD and returned PASS in ~162 s. No mismatch — no reasoning-weak model on judgment work, no high-cost model on pure mechanics. - Feedback-loop gap analysis — verification was incremental, not end-loaded: green baseline confirmed before any edit; Red confirmed via
pnpm run checkimmediately after the test edit;pnpm run check+ single-file Vitest + full-suite Vitest +pnpm fallow dead-codeall run before the refactor commit;lint:mdbefore the docs commit. Each gate fired at the transition it guards rather than batched at the end. - Escalation-delay / unused-tool lenses — N/A; no
rabbit-holeormissing-contextfriction occurred.
Changes made
.pi/skills/fallow/SKILL.md(gotcha #6) — added a third remedy for the object-literal-wiring blind spot: move the read into a traced closure body at the composition root (e.g.getX: () => owner.member), demoting suppression to last resort. Evidence: issues #341 (discovered + suppressed) and #368 (resolved by restructuring) are the same recurring pattern.