mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
330 lines
25 KiB
Markdown
330 lines
25 KiB
Markdown
---
|
||
issue: 591
|
||
issue_title: "pi-permission-system: design the model-assisted permission judge (tool-augmented, deny-first, extensible)"
|
||
---
|
||
|
||
# ADR 0007 — model-assisted permission judge as an Authorizer chain
|
||
|
||
## Release Recommendation
|
||
|
||
**Release:** ship independently
|
||
|
||
This is Phase 11 Step 7, tagged `Release: independent` in the roadmap.
|
||
It is a documentation-only decision record: it touches `docs/decisions/` and `docs/architecture/`, both release-please `exclude-paths`, so it cuts no physical release on its own — the same finding [#581] reached before it was reverted.
|
||
The design it records is implemented later under [#472], which releases on its own merits.
|
||
|
||
## Problem Statement
|
||
|
||
[#472] asks for a case-by-case judge that participates in a permission decision alongside the static rule engine and `yoloMode`, deciding per ask rather than blanket-approving.
|
||
It was deferred by name in Phases 9 and 10; [#581] then attempted the decision record but treated it as transcription of the architecture doc's settled `ModelTriageAuthorizer` prose, and that ADR was found premature and reverted.
|
||
Two concrete use cases surfaced during the [#581] retro show the real design is broader than — and in one respect contradicts — the reverted prose:
|
||
|
||
1. **Reject errant "typo" paths automatically.**
|
||
A light model reviews `external_directory` asks, *defers* the ones that do not match a configured typo pattern, and *denies* the ones that do — returning a teaching reason (wrong path; correct location) so the invoking model self-corrects.
|
||
2. **Adjudicate opaque bash commands.**
|
||
Commands the deterministic parser cannot decompose (`bash -c "…"`, `eval`, unparseable constructs) floor to `ask` via sentinels.
|
||
A model decomposes the opaque command, queries the deterministic engine per sub-command, and *allows* if clearly fine, *denies* if it hits a denied path, or *defers* if it cannot decide confidently.
|
||
|
||
This issue is the design gate for [#472] and supersedes [#581].
|
||
The deliverable is a decision, not code — so the design below was settled interactively with the operator, not inferred from the existing prose.
|
||
|
||
## Goals
|
||
|
||
- Record `docs/decisions/0007-model-judge-authorizer-chain-adr.md` settling the full design across both use cases, so [#472] becomes schedulable on its own merits.
|
||
- Establish the verdict range as `allow | deny | defer` — a superset of the reverted ADR's allow-or-escalate framing, driven by use case 1 being deny-first.
|
||
- Model the live-authority layer as a **Chain of Responsibility**: each link decides or defers; the terminal link cannot defer and pauses the system until it decides (today, the human).
|
||
- Keep the package model-agnostic: it makes no LLM call, exposes a named-capability registration seam plus an injected query capability, and owns only the safety policy it enforces.
|
||
- Reconcile the architecture doc's `Discriminating delegation` and `pluggable escalation seam` sections with the chain model, and mark Step 7 complete.
|
||
- Record the **dogfooding objective** as slice 1's acceptance criterion: a first-party monorepo package (e.g. `packages/pi-permission-model-judge`) implementing the deny-first typo-path reviewer, so the `registerAuthorizer` seam is born consumed (the [#267] vacant-surface guard) and the config split proves itself concretely.
|
||
|
||
## Non-Goals
|
||
|
||
- Implementing the judge, the chain, the `defer` verdict, the registration seam, or the downstream extension — that is [#472]'s, which this ADR unblocks.
|
||
- Deciding [#472]'s implementation decomposition (chain infrastructure, deny-first slice, allow-capable slice, the downstream orchestrator package) — that is [#472]'s own `/plan-issue`.
|
||
- Building registration for **terminal-replacement** backends (a chat-bot or remote reviewer *as* the authority, not a non-terminal link) — the same seam's other role, built when a real non-subagent backend needs it.
|
||
- The non-deterministic access-intent classifier that reshapes intent *before* `evaluate()` — a different seam feeding recorded authority; it keeps its own future decision record and stays out of scope.
|
||
- Any `src/`, `test/`, `README.md`, `config.example.json`, or `schemas/` change — no runtime surface references the not-yet-built symbols (verified: `ModelTriageAuthorizer` appears only in `docs/`).
|
||
|
||
## Background
|
||
|
||
- `Authorizer` (`src/authority/authorizer.ts`) is the live-authority role: `authorize(details) → Promise<PermissionPromptDecision>`, one method, returning `allow | deny` — there is **no `defer` verdict today**.
|
||
- `selectAuthorizer(ctx, deps)` performs a hardcoded three-way dispatch, returning a **single** `Authorizer`: `hasUI` → `LocalUserAuthorizer` (human, decides); `isSubagent` → `ParentAuthorizer` (forwards up to the parent node's authority); else → `DenyingAuthorizer` (headless, always denies).
|
||
Evaluated once per session activation (`AuthorizerSelection.activate`).
|
||
So the chain is effectively length 1 today.
|
||
- `PermissionsService` (`src/service.ts`), published via `Symbol.for()` and consumed cross-extension, already answers the query primitives the judge needs: `checkPermission(surface, value?, agentName?)` is "is this external?"
|
||
(`external_directory`), "what does this bash resolve to / does it hit a deny?"
|
||
(`bash`, decomposed at gate parity via `resolveBashAdvisoryCheck`, [#309]), and per-surface rule queries; `getToolPermission` answers tool-level state.
|
||
- `registerToolAccessExtractor(toolName, extractor)` / `registerToolInputFormatter(toolName, formatter)` are the established named-capability registration seams this design mirrors.
|
||
- ADR 0005 (`docs/decisions/0005-serving-authorizer-provenance.md`) established that determinism governs *recorded* authority (`evaluate()`), never the *live*-authority layer — the enabling premise for a non-deterministic model holding an `Authorizer` role.
|
||
|
||
Standing constraints from AGENTS.md and the package skill that the ADR must honor:
|
||
|
||
- Registration must land synchronously and be visible before the session's first ask; cross-session visibility rides `globalThis` + `Symbol.for()` (the [#296] bus-split lesson).
|
||
- `permissions:ready` is emitted when the service is (re)published, surviving `/reload`.
|
||
- Default to least privilege: a session no live authority claims selects `DenyingAuthorizer`.
|
||
- A declared config field not read at runtime is a maintenance trap — so this package must not hold model-prompt config it never reads.
|
||
- The arch doc inline-copies `rule.ts` types; this design changes no rule type, so that listing is untouched.
|
||
|
||
## Design Overview
|
||
|
||
The ADR records the following settled design.
|
||
It is documentation; the TypeScript below is the design the ADR commits to, for [#472] to build.
|
||
|
||
### 1. The live-authority layer is a Chain of Responsibility
|
||
|
||
Each link either decides (`allow` / `deny`) or defers to the next link.
|
||
The terminal link cannot defer; the chain ends there and the system pauses until it decides.
|
||
|
||
```typescript
|
||
type AuthorizerVerdict =
|
||
| { kind: "allow" }
|
||
| { kind: "deny"; reason?: string } // reason is the teaching signal (use case 1)
|
||
| { kind: "defer" };
|
||
|
||
/** A non-terminal chain link: may decide or defer. */
|
||
interface Authorizer {
|
||
authorize(
|
||
details: PromptPermissionDetails,
|
||
query: PermissionQuery,
|
||
): Promise<AuthorizerVerdict>;
|
||
}
|
||
|
||
/** The terminal link: structurally cannot defer. */
|
||
interface TerminalAuthorizer {
|
||
authorize(
|
||
details: PromptPermissionDetails,
|
||
query: PermissionQuery,
|
||
): Promise<TerminalVerdict>; // { kind: "allow" } | { kind: "deny"; reason? }
|
||
}
|
||
```
|
||
|
||
The "terminal cannot defer" invariant is **enforced at the type level**: `TerminalAuthorizer` returns only `allow | deny`, so a link that could defer cannot occupy the terminal slot — violating it is a compile error, not a runtime assertion.
|
||
`LocalUserAuthorizer` and `DenyingAuthorizer` are `TerminalAuthorizer`s; `ModelTriageAuthorizer` is an `Authorizer` (non-terminal).
|
||
`ParentAuthorizer` is terminal *for its node* — it forwards up and returns the parent node's `allow | deny`, which is the multi-hop recursion ("a node's terminal hands off to the parent node's chain").
|
||
|
||
### 2. Chain composition: registered links, then the context-selected terminal
|
||
|
||
`selectAuthorizer` (single terminal) generalizes to `composeAuthorizerChain` (ordered non-terminal links + terminal).
|
||
The terminal keeps today's context selection unchanged.
|
||
|
||
```typescript
|
||
function composeAuthorizerChain(
|
||
ctx: ExtensionContext,
|
||
configuredChain: string[], // operator policy: ordered link NAMES
|
||
registry: AuthorizerRegistry,
|
||
query: PermissionQuery,
|
||
policy: DelegationPolicy,
|
||
): AuthorizerChain {
|
||
const links = configuredChain
|
||
.map((name) => registry.get(name) ?? warnSkip(name)) // missing → skip, fail-safe
|
||
.filter((link): link is Authorizer => link !== undefined);
|
||
return new AuthorizerChain(links, selectTerminal(ctx), policy); // terminal last
|
||
}
|
||
```
|
||
|
||
The chain walks links until one decides, then applies the enforcement checkpoint, then falls to the terminal:
|
||
|
||
```typescript
|
||
async authorize(details: PromptPermissionDetails): Promise<TerminalVerdict> {
|
||
for (const link of this.links) {
|
||
const verdict = await link.authorize(details, this.query);
|
||
if (verdict.kind === "deny") return verdict;
|
||
if (verdict.kind === "allow") {
|
||
// Enforcement checkpoint (owned HERE, not the link):
|
||
// an allow on an excluded surface is downgraded to defer.
|
||
if (this.policy.isDelegationExcluded(details.surface)) continue;
|
||
return verdict;
|
||
}
|
||
// defer -> next link
|
||
}
|
||
return this.terminal.authorize(details, this.query); // never defers
|
||
}
|
||
```
|
||
|
||
Three invariants fall out and belong in the ADR:
|
||
|
||
1. **Config order wins, never registration order.**
|
||
Chain order is security-relevant (an allow-capable link ahead of a deny-capable one changes outcomes), so it is deterministic operator policy — never a function of nondeterministic extension load order.
|
||
2. **Skipping any non-terminal link is always fail-safe.**
|
||
A missing or unregistered link removes only allow/deny *shortcuts*; the ask still reaches the terminal.
|
||
Absence of a judge means *more* prompting, never less.
|
||
3. **Registration alone grants no authority.**
|
||
A link decides nothing until the operator names it in `authorizerChain` — the opt-in activation model.
|
||
|
||
### 3. The query capability is injected, not imported
|
||
|
||
The judge never reaches for `PermissionsService` via `Symbol.for()` (a Law-of-Demeter reach-through to a global).
|
||
The chain injects a narrow, session-scoped `PermissionQuery` into each link at `authorize` time — a projection of `PermissionsService` limited to what a link needs (ISP), backed by the same resolver the gates use so it answers at gate parity.
|
||
|
||
```typescript
|
||
/** Narrow, injected projection of PermissionsService. */
|
||
interface PermissionQuery {
|
||
checkPermission(surface: string, value?: string, agentName?: string): PermissionCheckResult;
|
||
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
||
}
|
||
```
|
||
|
||
The tool-augmented adjudication (use case 2) exposes these primitives to the model *as tools*: the model decomposes an opaque command and calls `checkPermission("bash", subCommand)` / `checkPermission("external_directory", token)` per piece; the deterministic engine answers every sub-question.
|
||
The model's non-determinism is confined to *how it decomposes*, never *what the rules decide* — determinism-of-decision survives at the leaf.
|
||
|
||
### 4. Named-capability registration, opt-in activation, `permissions:ready` hook
|
||
|
||
Registration mirrors `registerToolAccessExtractor`: a downstream extension offers a **named** capability.
|
||
The `PermissionsService` interface gains one method:
|
||
|
||
```typescript
|
||
registerAuthorizer(name: string, authorize: Authorizer["authorize"]): () => void;
|
||
```
|
||
|
||
The consumer call site (the downstream `pi-permission-model-judge` extension) registers in a `permissions:ready` handler so it is robust to load order and survives `/reload`:
|
||
|
||
```typescript
|
||
pi.events.on("permissions:ready", () => {
|
||
const orchestrator = createOrchestrator(myConfig); // reads model/provider/prompt HERE
|
||
getPermissionsService()?.registerAuthorizer("model-judge", (details, query) =>
|
||
orchestrator.judge(details, query), // query injected; no service reach-through
|
||
);
|
||
});
|
||
```
|
||
|
||
The ordering contract the ADR fixes:
|
||
|
||
```text
|
||
1. load pi-permission-system publishes PermissionsService + emits `permissions:ready`
|
||
2. register downstream (on `permissions:ready`) offers registerAuthorizer("model-judge", fn)
|
||
→ parked in the registry; grants NO authority yet
|
||
3. compose per session, pi-permission-system reads config.authorizerChain and binds
|
||
names → registered capabilities; a name with no registration is skipped + warned
|
||
4. ask chain walked; each link gets injected PermissionQuery; enforcement checkpoint
|
||
downgrades an excluded-surface allow to defer; terminal always decides
|
||
```
|
||
|
||
### 5. Config split: policy here, mechanism downstream
|
||
|
||
Two independent extension config files, joined only by the link name — no merged schema.
|
||
This package declares and *enforces* the safety policy; the downstream extension declares and *uses* the model mechanism.
|
||
|
||
```jsonc
|
||
// pi-permission-system config.json — operator-owned policy (read + enforced HERE)
|
||
{
|
||
"authorizerChain": ["model-judge"], // ordered link names; the activation gate
|
||
"modelDelegation": {
|
||
"allowedSurfaces": ["bash"],
|
||
"excludedSurfaces": ["external_directory"] // + secret-shaped path always excluded
|
||
}
|
||
}
|
||
```
|
||
|
||
```jsonc
|
||
// pi-permission-model-judge config.json — downstream-owned mechanism (read THERE)
|
||
{ "provider": "anthropic", "model": "claude-haiku-…", "instructions": "…", "timeoutMs": 5000 }
|
||
```
|
||
|
||
The bounded-delegation policy is enforced at the chain's checkpoint (§2), so a buggy or over-eager external judge can never exceed what the operator's policy permits.
|
||
This is why the split is safe: policy lives where it is enforced; mechanism lives where the LLM call is made.
|
||
|
||
### 6. Two slices, a capability gradient
|
||
|
||
Both use cases are the *same* `ModelTriageAuthorizer` link; they differ only by which verdicts are enabled and how much envelope guards them.
|
||
|
||
| Aspect | Slice 1 — deny-first reviewer (use case 1) | Slice 2 — allow-capable adjudicator (use case 2) |
|
||
| ------------ | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||
| Verdicts | `deny`, `defer` | `+ allow` |
|
||
| Risk | strictly more restrictive — always safe | loosens privilege — needs full envelope |
|
||
| Envelope | fail-closed only (unreachable/uncertain → defer) | + hard exclusions, audit `origin:"authorizer:model"`, non-persistence, off by default, bounded-delegation ruleset |
|
||
| Failure mode | a wrong deny — recoverable (agent self-corrects on the reason) | decomposition infidelity — mis-split obfuscation (`bash -c "safe; evil"`) allowed on the safe piece alone |
|
||
|
||
The ADR states plainly that the "can never grant more than the engine grants for the pieces it identifies" safety property holds *only if decomposition is faithful*; obfuscation is the residual risk, and it is exactly why slice 2 is gated behind the whole envelope while slice 1 needs almost none.
|
||
The gradient is the argument for shipping deny-first.
|
||
|
||
Slice 1 is validated by **dogfooding**: a first-party extension in this monorepo (e.g. `packages/pi-permission-model-judge`) implementing the typo-path reviewer against the real seam.
|
||
This is a design safeguard, not just a demo — the arch doc's [#267] history guard warns that an inbound registration surface nobody consumes goes vacant; a first-party consumer registering `"model-judge"` on day one makes `registerAuthorizer` born consumed, and its own config file (provider/model/instructions) exercises the config split end to end.
|
||
The ADR's Consequences section names this objective; the concrete issue is filed by the next `/plan-improvements` pass when the phase is scoped.
|
||
|
||
### Relationship to `evaluate()` and rule-driven promotion
|
||
|
||
The judge sits on the ask-*consuming* side of `evaluate()`, distinct from the ask-*producing* side (rule-driven promotion, [#509]).
|
||
Rule-driven promotion produces the fail-safe false-positive ask (`git grep id_rsa` prompts); the judge dismisses it on the consuming side without hard-coding per-command file-argument tables.
|
||
The two compose cleanly: a promoted token emits the same structured descriptor a prefixed path does, so a link needs no promotion-specific knowledge.
|
||
|
||
## Module-Level Changes
|
||
|
||
Documentation only.
|
||
No `src/`, `test/`, `README.md`, config, or schema change.
|
||
|
||
- **New:** `packages/pi-permission-system/docs/decisions/0007-model-judge-authorizer-chain-adr.md` — the ADR recording §1–§6, rejected alternatives (opt-out activation; judge imports `PermissionsService`; a single terminal instead of a chain; ask-only allow-or-escalate verdict range), and accepted limitations ([#472] owns provider/prompt/threshold/timeout tuning, the slice decomposition, and the downstream package; terminal-replacement registration deferred; the pre-`evaluate()` classifier keeps its own future ADR).
|
||
The Consequences section names the dogfooding objective: slice 1 is accepted by a first-party monorepo judge extension registering against the real seam.
|
||
The reverted 0007 slot is free, so this is ADR 0007.
|
||
- **Changed:** `packages/pi-permission-system/docs/architecture/architecture.md`
|
||
- Rewrite the `Discriminating delegation: a model Authorizer` section (line ~604): the chain model, verdict range `allow | deny | defer`, deny-first two-slice gradient, injected `PermissionQuery`, opt-in named registration, config split — superseding the ask-only allow-or-escalate framing.
|
||
- Reconcile the `Beyond the target: a pluggable escalation seam` section (line ~681): it is now *subsumed* by the chain — registering a link is the seam; a terminal-replacement backend is the same seam's terminal role, deferred (not a mechanism beside the chain).
|
||
- Leave the `Beyond the target: a non-deterministic access-intent classifier` section (line ~668) intact — still out of scope with its own future ADR.
|
||
- Reword the aspirational-extension-points sentence (line ~499): the model-triage `Authorizer` and the pluggable escalation seam are now designed (link ADR 0007); the classifier remains aspirational.
|
||
- Reconcile the `Resolved direction` recursion language (line ~633+) from "a node's `Authorizer`" to the chain framing where the terminal hands off to the parent node's chain.
|
||
- Mark Phase 11 Step 7 complete: `✅` on the `#### Step 7:` heading and the `S7` Mermaid node, and link ADR 0007 from both the step target and the `Discriminating delegation` section.
|
||
- Update the [#472] deferral dispositions (lines ~900, ~1041) to record that [#472] now carries this ADR and is schedulable.
|
||
- **Not edited:** `docs/architecture/history/phase-8|9|10-*.md`, `docs/plans/0509|0555|0556|0557|0581-*.md`, `docs/retro/0581-*.md`, `docs/retro/phase-9-*.md` — these are frozen point-in-time records that mention `ModelTriageAuthorizer`; they are not live design docs and must not be rewritten.
|
||
|
||
## Test Impact Analysis
|
||
|
||
Not applicable in this issue — the deliverable is a decision record with no code.
|
||
The tests the design *enables for [#472]* (recorded here so [#472]'s TDD plan inherits them): a chain walk that stops at the first deciding link; the `defer` verdict advancing the cursor; the terminal-cannot-defer type constraint; a missing configured link name skipping fail-safe; the enforcement checkpoint downgrading an excluded-surface `allow` to `defer`; opt-in activation (a registered-but-unconfigured link deciding nothing); and the injected `PermissionQuery` answering at gate parity.
|
||
|
||
## Invariants at risk
|
||
|
||
This is the exact failure mode that reverted [#581]: an internally consistent ADR that contradicts un-reconciled prose elsewhere in the architecture doc.
|
||
The pre-completion reviewer must confirm cross-doc consistency, not just the ADR's internal coherence.
|
||
|
||
- **Cross-doc verdict-range consistency.**
|
||
The pre-revert `Discriminating delegation` prose frames the judge as ask-only, allow-or-escalate; the new design is `allow | deny | defer`.
|
||
Grep the whole architecture doc for stale framing before finalizing: `grep -nE "ask-only|allow-or-escalate|escalate|ModelTriageAuthorizer|quarantine|a model .Authorizer" architecture.md`. ([#581] missed the non-persistence parenthetical at line ~627 because its grep targeted one section; sweep the whole file.)
|
||
- **Recursion language.**
|
||
The `Resolved direction` and `the recursion` passages describe "a node's `Authorizer`"; under the chain they describe a node's *chain* whose terminal hands off upward.
|
||
Both must read consistently.
|
||
- **Aspirational list.**
|
||
Line ~499 lists the model-triage Authorizer and the pluggable escalation seam as aspirational; ADR 0007 designs both, so the list must move them to "designed, pending [#472]."
|
||
|
||
## Build Order
|
||
|
||
Documentation-only, so `/build-plan` (no red→green cycles).
|
||
Numbered `docs:` commits, each leaving the docs internally consistent.
|
||
|
||
1. **Author the ADR.**
|
||
Write `docs/decisions/0007-model-judge-authorizer-chain-adr.md` (§1–§6, rejected alternatives, accepted limitations, reference-link definitions).
|
||
Verify with `pnpm exec rumdl check` on the new file.
|
||
Commit: `docs(pi-permission-system): record ADR 0007 for the model-judge Authorizer chain (#591)`.
|
||
2. **Reconcile the architecture doc and mark Step 7 complete.**
|
||
Rewrite `Discriminating delegation`, reconcile the `pluggable escalation seam` and `Resolved direction` sections and the aspirational list, mark Step 7 `✅` (heading + `S7` node), link ADR 0007, and update the [#472] dispositions — in one commit so the doc is never half-reconciled.
|
||
Run the whole-file grep from *Invariants at risk* to confirm no stale framing remains, and verify the four Mermaid diagrams still render.
|
||
Commit: `docs(pi-permission-system): reconcile architecture with ADR 0007 and mark Phase 11 Step 7 (#591)`.
|
||
|
||
Marking Step 7 completes all seven Phase 11 steps.
|
||
Flipping the Phase 11 heading to `(complete)` and extracting its detail to `history/phase-11-*.md` is a distinct phase-close activity (the pattern Phases 9–10 follow), out of scope here — route it to `/finish-phase` as [#581] did.
|
||
|
||
## Risks and Mitigations
|
||
|
||
- **Risk: another transcription-not-decision slip.**
|
||
Mitigated: the design above was settled interactively (chain model, injected query, opt-in activation, config split) rather than lifted from the reverted prose; the ADR records *why* each fork went the way it did (rejected alternatives), which is the deliberation an ADR exists to carry.
|
||
- **Risk: the ADR over-commits implementation detail that belongs to [#472].**
|
||
Mitigated: provider/prompt/threshold/timeout, the slice decomposition, and the downstream package are explicit accepted-limitations deferred to [#472]; the ADR settles architecture and safety envelope only.
|
||
- **Risk: stale architecture-doc prose survives reconciliation (the [#581] failure).**
|
||
Mitigated: the *Invariants at risk* whole-file grep and the pre-completion reviewer's cross-doc check.
|
||
- **Risk: scope creep into building the chain.**
|
||
Mitigated: Non-Goals fences this to docs; no `src/`/`test/` change; [#472] implements.
|
||
|
||
## Open Questions
|
||
|
||
- **[#472]'s implementation decomposition.**
|
||
This design is materially larger than [#472]'s original "support a case-by-case judge" framing (a chain refactor of the Authorizer spine + `defer` verdict + named registration + injected `PermissionQuery` + config + two judge slices + a new downstream orchestrator package).
|
||
The next `/plan-improvements` pass sequences this: [#472]'s decomposition (chain infrastructure; deny-first slice; allow-capable slice) plus the dogfood extension become roadmap-step candidates, and the dogfood-extension issue is filed there — deferred deliberately, not filed speculatively here.
|
||
The dogfood extension lives in this monorepo as a new package (per the AGENTS.md new-package checklist), settled during planning.
|
||
- **Terminal-replacement registration.**
|
||
Registering a backend *as* the terminal authority (a chat-bot / remote reviewer replacing the human) is the chain seam's other role, deferred until a real non-subagent backend needs it; noted in the ADR as future, not filed.
|
||
|
||
[#267]: https://github.com/gotgenes/pi-packages/issues/267
|
||
[#296]: https://github.com/gotgenes/pi-packages/issues/296
|
||
[#309]: https://github.com/gotgenes/pi-packages/issues/309
|
||
[#472]: https://github.com/gotgenes/pi-packages/issues/472
|
||
[#509]: https://github.com/gotgenes/pi-packages/issues/509
|
||
[#581]: https://github.com/gotgenes/pi-packages/issues/581
|