docs: add agent guidance and permission research docs

- add AGENTS.md and docs for issue tracker, triage labels, and domain docs
- add CONTEXT.md glossary for the permission authorization domain
- add research report on context ownership for forwarded bash asks
- ignore .pi-subagents and .codegraph directories
- remove pi-permission-ai-judge from settings packages
This commit is contained in:
2026-08-09 00:23:24 +08:00
parent f62a073de7
commit fba79504ac
8 changed files with 309 additions and 1 deletions
+4
View File
@@ -141,3 +141,7 @@ dist
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/
# Vibe coding
.pi-subagents/
.codegraph/
-1
View File
@@ -1,5 +1,4 @@
{
"packages": [
"../packages/pi-permission-ai-judge"
]
}
+17
View File
@@ -0,0 +1,17 @@
# AGENTS.md
Guidance for AI agents working in this repo.
## Agent skills
### Issue tracker
Issues are tracked as Plane work items in the `pi-extensions` (`PIEXTENSIO`) project, via the Plane MCP tools. See `docs/agents/issue-tracker.md`.
### Triage labels
Five canonical triage roles map to labels of the same name (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
### Domain docs
Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
+41
View File
@@ -0,0 +1,41 @@
# Permission Authorization
This context describes how ambiguous coding-agent operations are reviewed before they may execute.
## Language
**Deterministic Permission Policy**:
The rule-based authority that classifies an operation as allowed, denied, or requiring a decision.
_Avoid_: Static judge
**Authorization Judge**:
An independent reviewer that proposes a verdict for an operation the Deterministic Permission Policy could not decide.
_Avoid_: Bash parser, safety classifier
**Shadow Mode**:
An observation mode in which an Authorization Judge records a verdict without changing whether the operation executes.
_Avoid_: Dry run
**Enforce Mode**:
An authority mode in which selected Authorization Judge verdicts may directly determine whether an operation executes.
_Avoid_: Production mode
**Defer**:
A verdict stating that the available information or the judge itself is insufficient to decide, leaving the decision to the next authority.
_Avoid_: Deny, error
**False Allow**:
A Shadow Mode outcome in which the Authorization Judge proposes approval and the human reviewer rejects the same permission request.
_Avoid_: False positive
**Requesting Session**:
The session in which the operation requiring authorization originated. For a forwarded request, this is the child session.
_Avoid_: Current session
**Serving Session**:
The authority-bearing session that resolves a forwarded request and runs its configured Authorization Judge before the terminal human authority.
_Avoid_: Parent context, current session
**Conversation Owner**:
The session whose conversation entries are supplied to an Authorization Judge. It may differ from the Requesting Session for forwarded requests.
_Avoid_: Requester
+37
View File
@@ -0,0 +1,37 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in.
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
## File structure
Single-context repo (this repo):
```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-<decision>.md
│ └── 0002-<decision>.md
└── packages/
```
(If this repo ever grows into a genuinely large multi-package monorepo, switch to the multi-context layout: a root `CONTEXT-MAP.md` pointing at a `CONTEXT.md` per package, with `docs/adr/` for system-wide decisions and per-package ADR dirs. For now one context covers everything.)
## Use the glossary's vocabulary
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0001 (<decision>) — but worth reopening because…_
+56
View File
@@ -0,0 +1,56 @@
# Issue tracker: Plane
Issues and specs for this repo live as **work items** in the **`pi-extensions`** project (`PIEXTENSIO`) on Plane. Use the **Plane MCP tools** for all operations — these are available to skills running in this repo. There is no `gh` / git-remote coupling; Plane is independent of this repo's GitHub remote.
## Project & workspace
- **Project**: `pi-extensions` — identifier `PIEXTENSIO`, id `270cee3f-cf84-48c8-bd4f-0882689f2a87`.
- All new issues go into this project. Look the id up with `plane_list_projects` if it ever changes.
## State vocabulary
States in this project (reference by name; resolve the id with `plane_list_states`):
| State | Group | Use for |
| --------- | ---------- | --------------------------------------------- |
| Backlog | backlog | default landing state for new issues |
| Todo | unstarted | accepted, queued for work |
| In Progress | started | actively being worked |
| Done | completed | finished |
| Cancelled | cancelled | discarded / wontfix-via-state |
A "closed" issue = `Done` or `Cancelled` (group `completed` / `cancelled`). "Open" = everything else (`plane_get_pql_reference` → `stateGroup IN openStates()`).
## Conventions
- **Create an issue**: `plane_create_work_item(project_id, name, description_html=...)`. It lands in `Backlog` by default. For triage, also attach the `needs-triage` label (create the label with `plane_create_label` on first use — none exist yet).
- **Read an issue**: `plane_retrieve_work_item(project_id, work_item_id, expand="assignees,labels,state")` — or `plane_retrieve_work_item_by_identifier("PIEXTENSIO-<n>")` when you only have the `PIEXTENSIO-N` id.
- **List issues**: `plane_list_work_items(project_id, pql=..., expand="labels,state")`. Resolve label/state names to ids first (`plane_list_labels`, `plane_list_states`). PQL examples: `stateGroup IN openStates()`; `priority = "high"`; `labels = "<label-uuid>"`. Call `plane_get_pql_reference` for full syntax.
- **Search by text**: `plane_search_work_items(query="...")` — matches name, sequence id, and project identifier (not the description body).
- **Comment on an issue**: `plane_create_work_item_comment(project_id, work_item_id, comment_html="...")`.
- **Apply / remove a label**: `plane_manage_work_item_label(project_id, work_item_id, add_label_id=..., remove_label_id=...)` — add/remove one label without replacing the list.
- **Change state**: `plane_update_work_item(project_id, work_item_id, state=<state-id>)`.
- **Close**: set state to `Done` (or `Cancelled` for wontfix).
## Pull requests as a triage surface
**PRs as a request surface: no.** PRs live on GitHub (`SikongJueluo/pi-extensions`) but are **not** treated as triage tickets. Only Plane work items go through triage. Flip this to `yes` and describe the GitHub-PR flow if you ever want external PRs in the queue.
## When a skill says "publish to the issue tracker"
Create a Plane work item via `plane_create_work_item` in the `pi-extensions` project.
## When a skill says "fetch the relevant ticket"
`plane_retrieve_work_item_by_identifier("PIEXTENSIO-<n>", expand="assignees,labels,state")`, plus `plane_list_work_item_comments` to read the discussion.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single work item with **child** work items as tickets.
- **Map**: a single work item labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. Create with `plane_create_work_item`, then `plane_create_label(project_id, "wayfinder:map", ...)` if the label doesn't exist, then attach it.
- **Child ticket**: a work item whose `parent` is the map — `plane_create_work_item(..., parent=<map-id>)` or `plane_update_work_item(parent=<map-id>)`. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, assign the driving dev via `plane_manage_work_item_assignee`.
- **Blocking**: native work-item relations are gated on this workspace's plan, so use a **body-text fallback** — put `Blocked by: PIEXTENSIO-<n>, PIEXTENSIO-<n>` at the top of the child body. A ticket is unblocked when every blocker is `Done`/`Cancelled`.
- **Frontier query**: list the map's open children via `plane_list_work_items` filtered to the map's children and `stateGroup IN openStates()`; drop any with an open `Blocked by` line or an assignee; first in created order wins.
- **Claim**: `plane_manage_work_item_assignee(project_id, work_item_id, add_user_id=<me>)` — resolve `<me>` with `plane_get_me`. The session's first write.
- **Resolve**: `plane_create_work_item_comment` with the answer, then `plane_update_work_item(state=<Done-id>)`, then append a context pointer (gist + link) to the map's Decisions-so-far (edit the map's description via `plane_update_work_item`).
+19
View File
@@ -0,0 +1,19 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker (Plane, project `pi-extensions` / `PIEXTENSIO`).
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
## Plane notes
- These labels **do not exist yet** in the `pi-extensions` project. The `triage` skill should create each label with `plane_create_label(project_id, name=...)` on first use (Plane has no predefined triage labels), then attach it with `plane_manage_work_item_label`. Resolve label names to ids via `plane_list_labels`.
- `wontfix` can be expressed either as a label **or** by moving the work item to the `Cancelled` state — prefer the label for explicit triage intent, and state for final closure.
- Edit the right-hand column to match any other vocabulary you adopt later.
+135
View File
@@ -0,0 +1,135 @@
# Research: context ownership for forwarded and subagent Bash asks
## Scope
This report describes the installed runtime inspected on 2026-08-08:
- `@gotgenes/pi-permission-system` 24.0.0
- `pi-subagents` 0.43.0
- the current `packages/pi-permission-ai-judge/src/index.ts`
Only first-party source, package documentation, and Pi API declarations are used. Product implementation remains out of scope.
## Executive answer
An external permission-system `Authorizer` callback receives only `(details, query, log)`. It receives **no ask-time `ExtensionContext`**. Any conversation it reads therefore belongs to the extension lifecycle context captured when that callback was registered.
For a direct primary-session Bash ask, the root permission-system composes its configured authorizer links before `LocalUserAuthorizer`. A Judge registered by the UI-present root may safely identify the captured `sessionManager` as the primary conversation.
A child ask has two distinct possible authorizer-chain phases:
1. The child's own permission session composes child-local links before its terminal `ParentAuthorizer`.
2. If the child reaches `ParentAuthorizer`, the request is written to the serving session. The serving `ForwardedRequestServer` resolves serving policy and, when the result is still `ask`, composes the serving session's links before its terminal authorizer.
A future Judge must therefore bind only from the UI-present root/service-owning lifecycle context. Otherwise a separately registered child link could judge with child context, or a registered in-process child could capture child context while accidentally registering its callback into the parent's process-global service.
For a forwarded ask handled by the root link, the captured conversation is the **serving root's conversation**, not the requester's child transcript. Link-visible child provenance is `details.forwarding`; there is no cross-session API for reading the child's conversation.
## Proven call flows
### Direct primary Bash ask
1. The primary tool call reaches the primary permission gate.
2. Deterministic policy returns `ask`.
3. `AuthorizerSelection.escalate()` resolves configured links and composes them before the selected terminal.
4. The root Judge link may return `allow`, `deny`, or `defer`.
5. `defer` continues to `LocalUserAuthorizer`; it is not a denial.
The Judge callback may use a root `ExtensionContext` captured at `session_start`, but that context is closure state rather than an argument to `authorize()`.
### Child ask forwarded to the root
1. The child's deterministic policy returns `ask`.
2. The child's `AuthorizerSelection` first runs links registered in that child session, if any.
3. If all links defer or no link exists, the child terminal `ParentAuthorizer` resolves a target session, writes a `ForwardedPermissionRequest`, and polls for a response.
4. The UI-present root's `ForwardingManager` drains the target session inbox.
5. `ForwardedRequestServer` resolves the child-fixed access intent against serving policy.
6. A serving-policy `allow` or `deny` completes without the root chain. A serving-policy `ask` is projected into `PromptPermissionDetails` and passed to the root `AuthorizerSelection`.
7. Root links run before `LocalUserAuthorizer`. A root Judge therefore sees root-captured conversation plus forwarded provenance, not child conversation.
8. The root writes the decision; the child receives it and resumes or blocks.
`ParentAuthorizer` always uses filesystem request/response transport, including registered in-process children. The registry resolves identity and parentage; it is not an in-memory response fast path.
### Nested children
Depth alone does not determine the result. A request targeting a headless immediate parent will normally remain unserved and time out. A request routed directly to the UI-present root can still be served. The server warns on a detected multi-hop mismatch but deliberately continues processing a correctly targeted request.
## What an authorizer link can actually see
| Evidence | Direct primary ask | Parent-side forwarded ask |
|---|---|---|
| `details.forwarding` | absent | requester agent/session, possibly nullable for version-skew data |
| `details.agentName` | active primary agent | requesting child agent |
| `details.surface` / `details.value` | usually derived fields are absent on the internal details object | child's explicit display projection |
| `details.accessIntent` | gate surface, match values, boundary value | same child-fixed facts projected by the server |
| `details.command` | winning Bash command unit | absent |
| `details.message` | formatted prompt; includes full Bash input text when it differs from the winning unit | same child-formatted prompt, prefixed with child identity by the server |
| raw tool input | unavailable | unavailable |
| wire `principal` / `requesterCwd` | unavailable | deliberately withheld from chain links |
| ask-time `ExtensionContext` | unavailable | unavailable |
For a Bash ask, `details.accessIntent.matchValues[0]` and direct `details.command` identify the permission-system command unit that caused `ask`. The full original Bash input is not preserved as a structured authorizer field; when it differs, it survives only inside the formatted `details.message` text. The later input-contract decision must not pretend that `details.command` is the full raw Bash invocation or silently parse an authorization-critical command from prose.
## Registration and context-ownership hazards
The permissions service is stored under `Symbol.for(...)` on `globalThis` and registration stores an authorizer callback in the service owner's `AuthorizerRegistry`.
- A normal UI root publishes its service.
- A registered in-process child does not publish its own service, but still emits the ready event.
- Consequently, a consumer extension loaded in that child can resolve the parent's process-global service. If it registers, its child-captured closure is stored in the parent's registry. If the root already registered the same name, duplicate registration throws.
- A separate child process has separate `globalThis` state and may have its own service lifecycle; registering there would create a child-local link with child-captured context.
Therefore successful `getPermissionsService()` lookup does **not** prove that the current extension instance owns that service's session context.
## Safe v0.1 ownership contract
1. Register the AI Judge only from a lifecycle context proven to be the UI-present, non-subagent root. Do not register from headless or subagent lifecycle contexts.
2. Snapshot and retain that root context/session identity as registration provenance. The callback must not infer ownership merely from the process-global service lookup.
3. For `details.forwarding` absent, treat the captured root conversation as the requester's current conversation.
4. For `details.forwarding` present, label the origin as forwarded and treat the captured conversation only as **serving-root user-intent context**. Never describe it as the child's conversation and never infer child-local dialogue that is not present.
5. Preserve `details.forwarding.requesterSessionId` and `requesterAgentName` as provenance. If forwarded provenance is missing, blank, or `unknown`, the Judge must return `{ kind: "defer" }`.
6. If registration provenance is not demonstrably the UI root, the captured session changed unexpectedly, or context ownership otherwise cannot be established, return `{ kind: "defer" }`.
7. Judge `defer` means continue through the authorizer chain to human or terminal authority. The Judge must not synthesize `confirmationUnavailable`; only terminal authority owns unavailable-authority denial.
8. Ensure a logical forwarded request produces at most one Judge prediction. Root-only registration is the v0.1 mechanism that avoids child-then-parent duplicate judgments.
Whether serving-root conversation is sufficient evidence to grant a later Enforce-mode `allow` for a forwarded child request remains a product decision for **Define the exact authorization input and conversation contract**. The facts established here are that root user intent is available, child conversation is not, and the two must not be conflated.
## Implications for the later input-contract ticket
- Add an explicit origin such as `local` or `forwarded_subagent`.
- Record the conversation owner separately from requester identity.
- For forwarded asks, carry only the link-visible projection: `details.forwarding`, `details.surface/value`, `details.accessIntent`, and the formatted message. Do not claim access to the raw `ForwardedPermissionRequest`, `principal`, or `requesterCwd`.
- Define structured handling for both the triggering Bash command unit and the unavailable full raw Bash invocation. If v0.1 requires the full invocation as structured data, permission-system must expose it; prose parsing is not an equivalent contract.
- Keep the user-intent priority explicit: root user messages are authoritative; child agent identity and the command are evidence, not instructions.
## Evidence index
| Claim | Primary source |
|---|---|
| Link callback receives only `details`, `query`, and `log` | `@gotgenes/pi-permission-system/src/authority/authorizer.ts`, `Authorizer.authorize` |
| Links compose before each session's terminal | `src/authority/authorizer-selection.ts`, `AuthorizerSelection.escalate`; `src/authority/authorizer-chain.ts`, `composeAuthorizerChain` |
| `defer` continues; `deny` is decisive | `src/authority/authorizer-chain.ts`, `decideFromVerdict` |
| Child terminal writes and polls forwarded request | `src/authority/approval-escalator.ts`, `ParentAuthorizer.waitForForwardedApproval` and `pollForForwardedResponse` |
| Root policy resolves before root escalation | `src/authority/forwarded-request-server.ts`, `resolveDecision` |
| Forwarded details projection and disclosure boundary | `src/authority/forwarded-request-server.ts`, `buildForwardedAskDetails` and `toAccessFacts` |
| Link-visible details fields | `src/authority/permission-prompter.ts`, `PromptPermissionDetails` |
| Full Bash command appears only in formatted message when distinct | `src/permission-prompts.ts`, `formatAskPrompt`; `src/handlers/gates/tool.ts`, `describeToolGate` |
| UI display derives from winning `details.command` | `src/permission-ui-prompt.ts`, `buildUiPrompt` and `directValue` |
| Service is process-global | `src/service.ts`, `SERVICE_KEY`, `publishPermissionsService`, `getPermissionsService` |
| Registered in-process child skips publication but emits ready | `src/service-lifecycle.ts`, `PermissionServiceLifecycle.activate` |
| Duplicate authorizer names throw | `src/authority/authorizer-registry.ts`, `AuthorizerRegistry.register` |
| Only UI non-subagent sessions poll forwarded inboxes | `src/authority/forwarding-manager.ts`, `ForwardingManager.start` |
| Registry-first, environment-second target resolution | `src/authority/permission-forwarding.ts`, `resolvePermissionForwardingTargetSessionId` |
| Multi-hop mismatch warns but continues | `src/authority/forwarded-request-server.ts`, `warnOnMultiHop` |
| Current Judge captures no context | `packages/pi-permission-ai-judge/src/index.ts`, current `session_start` handler and registered callback |
## Residual verification gap
The source establishes the registration hazard, but no runtime probe was run to observe extension lifecycle ordering when the future AI Judge is loaded into a registered in-process child. Before implementation acceptance, run one focused probe that logs, without conversation content:
- registering extension session ID and `hasUI`;
- service-owning session ID if exposed;
- callback invocation origin (`local`/`forwarded`);
- forwarded requester session ID.
The probe should confirm that the root-only guard prevents child registration and that one forwarded Bash ask creates exactly one Judge prediction at the serving root.