9.1 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 568 | Tool-kind classification decided once at the normalize boundary (extraction family) |
Retro: #568 — Tool-kind classification decided once at the normalize boundary (extraction family)
Stage: Planning (2026-07-10T00:00:00Z)
Session summary
Planned Phase 10 Step 1 of the pi-permission-system roadmap: a new access-intent/tool-kind.ts owning a ToolKind string-union classification and its single dispatch point classifyToolKind, with the five extraction-family consumers (input-normalizer.ts, tool-input-path.ts, handlers/gates/tool.ts, handlers/gates/tool-call-gate-pipeline.ts, permission-manager.ts) migrated onto it.
Wrote a five-step TDD plan (one true red for the new module, the rest behavior-preserving refactors under the existing green suite) and committed it as docs/plans/0568-tool-kind-classification-extraction.md.
Observations
- Bare union over rich product.
Chose a plain
ToolKindstring union rather than a discriminated object carrying the extracted command/target/path. A rich product would need the MCP server-name list, the extractor registry, andAccessPath— the last is forbidden inpermission-manager.tsby the ADR-0002no-restricted-importsrule, and the pipeline's bash guard andderiveSourcehave none of those inputs in hand. The classification is the single missing dispatch point; per-kind products stay where their dependencies live. - Extraction vs presentation split confirmed by grep.
The official recompute (
toolName === "(bash|mcp)"|source === "mcp") returns exactly 21: 10 extraction sites (this step) + 11 presentation sites (Step 2, #569). The metric keys literally ontoolName ===/source ===, so consumers dispatching on aclassifyToolKind/kind-named value produce zero grep matches — extraction drops to 0, total to 11 (≤ 12 target). getToolPermissiondead branches. Found thatgetToolPermission'snormalizedToolName === "bash"/"mcp"/"skill"branches (not grep-counted — different variable name) plus the SPECIAL and default arms all evaluate the identicalevaluate(normalizedToolName, "*", composedRules, platform).action. Folded a behavior-identical collapse into the manager migration step; flagged as optionally droppable in Open Questions.buildInputForSurfacedeliberately excluded. It dispatches on path/service surface names (external_directoryhas noToolKindvariant), not tool names, soclassifyToolKindwould drop a branch — left as-is and recorded in Non-Goals.- ADR-0002 boundary holds.
tool-kind.tsimports only the purePATH_BEARING_TOOLSset, stayingAccessPath-free, sopermission-manager.tsimportingclassifyToolKinddoes not breach the string boundary. Noted as an at-risk invariant pinned by the existing ESLint rule. - Release posture.
refactor:(hidden changelog type), head of batch "tool-kind-dispatch" (tail = Step 2, #569) → mid-batch defer; does not cut a release on its own. - First-party, unambiguous → no
ask_usergate; the one design choice (union vs product) was resolved via code-design heuristics.
Stage: Implementation — TDD (2026-07-10T15:00:00Z)
Session summary
Executed the five-step plan: added access-intent/tool-kind.ts (ToolKind + classifyToolKind) and migrated the five extraction consumers (tool-input-path, input-normalizer, permission-manager, and the tool-call gate pipeline's tool.ts + tool-call-gate-pipeline.ts) onto it, then recorded the roadmap step in architecture.md.
Four refactor: commits + one docs: commit; the suite grew from 2310 to 2317 (+7, the new classifyToolKind unit tests).
The extraction-family production discriminator sites dropped to 0 and the total family from 21 to 12 (target ≤ 12), all remaining sites being the presentation family Step 2 (#569) clears.
Observations
- No new characterization tests needed.
permission-manager-unified.test.tsalready pinned everyderiveSourcearm (special / skill / bash / mcp config+default / tool config+default), so the manager migration was a pure refactor under green. - Two in-scope cleanups folded into the manager step (deviation from a strict file-by-file plan, anticipated in the plan's Design Overview).
getToolPermission's per-kind branches were provably dead (every branch, including SPECIAL and default, evaluated the identicalevaluate(normalizedToolName, "*", composedRules, platform).action) and collapsed to one line;BUILT_IN_TOOL_PERMISSION_NAMESbecame dead once thepath/bashkinds covered its role and was removed. - Exhaustive switches in
normalizeInput,getToolInputPath, andderiveSourcemake a futureToolKindvariant a compile error rather than a silent fall-through — the OCP win. buildInputForSurfaceleft untouched as planned (it dispatches on service/path surface names includingexternal_directory, which has noToolKindvariant).- Metric nuance: the recompute grep now returns 12, one of which is a docstring inside
tool-kind.ts(the outcome permits sites inside that module); all 11 others are the presentation family. - Pre-completion reviewer: PASS — all deterministic checks green (
check, rootlint, 2317 tests,fallow dead-code), Mermaid re-rendered clean (4 charts), ADR-0002 boundary verified intact. No warnings.
Stage: Final Retrospective (2026-07-10T19:07:04Z)
Session summary
One continuous session carried #568 through all four stages — plan, TDD, ship, retro — for Phase 10 Step 1 of the pi-permission-system roadmap.
The implementation landed cleanly: five red→green→commit cycles, four refactor: + two docs: commits, suite 2310 → 2317 (+7), pre-completion PASS, CI green, issue closed, release deferred (batch tail is Step 2 / #569).
Zero rework, zero user corrections, zero CI or reviewer failures across the whole session.
Observations
What went well
- Plan mitigations validated at execution.
The plan's "fold the first consumer into the introduction commit to avoid a
fallow dead-codefailure on the unwired export" mitigation worked exactly as written — the reviewer explicitly confirmed it. The plan's Test Impact Analysis prediction ("permission-manager-unified.test.tsalready pins everyderiveSourcearm") held, so the manager migration was a pure refactor under green with no new characterization tests. - Reviewer scope corrected for a batched-release repo.
The pre-completion skill's Step 1 diff (
git diff $BASE..HEADwithBASE= last tag) over-scoped badly: the lastpi-permission-systemtag (v20.3.0) predates the entire already-landed Phase 9 batch, so the tag-based file list dragged in ~40 unrelatedauthority/files. Scoping the reviewer to the issue's own commit range (04f3e5c1^..HEAD) gave it the 10 files that actually changed. This recurs structurally: refactor issues ship mid-batch, so the last tag is routinely many commits behind. - Exhaustive-switch discipline paid off.
Migrating
normalizeInput/getToolInputPath/deriveSourcetoswitch (classifyToolKind(...))turned a future tool-kind addition into a compile error — the OCP win the issue set out to buy, now structurally enforced rather than convention.
What caused friction (agent side)
other(over-verification) — during the ship close-comment step I re-checked the 40-char SHA length three times with three different one-liners (wc -c,head -c 40,tr -d '\n' | wc -c).git rev-parse <short>already returns the full expanded SHA, so the length checks were redundant. Impact: ~3 extra tool calls, no rework.
What caused friction (user side)
mid-batch — deferask was moot in hindsight. The ship flow asked the operator up front whether to defer the release; the answer was defer. Only later (step 4b) did the range-check confirm every unreleased commit is a non-releasing type (docs:/refactor:), so nothing would release regardless. The early ask is deliberate design (a decision from the plan resists reversal), and cost one near-zero question — flagged as an observation, not a problem to fix.
Diagnostic details
- Model-performance correlation — one subagent dispatch: the
pre-completion-reviewer(its configured model) on judgment-heavy review work — an appropriate match; it ran the deterministic gates and returned a scoped PASS. - Escalation-delay tracking — no
rabbit-holefriction points; no error sequence exceeded one tool call. - Unused-tool detection — nothing missed;
colgrep/grep/readcovered planning exploration, and no lens indicated an un-dispatched subagent was warranted. - Feedback-loop gap analysis — verification ran incrementally:
pnpm run checkafter every shared-type step, the affected test file per red→green cycle, then the full suite + rootlint+fallow dead-codebefore the reviewer. No end-of-session-only verification.
Changes made
.pi/skills/pre-completion/SKILL.md(Step 1) — added a note that under batched releases the last-tag$BASE..HEADdiff over-scopes to sibling issues' shipped files, and to scope the reviewer to the issue's own commits (git diff --name-only <plan-commit>^..HEAD) instead.