mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
167 lines
18 KiB
Markdown
167 lines
18 KiB
Markdown
# Phase 8: Tidy first for the authority spine
|
||
|
||
The [authority model](../architecture.md#the-authority-model) is the declared target: an `Authorizer` role selected once per session, yolo as recorded authority, and `PermissionForwarder` split by direction of authority flow.
|
||
Phase 8 does not build the spine.
|
||
It makes the spine change easy — Kent Beck's "make the change that makes the change easy, then make the easy change" — by landing the preparatory refactorings the discovery trace found between `GateRunner` and the UI/file transport.
|
||
The spine itself (the `Authorizer` interface and its three implementations, `canConfirm()` dissolution, serving-as-resolution, grant-scope selection) is Phase 9, and the case-by-case model judge requested in [#472] rides on that spine as the `ModelTriageAuthorizer`, not on this phase.
|
||
|
||
## Findings
|
||
|
||
Health score 76 (B); no dead code; average cyclomatic complexity 1.4; maintainability 91.1.
|
||
The score deductions are large *test* arrow functions and test-tree duplication — production functions are small, so the remaining debt is structural, not syntactic.
|
||
The trace from `GateRunner` down to the UI dialog and forwarding files confirmed the elicitation thicket exactly as the target section describes it, plus the friction that would make the spine diff large:
|
||
|
||
- **yolo is smeared across the ask path.**
|
||
`shouldAutoApprovePermissionState` is checked in `PermissionPrompter.prompt` and again in the forwarded-inbox serve arm; `canResolveAskPermissionRequest`'s yolo arm sits in `PromptingGateway.canConfirm()`.
|
||
Three modules know about yolo on the decision path; the target says the ruleset should be the only one.
|
||
- **The three `Authorizer`s already exist as anonymous branches.**
|
||
`PermissionForwarder.requestApproval` dispatches hasUI → direct dialog (the future `LocalUserAuthorizer`), not-a-subagent → deny (`DenyingAuthorizer`), else → forward (`ParentAuthorizer`) — inside a 591-LOC class that also owns the opposite-direction serving role (`processInbox`).
|
||
- **Subagent detection is threaded as a dep triple.**
|
||
(`subagentSessionsDir`, `platform`, `registry`) is threaded into three constructors (`PromptingGateway`, `PermissionForwarder`, `ForwardingManager`), and `isSubagentExecutionContext` is re-evaluated up to three times per ask; the spine's "selected once per session" needs one owner for this predicate.
|
||
- **A third elicitation path.**
|
||
The deprecated `permissions:rpc:prompt` event-bus handler is a parallel prompt path (own hasUI check, own review-log entry, own UI-prompt event) the spine would otherwise have to adapt.
|
||
- **The test scaffolding the spine will rewrite is duplicated.**
|
||
`permission-manager-unified.test.ts` carries 24 clone groups (305 lines, accelerating churn); `permission-forwarder.test.ts` carries 6 groups including a 43-line clone ×2.
|
||
|
||
| Metric | Phase 7 close | Target after Phase 8 |
|
||
| ------------------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------- |
|
||
| Health score | 76 (B) | ≥ 76 (B) |
|
||
| yolo checks on the ask path | 3 (prompter, gateway, serve arm) | ✅ 1 (composition-stage rewrite) + serve arm (dissolves with the spine) |
|
||
| `canConfirm()` predicates | hasUI ∨ isSubagent ∨ yolo | ✅ hasUI ∨ isSubagent (selection-ready) |
|
||
| Elicitation paths the spine must adapt | 3 (gate prompt, forwarded inbox, RPC prompt) | ✅ 2 (gate prompt, forwarded inbox) |
|
||
| `PermissionForwarder` roles per class | 2 (escalation + serving, 591 LOC) | ✅ 1 each (two classes under `src/authority/`) |
|
||
| Subagent-detection dep-triple constructors | 3 | ✅ 1 (`SubagentDetection`) |
|
||
| fallow refactoring targets | 1 (`value-guards.ts`) | 1 (`value-guards.ts`; fan-in from `toRecord`/`getNonEmptyString` persists) |
|
||
| Duplication | 6.7% (3,129 lines) | ≤ 5.5% |
|
||
|
||
## Steps
|
||
|
||
1. ✅ **Extract shared fixtures from `permission-manager-unified.test.ts`.**
|
||
([#525]) Target: `test/permission-manager-unified.test.ts` (3,714 LOC, 24 clone groups / 305 duplicated lines, accelerating churn) — extract the repeated config-harness blocks into `test/helpers/manager-harness.ts` (or a sibling fixture module).
|
||
No production change; tidies the ground Step 2's manager tests land on.
|
||
Smell: Category D (test duplication).
|
||
Outcome: the file's clone groups drop to near zero; test-tree duplication falls measurably.
|
||
Landed: the seven config-harness factories plus the `sessionRule` builder now live in `test/helpers/manager-harness.ts`; the test file drops from 3,745 to 3,481 LOC with one intentional act/assert clone remaining (agent-frontmatter, kept per the plan's Non-Goals).
|
||
Release: independent
|
||
|
||
2. ✅ **Move yolo into recorded authority: composition-stage `ask` → `allow` rewrite.**
|
||
([#526]) Target: `src/permission-manager.ts` (apply the rewrite over the composed ruleset at check time, keyed off an injected yolo reader; yolo state must join the `resolvedPermissionsCache` key or be applied post-cache), `src/rule.ts` (`RuleOrigin` gains `"yolo"`; update this doc's inline `Rule` listing), `src/handlers/gates/helpers.ts` + `runner.ts` (a yolo-origin `allow` derives resolution `auto_approved`, and the runner writes the `permission_request.auto_approved` review entry so review-log parity holds).
|
||
Display must not change: `getComposedConfigRules` / `/permission-system show` keep showing the configured actions, not the rewrite.
|
||
Faithful to current behavior: explicit `deny` is not `ask`, so yolo suppresses prompts but preserves hard denies (see [yolo is recorded authority](../architecture.md#yolo-is-recorded-authority)).
|
||
Smell: Category C (policy smeared across the prompt path).
|
||
Outcome: `evaluate()` is the only yolo decision point; the prompter and gateway yolo arms become unreachable; review log and decision events keep reporting `auto_approved`.
|
||
Landed: `rewriteAsksToYolo` (pure `Ruleset` transform in `rule.ts`) is applied post-cache in `PermissionManager.check` behind an injected `isYoloEnabled` reader, wired in `index.ts` to `isYoloModeEnabled(configStore.current())`; `deriveResolution` maps a yolo-origin `allow` to `auto_approved` and `GateRunner` gained a yolo fast-path that writes the `permission_request.auto_approved` review entry (runner `logContext` shape, `toolCallId` not `requestId`).
|
||
Skill-reads under yolo resolve to `allow` via the yolo-aware sanitizer and log `policy_allow`/`origin: "builtin"` — an accepted parity nuance (the prompter arm still auto-approves nothing new).
|
||
The `yolo checks on the ask path` metric is not yet flipped; the prompter/gateway arms are removed in Step 3.
|
||
Release: batch "yolo-recorded-authority"
|
||
|
||
3. ✅ **Delete the dead yolo arms from the prompt path; dissolve `yolo-mode.ts`.**
|
||
([#527]) Target: `src/permission-prompter.ts` (drop the auto-approve arm), `src/prompting-gateway.ts` (`canConfirm()` = hasUI ∨ isSubagent; `canResolveAskPermissionRequest` deleted), `src/yolo-mode.ts` (dissolved — `isYoloModeEnabled` and the serve arm's check move next to their config in `extension-config.ts`).
|
||
The forwarded-inbox serve arm keeps its yolo check for now — it dissolves when serving becomes resolution (Phase 9), and is documented as such.
|
||
Smell: Category A (dead code after Step 2).
|
||
Outcome: no yolo knowledge on the prompt path; `canConfirm()` is reduced to the two Authorizer-selection predicates.
|
||
Landed: `PermissionPrompter.prompt()` dropped the auto-approve arm and its `config` dependency; `PromptingGateway.canConfirm()` is now `hasUI ∨ isSubagentExecutionContext(...)`, and `canResolveAskPermissionRequest` / `AskPermissionResolutionOptions` are deleted; `isYoloModeEnabled` moved into `extension-config.ts` and `yolo-mode.ts` is deleted; the forwarded-inbox serve arm re-points at `isYoloModeEnabled` with a comment noting it dissolves in the Phase 9 spine work.
|
||
Release: batch "yolo-recorded-authority"
|
||
|
||
4. ✅ **Extract a shared forwarded-permission test harness.**
|
||
([#528]) Target: `test/permission-forwarder.test.ts` (43-line clone ×2 plus 6 groups / 110 lines), `test/forwarding-manager.test.ts`, `test/permission-forwarding.test.ts` — extract request/response builders, temp forwarding-dir setup, and a fake `ForwarderContext` into `test/helpers/forwarding-fixtures.ts`.
|
||
Smell: Category D (test duplication).
|
||
Outcome: forwarder-family clone groups drop to near zero; Step 6 migrates its per-class tests onto the harness instead of copying scaffolding again.
|
||
Landed: `test/helpers/forwarding-fixtures.ts` holds `createForwardingTempDir` (handle + `cleanup`, with a `writeRequest` writer), `makeForwarderDeps`, `makeForwarderContext`, `makeUiDecision`, and `makeSubagentRegistry`; `permission-forwarder.test.ts` migrated fully (every `try/finally` temp-dir block gone, `makeEvents` reused from `handler-fixtures`) and `permission-forwarding.test.ts` onto `makeSubagentRegistry`.
|
||
`forwarding-manager.test.ts` was left unchanged — its `ExtensionContext`-cast ctx, mocked `subagent-context`, and fake-timer polling do not overlap the harness (per the plan's Non-Goals).
|
||
Release: independent
|
||
|
||
5. ✅ **Extract a `SubagentDetection` collaborator; seed `src/authority/`.**
|
||
([#529]) Target: new `src/authority/subagent-detection.ts` — a class constructed once in `index.ts` with (`subagentSessionsDir`, `platform`, `registry`), exposing `isSubagent(ctx)`; move `src/subagent-context.ts` → `src/authority/subagent-context.ts` (its consumers are all rewired by this step anyway).
|
||
`PromptingGateway`, `ForwardingManager`, and `PermissionForwarder` drop the threaded dep triple and take the collaborator.
|
||
Smell: Category C (dep triple threaded through three constructors) + Category E (seeds the authority domain directory).
|
||
Outcome: one construction site for subagent detection — the input the Phase 9 Authorizer selection consumes; `src/authority/` exists.
|
||
Landed: `src/authority/subagent-detection.ts` holds `SubagentDetection` (implements `SubagentDetector` + `RegisteredChildDetector`), constructed once in `index.ts` and shared; it delegates to the pure functions in the moved `src/authority/subagent-context.ts`, which keep their test file intact.
|
||
`PromptingGateway`, `ForwardingManager`, and `PermissionForwarder` took the `SubagentDetector` seam (the forwarder keeps `registry` for target resolution only), and the scope widened to `PermissionServiceLifecycle`, which took the `RegisteredChildDetector` seam and dropped its raw registry field — so all subagent-detection predicates now have one owner.
|
||
Release: independent
|
||
|
||
6. ✅ **Split `PermissionForwarder` by direction of authority flow.**
|
||
([#530]) Target: `src/forwarded-permissions/permission-forwarder.ts` (591 LOC, both roles) → `src/authority/approval-escalator.ts` (`ApprovalEscalator implements ApprovalRequester` — keeps the three-way dispatch with each branch a named method, plus the request-write/poll machinery) and `src/authority/forwarded-request-server.ts` (`ForwardedRequestServer implements InboxProcessor` — `processInbox` and the per-request serve flow); `src/forwarded-permissions/io.ts` → `src/authority/forwarding-io.ts`; the `forwarded-permissions/` directory dissolves.
|
||
Callers are unchanged: `PermissionPrompter` keeps depending on `ApprovalRequester`, `ForwardingManager` on `InboxProcessor`.
|
||
Smell: Category B/C (dual-role class; the target's declared split).
|
||
Outcome: each class constructs with only its own dependencies; Phase 9 turns the escalator's three named branches into the three `Authorizer`s branch-by-branch instead of dissecting a god class.
|
||
Landed: `src/authority/forwarder-context.ts` holds the shared `ForwarderContext` read-interface and `getSessionId`; `ApprovalEscalator` (escalation-up, `ApprovalRequester`) and `ForwardedRequestServer` (serving-down, `InboxProcessor`) each construct with only their own dependencies — the escalator dropped `config`/`events`, the server dropped `detection`/`registry`; `src/forwarded-permissions/` and `test/forwarded-permissions/` both dissolved.
|
||
Release: independent
|
||
|
||
7. ✅ **Remove the deprecated `permissions:rpc:check` / `permissions:rpc:prompt` event-bus channel.**
|
||
([#531]) Target: delete `src/permission-event-rpc.ts` and `test/permission-event-rpc.test.ts`; remove the deprecated request/reply payload types and channel constants from `src/permission-events.ts`; unwire from `index.ts` / `PermissionServiceLifecycle`; update the cross-extension docs to point exclusively at the `Symbol.for()` service accessor.
|
||
Before writing the migration note, verify the named replacement methods on the real `PermissionsService` type.
|
||
Narrows [#309] to the service path only — leave a comment on that issue.
|
||
Smell: Category A (deprecated subsystem) / Category F (duplicate cross-extension surface).
|
||
Outcome: one cross-extension policy/prompt surface; the spine adapts two elicitation paths instead of three; **breaking** for event-bus RPC consumers.
|
||
Landed: deleted `src/permission-event-rpc.ts` and its test; removed the RPC channel constants, request/reply payload types, the shared `PermissionsRpcReply` envelope, and `PERMISSIONS_PROTOCOL_VERSION` from `src/permission-events.ts`; removed the dead `rpc_prompt` UI-prompt source, `buildRpcUiPrompt`, and the `UI_PROMPT_SOURCES` whitelist entry; unwired registration and the two unsub handles from `src/index.ts`; repointed `docs/cross-extension-api.md` exclusively at the `Symbol.for()` service accessor; commented on [#309] narrowing its scope to the service path.
|
||
Release: independent
|
||
|
||
8. ✅ **Split `value-guards.ts` by cohesion.**
|
||
([#532]) Target: `src/value-guards.ts`: keep the generic parsing guards (`toRecord`, `getNonEmptyString`); move the domain guards (`isPermissionState`, `isDenyWithReason`) next to the types they guard (`src/types.ts`).
|
||
Note: [#547] already removed the config-only guards (`normalizeOptionalStringArray`, `normalizeOptionalPositiveInt`) when zod took over config validation, shrinking this target.
|
||
Smell: Category B (high-impact file) / Category E (mixed cohesion).
|
||
Outcome: domain guards co-located with their types; **fallow refactoring targets did not clear to 0** — `value-guards.ts` still reports as a target (19 dependents) because the fan-in came from the retained generic guards (`toRecord`/`getNonEmptyString`), not the relocated domain guards.
|
||
Landed: moved `isPermissionState` and `isDenyWithReason` from `src/value-guards.ts` to `src/types.ts`, beside `PermissionState`/`DenyWithReason`; repointed the three domain-guard consumers (`permission-manager.ts`, `normalize.ts`, `config-loader.ts`) to import from `./types`; moved the guard tests from `test/value-guards.test.ts` into a new `test/types.test.ts`.
|
||
Release: independent
|
||
|
||
## Step dependency diagram
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
S1["✅ Step 1 (#525)<br/>Manager-unified test fixtures"]
|
||
S2["✅ Step 2 (#526)<br/>yolo into the composed ruleset"]
|
||
S3["✅ Step 3 (#527)<br/>Delete dead yolo arms"]
|
||
S4["✅ Step 4 (#528)<br/>Forwarding test harness"]
|
||
S5["✅ Step 5 (#529)<br/>SubagentDetection + seed authority/"]
|
||
S6["✅ Step 6 (#530)<br/>Split PermissionForwarder by direction"]
|
||
S7["✅ Step 7 (#531)<br/>Remove deprecated event-bus RPC<br/>(breaking)"]
|
||
S8["✅ Step 8 (#532)<br/>Split value-guards.ts"]
|
||
|
||
S1 --> S2
|
||
S2 --> S3
|
||
S4 --> S6
|
||
S5 --> S6
|
||
```
|
||
|
||
## Parallel tracks
|
||
|
||
- **Track A — yolo becomes recorded authority:** Steps 1 → 2 → 3.
|
||
- **Track B — escalation machinery:** Steps 4 and 5 in parallel, then Step 6.
|
||
- **Track C — cross-extension surface reduction:** Step 7, independent.
|
||
- **Track D — health:** Step 8, independent.
|
||
|
||
## Release batches
|
||
|
||
- **Batch "yolo-recorded-authority":** Steps 2, 3 (ship together; tail = Step 3).
|
||
Step 2 relocates the yolo decision with observable review-log/decision-event field changes and Step 3 is its cleanup.
|
||
- Independently releasable: Steps 1, 4 (test-only; hidden changelog type), Steps 5, 6, 8 (refactors; auto-batch into the next release), Step 7 (**breaking** — ships as its own major-bump release).
|
||
|
||
## Non-goals
|
||
|
||
- **The spine itself.**
|
||
The `Authorizer` interface and its three implementations, `canConfirm()` dissolution, serving-as-resolution, the one-hop canary, grant-scope selection, and yolo inheritance are Phase 9 — this phase only removes the friction in their way.
|
||
- **The serve-arm yolo check.**
|
||
It survives Phase 8 (one isolated `if`) and dissolves when `processInbox` is refactored onto `evaluate()` plus Authorizer selection.
|
||
- **Principal identity and cross-session path portability.**
|
||
Still the deferred access-intent design work; the forwarded request keeps carrying display fields, not a re-evaluable intent, until then.
|
||
- **A big-bang `src/` reorganization.**
|
||
Only the files Phase 8 already rewrites move into `src/authority/`; see the directory sketch below.
|
||
|
||
## Directory sketch (forward-looking)
|
||
|
||
Phase 8 seeds `src/authority/` with the modules it rewrites: `subagent-detection.ts` and `subagent-context.ts` (Step 5), then `approval-escalator.ts`, `forwarded-request-server.ts`, and `forwarding-io.ts` (Step 6).
|
||
The remaining elicitation modules (`gate-prompter.ts`, `prompting-gateway.ts`, `permission-prompter.ts`, `permission-dialog.ts`, `permission-forwarding.ts`, `subagent-registry.ts`) migrate in Phase 9 as the spine rewrites them into the `Authorizer` interface and its implementations — there is no peer `subagent/` domain, because the subagent machinery is the cross-session edge of `authority/`.
|
||
|
||
[#309]: https://github.com/gotgenes/pi-packages/issues/309
|
||
[#472]: https://github.com/gotgenes/pi-packages/issues/472
|
||
[#525]: https://github.com/gotgenes/pi-packages/issues/525
|
||
[#526]: https://github.com/gotgenes/pi-packages/issues/526
|
||
[#527]: https://github.com/gotgenes/pi-packages/issues/527
|
||
[#528]: https://github.com/gotgenes/pi-packages/issues/528
|
||
[#529]: https://github.com/gotgenes/pi-packages/issues/529
|
||
[#530]: https://github.com/gotgenes/pi-packages/issues/530
|
||
[#531]: https://github.com/gotgenes/pi-packages/issues/531
|
||
[#532]: https://github.com/gotgenes/pi-packages/issues/532
|
||
[#547]: https://github.com/gotgenes/pi-packages/issues/547
|