14 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 580 | pi-permission-system: shell-tool alias config model (shellTools) |
Retro: #580 — Shell-tool alias config model (shellTools)
Stage: Planning (2026-07-13T00:00:00Z)
Session summary
Planned Phase 11 Step 2: an additive, non-breaking shellTools config field mapping a tool name to { commandField, workdirField? }, delivering the validated/merged/documented config surface only (Step 3 / #574 consumes it).
Produced a 3-cycle TDD plan — schema surface + regen, runtime carry-through + merge, docs + roadmap mark — committed as 0580-shell-tool-alias-config-model.md.
Observations
- Merge semantics were the one real design choice and are locked to shallow-merge by tool name (operator-confirmed after a walkthrough).
Rationale:
shellToolsonly ever tightens enforcement (routes a tool through the bash stack) and is inert when the tool is unregistered, so a dropped entry is a silent enforcement regression — additive merge is the safe, deterministic, least-privilege choice. Per-tool mapping override still works via key collision (spread replaces the colliding alias object wholesale); total codex opt-out is a package-disable concern, not a permission-config lever. "Replace wholesale" was rejected: its only added capability ("define one entry, silently drop all global entries") is a footgun with no legitimate use. - Grounded the design in the real tool by cloning
@howaboua/pi-codex-conversion:exec_commanduses canonical fieldscmd(required) +workdir(optional), confirming the issue's proposed{ commandField: "cmd", workdirField: "workdir" }shape and that a tool-name-keyed map is right (it also ships a code-modeexec; other extensions could register their own shells). - Kept
$defsat three entries by deliberately notid-tagging the alias sub-schema (it inlines), so theconfig-schema.test.ts$defsassertion stays green without edit. - Carry-through is compiler-enforced post-
#356:normalizePermissionSystemConfigreads the typed field, so a missed merge/normalize site failstsc— the#332/#347silent-drop class is structurally guarded. - Release is deferred (mid-batch, batch "shell-tool-aliases", tail =
#574); the plan's commits (feat:/docs:) wait onmainand auto-batch into the cut when Step 3 lands. - Next step:
/tdd-plan(this plan has test cycles).
Stage: Implementation — TDD (2026-07-13T12:40:00Z)
Session summary
Implemented all three planned TDD cycles: (1) shellTools schema surface + regenerated JSON schema, (2) runtime carry-through (PermissionSystemExtensionConfig + normalizePermissionSystemConfig) and shallow-by-tool-name merge in mergeUnifiedConfigs, (3) docs (config.example.json, configuration.md, README.md) + roadmap Step 2 marked ✅.
Test count 2374 → 2387 (+13); pnpm run check, root pnpm run lint, and pnpm fallow dead-code all green.
Observations
- Tidy-First assessor found no preparatory work warranted — every target file already had a direct precedent (
permissionMapSchemafor the newz.record, thepiInfrastructureReadPaths/toolInputPreviewMaxLengthcopy-through blocks, the three-branch optional-field merge blocks, flat inline-literal test cases). Implemented directly. - Deviation from plan: the plan listed exporting both
ShellToolAliasandShellToolsConfigfromconfig-schema.ts; onlyShellToolsConfigis exported (consumed byextension-config.ts).ShellToolAliashad no consumer yet and tripped thefallow dead-codegate, so it was dropped in arefactor:commit —#574reintroduces it when the enforcement gate consumes the field. Added one extra test beyond the plan (empty-stringcommandFieldrejection). - Design held as planned: the un-
id-tagged alias sub-schema inlines, keeping$defsat exactly three entries; regenerating the schema produces zero diff;config.example.jsonvalidates againstunifiedConfigSchema. - Fallow gate caught the speculative export — a reminder that
code-design's "no speculative re-exports" rule is enforced deterministically here, not just by review. - Pre-completion reviewer: PASS — deterministic checks all green, no code-design concerns, Mermaid parses, dead-code clean,
#574follow-up correctly recorded. No warnings. - Release: mid-batch — defer (batch "shell-tool-aliases", tail =
#574); confirm batching at ship time. Next step:/ship-issue.
Stage: Final Retrospective (2026-07-13T17:35:41Z)
Session summary
Shipped #580 cleanly across four stages (plan → TDD → ship → retro) in one continuous session: a non-breaking shellTools config surface, +13 tests, PASS pre-completion review, CI green on f66beef7.
The release was deferred at ship time per the plan's mid-batch — defer marker (batch "shell-tool-aliases", tail #574), so the issue stays open and the release-please PR is left unmerged.
Observations
What went well
- Grounded the config design in the real tool before designing it (Planning) — cloned
@howaboua/pi-codex-conversionviafetch_contentand readsrc/tools/exec/command-tool.tsto confirmexec_command's canonical fields (cmdrequired,workdiroptional) rather than trusting the issue's prose. This is themissing-contextfailure mode pre-empted: the field names and the tool-name-keyed-map shape were verified against source, not assumed. - The
ask_usermerge-semantics gate worked as a genuine design conversation (Planning) — the operator engaged across three rounds ("walk me through the consequences", "what happens when a project wants to clobber global") rather than picking blindly, and each round added new evidence (real-tool facts, the clobber-vs-disable distinction). The gate surfaced a security-relevant decision (shallow-merge vs. replace) that a silent default could have gotten wrong. - The
fallow dead-codegate caught the speculative export deterministically (TDD) — the safety net fired exactly where the plan erred, before push. - Clean incremental feedback loop (TDD) — ran
pnpm run check+ the affected test file after each of the three cycles, not just at the end; the full-suite/lint/fallow sweep at the end found only the one export issue.
What caused friction (agent side)
missing-context(planning-time, self-caught by gate) — the plan (0580Design Overview + Module-Level Changes) prescribed exporting bothShellToolAliasandShellToolsConfigfromconfig-schema.ts, butShellToolAliashas no in-scope consumer (its consumer is the deferred#574). Thefallow dead-codegate rejected it during the end-of-TDD sweep, forcing arefactor:commit (e7cc7260) to remove the export that afeat:commit (cd4f851a) had just added. Impact: one extra commit and a small feat-adds-then-refactor-removes churn within the same PR; no rework beyond that. Thecode-designskill already carries the rule ("Do not add speculative re-exports; fallow will flag them as dead code"), but it was not applied at plan time — the gap is that/plan-issuedoes not prompt to defer an export whose only consumer is a later issue.
What caused friction (user side)
- Mis-click on the ship-stage release-coordination
ask_user(cancelled the flow by accident). I paused rather than guessing the release decision, re-asked in plain text, and the operator confirmed defer immediately. Impact: none — no rework, correct outcome; the pause-don't-guess behavior on a high-stakes irreversible gate was the right call.
Diagnostic details
- Model-performance correlation — both subagents (
tidy-first-assessor,pre-completion-reviewer) ran onanthropic/claude-sonnet-5, appropriate for read + judgment work; no reasoning-weak-on-judgment or costly-on-mechanical mismatch. The main session ran onopus-4-8/sonnet-5. - Escalation-delay tracking — no
rabbit-holesequences; no error was retried more than once. - Unused-tool detection — no gaps;
fetch_content(repo clone) was the right tool for verifying the external tool's field names, and the subagents covered tidy-first + pre-completion. - Feedback-loop gap analysis — verification ran incrementally (per-cycle
check+ affected test file), not end-only; the deferred-to-end checks (rootlint,fallow dead-code) are the ones that must run late anyway.
Changes made
-
Appended this Final Retrospective stage entry to
packages/pi-permission-system/docs/retro/0580-shell-tool-alias-config-model.md. -
No prompt or
AGENTS.mdchanges: the one candidate (a/plan-issuereminder to defer an export whose only consumer is a later issue) was declined as first-instance over-fitting — thecode-designskill already carries the underlying "no speculative re-exports" rule.Revisit if the speculative-export-tripping-
fallowpattern recurs. -
Post-ship rename of the
shellToolsconfig keys — the operator noticed "field" is not Pi's vocabulary for tool-call input parts. Verified against~/development/pi/pi/packages/ai/src/types.ts: Pi usesTool.parameters(declared schema),ToolCall.arguments(runtime values), and JSON-Schema properties — never "field." The termcommandField/workdirFieldwas inherited verbatim from the issue body and never reconciled at plan time (a planning-stagemissing-context: the field valuescmd/workdirwere grounded against the real tool, but the meta-term was not). RenamedcommandField→commandArgumentandworkdirField→workdirArgument(operator-chosen: the value names a key inToolCall.arguments, which is what#574reads at gate time) acrosssrc/config-schema.ts(keys + prose descriptions), regeneratedschemas/permissions.schema.json,config/config.example.json,docs/configuration.md, the three test files,docs/architecture/architecture.md, and both plan files (0580+ the not-yet-implemented0574). Safe to do without a breaking-change footer because#580is merged but unreleased (mid-batch defer) and#574has a plan but no implementation. The historical Planning/TDD stage entries above keep the originalcommandField/workdirFieldterm as an accurate timeline of what those stages produced.
Stage: Final Retrospective — Addendum: naming correction (2026-07-13T18:42:03Z)
Session summary
A short follow-up session in which the operator questioned whether "field" is Pi's term for tool-call input parts.
Investigation against Pi's source confirmed it is not, and the shellTools config keys were renamed commandField/workdirField → commandArgument/workdirArgument before release (execution detail recorded in the addendum stage's ### Changes made item 3 above).
This addendum captures the reusable lesson behind that rename.
Observations
What went well
- Verified against the authoritative source instead of asserting — read
~/development/pi/pi/packages/ai/src/types.tsand confirmedTool.parameters/ToolCall.arguments/ JSON-Schema properties are Pi's vocabulary, so the answer to "is 'field' Pi's term?" rested on the actual SDK contract, not memory. - Caught inside the unreleased window — because
#580was merged but held under the mid-batch defer, the rename was a cleanrefactor:(noBREAKING CHANGE:footer, no consumer to migrate);#574had a plan but no implementation, so updating its plan file was the only downstream cost. ask_useron a preference-sensitive naming call rather than picking a term unilaterally — the operator chosecommandArgument/workdirArgumentfrom four grounded options (argument/parameter/key/keep).
What caused friction (agent side)
missing-context(user-caught) — the config keyscommandField/workdirFieldwere inherited verbatim from the issue body and carried into a public config surface without reconciling the term against Pi's SDK vocabulary. The planning stage grounded the field values (cmd/workdir) against the real tool's source but never checked the naming term itself; neither planning, TDD, pre-completion review, nor the first Final Retrospective caught it. Impact: a post-merge rename touching 11 files plus a CI run — avoided released breakage only because the mid-batch defer left#580unreleased.
What caused friction (user side)
- The SDK-vocabulary mismatch was user-caught, not self-identified — evidence that the existing planning guard for public-surface naming (
plan-issuestep 6, which searches sibling packages for conventions) does not prompt a check against the SDK's term for the domain concept a config key names. - Bidirectional (positive): the operator proactively shared the local Pi source path (
~/development/pi/pi) after afind /probe stalled, unblocking the verification in one turn rather than leaving me to hunt for the checkout.
Diagnostic details
- Model-performance correlation — no subagents dispatched this session; the investigation and rename ran in the main session (
opus-4-8/sonnet-5), appropriate for a source-reading + judgment task. - Unused-tool detection — one minor detour: a
find /sweep for Pi's.d.tsfiles was aborted before the operator pointed to~/development/pi/pi; a first grep of the known local checkout path would have skipped it. NoExplore/colgrepgap otherwise. - Feedback-loop gap analysis — verification was incremental and complete: affected-file tests +
pnpm run checkafter the rename, rootlint+ full suite (2387) before commit, and CI watched to green post-push.
Changes made
- Appended this Addendum stage entry to
packages/pi-permission-system/docs/retro/0580-shell-tool-alias-config-model.md. - Added an SDK-vocabulary naming guard to
.pi/prompts/plan-issue.mdstep 6: a config key or public field naming an SDK/domain concept should use the SDK's own term (verified against SDK types), not a term adopted verbatim from the issue body (Refs #580). The rationale and the 11-file worked example stay here in the retro; the prompt carries only the rule plus a one-clause example.