@preapexis/piexis 0.2.2 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +85 -131
  2. package/apps/server/src/index.ts +97 -0
  3. package/apps/server/src/session-manager.ts +95 -0
  4. package/apps/server/src/workspace.ts +39 -0
  5. package/{tsconfig.json → apps/server/tsconfig.json} +3 -3
  6. package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
  7. package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
  8. package/apps/web/dist/index.html +3 -0
  9. package/bin/piexis-web.mjs +24 -0
  10. package/extensions/agents.ts +205 -0
  11. package/extensions/ask-question.ts +54 -95
  12. package/extensions/recommended-tools.ts +62 -0
  13. package/extensions/ui.ts +204 -58
  14. package/extensions/webui.ts +45 -0
  15. package/lib/agent-config.ts +55 -0
  16. package/lib/agents.ts +208 -0
  17. package/lib/question-model.ts +98 -0
  18. package/lib/question-ui.ts +214 -0
  19. package/lib/recommended-tools.ts +119 -0
  20. package/lib/subagent-runner.ts +146 -0
  21. package/lib/ui-tips.json +41 -0
  22. package/package.json +41 -14
  23. package/packages/pi-integration/src/rpc-client.ts +99 -0
  24. package/packages/shared/src/index.ts +27 -0
  25. package/prompts/init.md +10 -2
  26. package/prompts/summary.md +7 -6
  27. package/schemas/agent.schema.json +48 -0
  28. package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
  29. package/skills/caveman/LICENSE-MIT +21 -0
  30. package/skills/caveman/NOTICE +10 -0
  31. package/skills/caveman/SKILL.md +89 -0
  32. package/skills/find-skills/SKILL.md +141 -0
  33. package/skills/ponytail/LICENSE +21 -0
  34. package/skills/ponytail/NOTICE +9 -0
  35. package/skills/ponytail/SKILL.md +52 -0
  36. package/themes/midnight.json +81 -0
  37. package/.github/workflows/publish.yml +0 -35
  38. package/AGENTS.md +0 -58
  39. package/eval/README.md +0 -72
  40. package/eval/permission-cases.json +0 -28
  41. package/eval/permissions.mjs +0 -103
  42. package/extensions/background-tasks.ts +0 -433
  43. package/extensions/background-worker.ts +0 -50
  44. package/extensions/glob.ts +0 -32
  45. package/extensions/mode.ts +0 -397
  46. package/lib/bash-approvals.ts +0 -113
  47. package/lib/bash-policy.ts +0 -164
  48. package/lib/mode-policy.ts +0 -174
  49. package/lib/path-policy.ts +0 -182
  50. package/lib/permission-classifier.ts +0 -128
  51. package/lib/permission-config.ts +0 -91
  52. package/lib/permission-denials.ts +0 -32
  53. package/lib/permission-intent.ts +0 -128
  54. package/lib/permission-taxonomy.ts +0 -15
  55. package/lib/task-broker.ts +0 -65
  56. package/lib/task-checkout.ts +0 -39
  57. package/lib/task-git.ts +0 -44
  58. package/lib/task-patch.ts +0 -106
  59. package/lib/task-permissions.ts +0 -51
  60. package/lib/task-report.ts +0 -62
  61. package/lib/task-worktree-policy.ts +0 -56
  62. package/scripts/release.mjs +0 -32
  63. package/skills/claude-api/SKILL.md +0 -572
  64. package/skills/claude-api/csharp/claude-api/README.md +0 -362
  65. package/skills/claude-api/csharp/claude-api/batches.md +0 -14
  66. package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
  67. package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
  68. package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
  69. package/skills/claude-api/curl/examples.md +0 -256
  70. package/skills/claude-api/curl/managed-agents.md +0 -371
  71. package/skills/claude-api/go/claude-api/README.md +0 -188
  72. package/skills/claude-api/go/claude-api/files-api.md +0 -23
  73. package/skills/claude-api/go/claude-api/streaming.md +0 -43
  74. package/skills/claude-api/go/claude-api/tool-use.md +0 -220
  75. package/skills/claude-api/go/managed-agents/README.md +0 -564
  76. package/skills/claude-api/java/claude-api/README.md +0 -238
  77. package/skills/claude-api/java/claude-api/files-api.md +0 -27
  78. package/skills/claude-api/java/claude-api/streaming.md +0 -24
  79. package/skills/claude-api/java/claude-api/tool-use.md +0 -228
  80. package/skills/claude-api/java/managed-agents/README.md +0 -443
  81. package/skills/claude-api/php/claude-api/README.md +0 -174
  82. package/skills/claude-api/php/claude-api/batches.md +0 -15
  83. package/skills/claude-api/php/claude-api/files-api.md +0 -13
  84. package/skills/claude-api/php/claude-api/streaming.md +0 -27
  85. package/skills/claude-api/php/claude-api/tool-use.md +0 -253
  86. package/skills/claude-api/php/managed-agents/README.md +0 -443
  87. package/skills/claude-api/python/claude-api/README.md +0 -565
  88. package/skills/claude-api/python/claude-api/batches.md +0 -198
  89. package/skills/claude-api/python/claude-api/files-api.md +0 -170
  90. package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
  91. package/skills/claude-api/python/claude-api/streaming.md +0 -179
  92. package/skills/claude-api/python/claude-api/tool-use.md +0 -627
  93. package/skills/claude-api/python/managed-agents/README.md +0 -335
  94. package/skills/claude-api/ruby/claude-api/README.md +0 -134
  95. package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
  96. package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
  97. package/skills/claude-api/ruby/managed-agents/README.md +0 -394
  98. package/skills/claude-api/shared/admin-api.md +0 -179
  99. package/skills/claude-api/shared/agent-design.md +0 -101
  100. package/skills/claude-api/shared/anthropic-cli.md +0 -269
  101. package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
  102. package/skills/claude-api/shared/cost-optimization.md +0 -233
  103. package/skills/claude-api/shared/error-codes.md +0 -262
  104. package/skills/claude-api/shared/live-sources.md +0 -166
  105. package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
  106. package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
  107. package/skills/claude-api/shared/managed-agents-core.md +0 -359
  108. package/skills/claude-api/shared/managed-agents-environments.md +0 -226
  109. package/skills/claude-api/shared/managed-agents-events.md +0 -290
  110. package/skills/claude-api/shared/managed-agents-memory.md +0 -201
  111. package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
  112. package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
  113. package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
  114. package/skills/claude-api/shared/managed-agents-overview.md +0 -77
  115. package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
  116. package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
  117. package/skills/claude-api/shared/managed-agents-tools.md +0 -504
  118. package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
  119. package/skills/claude-api/shared/model-migration.md +0 -1872
  120. package/skills/claude-api/shared/models.md +0 -142
  121. package/skills/claude-api/shared/platform-availability.md +0 -53
  122. package/skills/claude-api/shared/prompt-audit.md +0 -223
  123. package/skills/claude-api/shared/prompt-caching.md +0 -292
  124. package/skills/claude-api/shared/token-counting.md +0 -56
  125. package/skills/claude-api/shared/tool-use-concepts.md +0 -526
  126. package/skills/claude-api/typescript/claude-api/README.md +0 -405
  127. package/skills/claude-api/typescript/claude-api/batches.md +0 -106
  128. package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
  129. package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
  130. package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
  131. package/skills/claude-api/typescript/managed-agents/README.md +0 -359
  132. package/skills-lock.json +0 -359
  133. package/tests/auto-permissions.test.mjs +0 -315
  134. package/tests/background-tasks.test.mjs +0 -27
  135. package/tests/background-worker.test.mjs +0 -60
  136. package/tests/bash-permissions.test.mjs +0 -276
  137. package/tests/glob.test.mjs +0 -31
  138. package/tests/mode.test.mjs +0 -62
  139. package/tests/permission-evaluation.test.mjs +0 -70
  140. package/tests/permission-foundation.test.mjs +0 -194
  141. package/tests/permission-policy.test.mjs +0 -158
  142. package/tests/runtime/README.md +0 -51
  143. package/tests/runtime/harness.mjs +0 -190
  144. package/tests/runtime/permissions.test.mjs +0 -192
  145. package/tests/runtime/tui.test.mjs +0 -60
  146. package/tests/skills-routing.test.mjs +0 -51
  147. package/tests/task-integration.test.mjs +0 -324
  148. package/tests/task-permissions.test.mjs +0 -178
  149. package/tests/task-worktree-policy.test.mjs +0 -53
package/README.md CHANGED
@@ -1,180 +1,145 @@
1
1
  # PieXis
2
2
 
3
- PieXis is a Pi package with permission/approval controls, background Git-worktree tasks, prompts, skills, and UI resources. It requires Node.js 22.19+ and a current Pi release (local runtime checks passed with Pi 0.85.1 and 0.87.1 on Linux).
3
+ PieXis is a Pi package with interactive questions, coding skills, prompts, and terminal UI resources. It requires Node.js 22.19+ and a current Pi release.
4
4
 
5
- > [!WARNING]
6
- > **PieXis permission controls are not a security boundary.** They help prevent accidental or unapproved agent tool actions; they do **not** provide OS-level sandboxing, filesystem isolation, workspace-only filesystem enforcement, protected-path enforcement, network isolation, or network allowlisting. Run Pi in an appropriate container, VM, sandbox, or restricted user account when executing untrusted code or allowing unattended/high-autonomy agents.
7
-
8
- ## Install and use
5
+ ## Installation
9
6
 
10
7
  ```sh
11
8
  pi install npm:@preapexis/piexis
12
9
  ```
13
10
 
14
- Installing the package loads its declared extensions, prompts, skills, and theme. Loading only `extensions/mode.ts` loads only the permission extension. Restart Pi or run `/reload` after installation.
11
+ Restart Pi or run `/reload` after installation. The package loads AskQuestion, agent profiles/subagents, the custom header/footer, automatic recommended-tool setup, prompts, skills, and themes.
15
12
 
16
- Use `Alt+M` while the agent is idle to cycle Manual → Accept Edits → Plan → Auto (when configured). `/plan [task]` enters Plan mode. Start `dontAsk` and `bypassPermissions` only with `--permission-mode`; `--dangerously-skip-permissions` selects `bypassPermissions`.
13
+ ## Resources
17
14
 
18
- ## Permission modes
15
+ - **AskQuestion** provides keyboard-driven single/multi selection and text questions in the interactive TUI, with validation, answer review, and cancellation. It requires terminal input and output.
16
+ - **UI** shows project information, agent activity, Git branch, model, and context usage. The startup header shows three random, category-labelled tips from `lib/ui-tips.json`, preferring different categories and staying stable across redraws. Tips cycle without repeats until the list is exhausted within the extension runtime; `/reload` starts a new cycle. Each JSON entry has `category` and `text` fields; edit them to change the recommendations.
17
+ - **Recommended tools** offers to enable native `grep`, `find`, and `ls` for the project at startup; no slash command is needed.
18
+ - **Agents** provides `plan`, `orchestrator`, `debug`, `ask`, and `code` profiles, all callable through the `subagent` tool. Small delegated tasks can automatically use a cheaper model.
19
+ - **Skills** cover implementation, debugging, review, design, refactoring, validation, and document workflows. Bundled **caveman** adds concise chat style; **ponytail** guides the smallest complete coding change. Only their core instructions are included, not upstream hooks or companion skills. Source revisions and licenses live beside each skill.
20
+ - **Prompts** include `/init`, `/commit`, and `/summary`. `/init` adds workflow guidance to `AGENTS.md`: caveman for responses, grilling for clarification questions, and ponytail for building. These are agent instructions, not runtime enforcement.
19
21
 
20
- | Mode | Behavior |
21
- | --- | --- |
22
- | **Manual** (`default`) | Project-local ordinary reads/searches, a tiny harmless Bash-builtin set, and `AskQuestion` are pre-approved; other actions need approval. **Allow once** applies only to the current call. |
23
- | **Accept Edits** (`acceptEdits`) | Also pre-approves ordinary project-local `edit` and `write` calls. Bash outside the harmless-builtin set, external paths, and sensitive paths still require approval. |
24
- | **Plan** (`plan`) | Allows project-local ordinary reads, task status/diff inspection/cancellation, and `AskQuestion`; Bash, external/sensitive path-tool reads, task creation/apply/cleanup, and file mutations are blocked. |
25
- | **Auto** (`auto`) | Directly permits ordinary project-local reads, `edit`, and `write`. Sensitive paths, protected configuration edits, uncertain recursive searches, and malformed or unsupported paths/tools remain denied. Bash outside the harmless-builtin set and eligible narrow session grants, plus remaining known external actions, receive intent-aware model review. Missing authorization, timeout, error, or invalid review denies the action. |
26
- | **Don't Ask** (`dontAsk`) | Permits only pre-approved reads/questions and harmless Bash builtins; denies other calls without a dialog. Launch-only. |
27
- | **Bypass Permissions** (`bypassPermissions`) | Skips **PieXis extension-level permission checks**. It does not enter or escape a sandbox, and does not override OS permissions or other extensions. Task scope/confirmation safeguards remain. Launch-only. |
22
+ - **Themes** include warm **orange** and **midnight**, a dark slate palette with cyan, violet, and mint accents. Run `/reload`, then choose a palette in `/settings` → **Theme**.
23
+
24
+ Caveman and ponytail ship with this package; no separate installation is needed. Their upstream standalone installation commands are:
25
+
26
+ ```sh
27
+ npx skills add https://github.com/juliusbrussee/caveman --skill caveman
28
+ npx skills add https://github.com/dietrichgebert/ponytail --skill ponytail
29
+ ```
28
30
 
29
- Approval UI is fail-closed: calls requiring approval are denied when no permission UI is available. Relative paths resolve against the tool's current working directory, independently of the repository boundary. Both requested and resolved paths are checked; a symlink that resolves outside the project is treated as external, and malformed or unresolvable paths are denied. This is a permission policy, not filesystem sandboxing.
31
+ Avoid loading standalone copies alongside the bundled skills, which can cause name collisions. In Pi, explicitly invoke the bundled skills with `/skill:caveman` and `/skill:ponytail`.
30
32
 
31
- `grep`, `find`, and `glob` receive a bounded, metadata-only subtree preflight. Sensitive descendants, symlinks, external trees, inspection errors, or exhaustion of the 2,000-entry/directory budget require fresh one-time approval in Manual/Accept Edits and are denied in Auto/Plan/Don't Ask. No file contents are read by this preflight. It deliberately does not assume ignore rules exclude sensitive files; large repositories (including their `.git` contents) may require narrowing the search directory. It does not filter tool results or prevent filesystem races.
33
+ ## Automatic tool setup
32
34
 
33
- Credential categories include environment files, common credential files, SSH/cloud/key directories, and Pi authentication files. Path-tool mutations (`edit`/`write`) beneath `.git` and Pi's configuration directory (`CONFIG_DIR_NAME`, normally `.pi`) require explicit approval, including settings, permission configuration, and extensions. Ordinary non-secret configuration reads remain permitted. `.env.example` is not automatically assumed secret-free: explicitly approve that exact file in Manual/Accept Edits without granting access to other environment files.
35
+ On startup in a trusted project with terminal input/output, Piexis offers any pending setup: available, inactive `grep`, `find`, and `ls` tools, or a missing project agent configuration when the agent extension is loaded. Earlier tool approval does not hide the new agent setup; if tools are already configured, the prompt offers only `.pi/agent.json` creation and leaves tool settings unchanged:
34
36
 
35
- Session approvals are in-memory and never persisted. Filesystem approvals remain scoped to the file/directory and exact tool operation (`read`, `write`, `edit`, etc.). Broad grants do not authorize credential paths or protected path-tool configuration edits. Recursive-boundary requests offer only one-time approval or denial. Auto now honors eligible exact-file/directory grants and fingerprinted Bash grants after non-overridable checks, but only for the same verified current request. Whole-tool/project and legacy executable grants never bypass Auto review. Session approvals are cleared on branch navigation and session replacement.
37
+ - **Enable** saves the project choice and tool additions in `.pi/settings.json`, then activates them in the current session without removing active tools or extensions. It also creates `.pi/agent.json` if neither that file nor legacy `.pi/agents.json` exists. The scaffold uses the current model for `model`, `small_model`, and `subagent_model`, preserves user-level agent overrides, and populates all five `subagent_variant_overrides` with `model` and `effort`. Default effort is `high` for plan/orchestrator/debug, `low` for ask, and `medium` for code. Existing agent configuration is never overwritten. If creation fails or no model is selected, tools remain enabled and a warning explains how to configure agents manually.
38
+ - **Not now** (the default) or Esc changes nothing. The prompt is shown at most once per project per extension runtime; a subsequent startup or reload may offer it again.
39
+ - **Don't ask again** records `"disabled"` only for features offered in that prompt: `piexis.recommendedTools` for tool setup and `piexis.agentSetup` for agent setup. It changes neither tools nor agent files. Approval records `"enabled"`. An existing `agent.json` or legacy `agents.json` completes agent setup; if creation failed or the file was removed, startup offers it again unless `agentSetup` is `"disabled"`. Remove the corresponding disabled preference to allow another offer.
36
40
 
37
- The advanced Manual/Accept Edits option remains **Allow ALL bash actions for this session**. This is deliberately broad: it can still modify configuration or access host resources. The bounded Bash checks do not constrain arbitrary program or extension internals. `AskQuestion` remains available in every mode, including after a denial stop.
41
+ User-level settings are never written. Other project settings and existing `piexis` keys are preserved. Modern Pi uses additive `defaultTools` entries such as `["+grep", "+find", "+ls"]`. Older Pi releases without additive support use a plain list of the four default tools plus the approved additions, only when no tool selection is configured.
38
42
 
39
- ### Permission decision order
43
+ Setup skips non-TUI/non-TTY runs, untrusted projects, and runs with CLI tool-selection overrides (`--tools`, `--exclude-tools`, `--no-tools`, or `--no-builtin-tools`, including their short forms). Explicit `defaultTools` allowlists, including an empty list, are left alone; they can still receive an agent-only setup offer. Additive `-tool` exclusions remain respected. Only Pi's available built-in tools are offered. Malformed or non-regular settings files and settings changed during the prompt are not overwritten; a warning explains why setup was skipped.
40
44
 
41
- `lib/mode-policy.ts` exposes the shared, UI-independent `evaluatePermission` function. Foreground mode handling and the parent task broker supply the mode, tool arguments, current directory, workspace boundary, and authorization context; actor identity itself grants no authority. Worker calls also require a live delegated authorization and pass capability/path-scope checks. Foreground grants are never inherited by workers. The host still validates tool schemas.
45
+ The setup extension is `extensions/recommended-tools.ts`; package resource filtering can omit it. Native `find` provides file discovery by glob pattern; no wrapper extension is needed.
42
46
 
43
- 1. `AskQuestion` retains its exception. Worker capability/path-scope checks precede the parent mode's Bypass exception.
44
- 2. Reject malformed inputs and unresolvable paths.
45
- 3. Apply Plan restrictions, bounded Bash analysis, and Auto non-overridable checks.
46
- 4. Match eligible scoped approvals and mode defaults. Auto rejects broad/stale grants; unsupported shell forms cannot match a reusable narrow Bash grant.
47
- 5. Return `require-approval`, or deny in Don't Ask. Model review and approval UI are handled by the extension and fail closed.
47
+ ## Agents and subagents
48
48
 
49
- Repository configuration currently selects models only; there are no repository-provided allow rules or trusted-environment grants. Future permission-widening settings must require user confirmation rather than trusting repository declarations. Background tools are classified individually. Their handlers authorize task creation with the complete brief and require explicit confirmation for actual patch application and validated cleanup; this is not a blanket allow for custom tools.
49
+ Use `/agent` to select a profile, `/agent plan` to switch directly, or `pi --agent orchestrator` at startup. **Alt+A** cycles `plan → orchestrator → debug → ask → code` while idle; cycling is blocked while an agent is running. The footer shows the active profile (`agent:code`) and, on wider terminals, the shortcut hint. `/hotkeys` lists the shortcut. `code` is the default. Profile choices follow the active session branch. Profiles add instructions rather than replacing repository guidance or changing the main session's tools:
50
50
 
51
- ## Configuration
51
+ | Profile | Purpose |
52
+ | --- | --- |
53
+ | `plan` | Explore and return a plan; no implementation |
54
+ | `orchestrator` | Delegate focused tasks and combine verified results |
55
+ | `debug` | Establish evidence, diagnose, fix, and verify |
56
+ | `ask` | Answer questions from code/documentation; no changes |
57
+ | `code` | Implement scoped changes and run checks |
52
58
 
53
- Launching PieXis does not create `.pi/` or project configuration. It uses in-memory defaults when `.pi/permission-modes.json` is absent. Run `/piexis-init` to explicitly create `.pi/settings.json` and `.pi/permission-modes.json`; existing files are never overwritten.
59
+ All five are callable with `subagent({ "agent": "ask", "task": "Locate the configuration loader" })`. The orchestrator chooses when to delegate; delegation is not an automatic pipeline. Each child receives its task, normal Pi resources, and profile instructions, **not** the parent's conversation. Supply necessary context explicitly. Children share the workspace, so calls are sequential; delegation stops at two child levels. A delegated orchestrator can call other subagents at the second level.
54
60
 
55
- `permission-modes.json` can select Auto's authenticated classifier and permitted background models:
61
+ Optional configuration lives in `~/.pi/agent/agent.json` (or `$PI_CODING_AGENT_DIR/agent.json`) and trusted-project `.pi/agent.json`. Legacy `agents.json` is still read when `agent.json` is absent in that scope; no files are renamed or deleted. Project fields override user fields; per-variant overrides merge by field. Accepting **Enable** during project setup creates the project scaffold only when no project agent configuration exists; user-level files are never written. Defaults use the current model unless user-level configuration specifies another model. New scaffolds set `delegate_exploration` to `false`; edit it to opt in. `/agent-config` creates a missing project config or fills only its missing settings with defaults, preserving every configured value. It uses the active model when it must choose a model. Generated files include `$schema` for editor validation; configuration reloads on each profile switch and delegation. Use exact `provider/model-id` strings from `/model`, not fuzzy aliases:
56
62
 
57
63
  ```json
58
64
  {
59
- "version": 1,
60
- "initialized": true,
61
- "classifier": {
62
- "provider": "anthropic",
63
- "model": "claude-haiku-4-5",
64
- "timeoutMs": 15000
65
- },
66
- "background": { "models": ["anthropic/claude-haiku-4-5"] }
65
+ "$schema": "https://raw.githubusercontent.com/VarunGaikwad/piexis/main/schemas/agent.schema.json",
66
+ "model": "provider/default-model",
67
+ "small_model": "provider/default-model",
68
+ "subagent_model": "provider/default-model",
69
+ "delegate_exploration": true,
70
+ "subagent_variant_overrides": {
71
+ "plan": { "model": "provider/default-model", "effort": "high" },
72
+ "orchestrator": { "model": "provider/default-model", "effort": "high" },
73
+ "debug": { "model": "provider/default-model", "effort": "high" },
74
+ "ask": { "model": "provider/default-model", "effort": "low" },
75
+ "code": { "model": "provider/default-model", "effort": "medium" }
76
+ }
67
77
  }
68
78
  ```
69
79
 
70
- `PI_PERMISSION_CLASSIFIER=provider/model` overrides the configured classifier. Configuration contains no credentials; Pi authentication is reused.
71
-
72
- ## Auto review policy and privacy
73
-
74
- Auto uses **deterministic policy → one model reviewer**, not a multi-model pipeline. Ordinary local reads/edits do not call the reviewer. Review compares the proposed action against the current user request and earlier user constraints, rather than treating ordinary-looking command names as sufficient authorization.
75
-
76
- | Review category | Rule |
77
- | --- | --- |
78
- | Ordinary development | Must serve the current requested task |
79
- | External filesystem | Intent must cover the external target and operation |
80
- | Destructive changes | Specific authorization for operation, target, and scope |
81
- | Credential exploration/exposure | Denied; normal use of an authenticated CLI is distinct |
82
- | Data egress | Authorization must cover destination, payload, and purpose |
83
- | Production/shared systems | Explicit target and operation required |
84
- | Privilege/security weakening | Denied in unattended Auto |
85
- | Safeguard bypasses | Explicit authorization for the particular bypass |
86
- | Opaque execution or ambiguity | Denied when effects/authorization cannot be established |
87
-
88
- The taxonomy lives in `lib/permission-taxonomy.ts`. Shell syntax is boundedly parsed; full program semantics remain model-assessed. These are permission-review policies, not guarantees that every shell effect will be detected. Every model allow must cite the current user message and any supporting user messages using valid IDs. Unknown categories, missing/currently invalid evidence, malformed output, abnormal completion, and provider failure fail closed. The host rejects allows in prohibited categories even if the model returns them. Auto does not add approval dialogs or change modes on denial. Repeated denials stop the run under the limits below.
89
-
90
- ### What is sent
91
-
92
- For reviewed actions, the configured provider receives:
93
- - Tool name and Bash command or requested filesystem path.
94
- - Working directory, workspace boundary, and deterministic path-policy findings.
95
- - Bounded, provenance-labelled user messages from the active branch.
96
- - Eligible file/directory approval scopes from that same current request when review is still needed. Invalid or stale Bash grants are not forwarded as authorization.
97
- - Bounded shell parsing results and risk findings; no script or Git configuration contents.
98
- - For task creation and worker review, the proposed task/brief, selected model, and delegated path/Bash scope, explicitly marked as untrusted action data—not user authorization.
99
-
100
- Other than the proposed delegation brief described above, it does **not** receive assistant prose/reasoning, raw tool-output history, worker result summaries, compaction summaries, file contents, edit/write replacement text, environment-variable objects, or Pi authentication credentials. The reviewer has no tool access. User prose, paths, commands, and approval strings can themselves contain secrets: recognition/redaction is best-effort, not a secret-detection guarantee. Do not use Auto when those details must not be disclosed to the selected provider. Recognizably secret-bearing or oversized review payloads are denied locally, without contacting the provider. PieXis does not log review payloads; Pi's normal user-message/session storage still applies.
80
+ **Alt+A** and `/agent` apply the selected profile's `model` and `effort` from `subagent_variant_overrides` to the main session too. Without a profile model override, switching uses the top-level `model` if configured; otherwise it keeps the current model. Missing effort overrides leave the current thinking level alone (subject to Pi's model-capability clamping). `subagent_model` and `small_model` remain delegation-only settings. Invalid configuration or an unavailable model leaves the current profile unchanged and shows a warning.
101
81
 
102
- ### Provenance and context limits
82
+ Replace the example model IDs with available, authenticated models. `model` initializes a new main session; explicit CLI model selections and resumed sessions win at startup. An explicit profile switch subsequently applies the configured settings. `subagent_model` defaults to the current main-session model. A per-variant `model` wins over all automatic routing. New scaffolds explicitly pin each variant to the default subagent model; remove the `model` field from a variant's override to let it follow automatic routing or later `subagent_model` changes. Otherwise, short lookups/summaries in `ask` and clearly mechanical typo/comment/label/symbol edits in `code` use `small_model` when configured. Ambiguous, longer, or complex tasks, including planning, debugging, and orchestration, keep the normal subagent model. This is a conservative text heuristic, not a classifier or a guarantee of task difficulty. Omit `small_model` to disable automatic small-task routing.
103
83
 
104
- `lib/permission-intent.ts` observes Pi's `input` source and exactly matches delivered user text. It stores only a content digest, timestamp, and source in non-model session metadata, not another prompt copy. Only matched `interactive` (including normal CLI input) and `rpc` inputs count as user authorization. This is observational provenance inside a trusted extension runtime, not cryptographic proof of human identity or protection against malicious extensions.
84
+ Set `delegate_exploration` to `true` to let every profile offload bounded, read-only codebase discovery to the small model. Call `subagent({ "agent": "ask", "purpose": "explore", "task": "Locate configuration loading and summarize the relevant files" })`. Exploration always uses `small_model` at low effort, even if `ask` has a per-variant model override. It is rejected unless the option and `small_model` are configured. Exploration children receive only `read`, `grep`, `find`, and `ls`; they cannot delegate further. The parent retains planning, judgment, edits, and final verification. This is opt-in because child-process overhead can cost more than a trivial direct read.
105
85
 
106
- Raw active-branch user entries preserve constraints across resume and compaction. Abandoned branches and generated summaries cannot supply authorization. Unknown/extension-injected messages are excluded; if the current request has no verified provenance, reviewed actions are denied. Older sessions without provenance need a new direct, self-contained request. Expanded/transformed prompts that do not match the observed input are conservatively unverified; submit the request directly instead. Changes to historical user entries invalidate completeness rather than silently dropping constraints. Undelivered queued input pauses reviewed actions; authorization is rechecked after model review so a changed request, branch, or session cannot reuse an in-flight allow.
86
+ Effort accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Variant effort overrides win; small tasks otherwise request `low`, and other tasks inherit the parent's thinking level. Pi clamps effort to the chosen model's capabilities. Results/progress show the selected model, **requested** effort, routing reason, output, and usage. Invalid configuration or unavailable child models fail explicitly rather than silently switching providers.
107
87
 
108
- The context budget is 16 verified messages, 6,000 characters per message, and 24,000 characters total. Omitted history, oversized messages, recognizable secret redaction, or unsupported non-text input marks the context incomplete. Reviewed actions then fail closed rather than allowing on a truncated authorization record; start a fresh session with the necessary constraints when history exceeds the budget. Direct local reads/edits remain available. Unit tests cover these contracts with mocked reviewers; they do not establish live-model classification accuracy.
88
+ Child agents run in separate Pi processes with ephemeral sessions and the same OS permissions. Their tool selection is a subset of the parent's active tools; `plan`/`ask` children select only `read`, `grep`, `find`, and `ls`, and disable native MCP access. With `delegate_exploration`, a non-exploration `plan` or `ask` child may also receive `subagent` to perform one nested exploration; exploration children never receive it. `plan`/`ask` parents may delegate only to those two profiles. Other children omit `AskQuestion` and return questions to the parent. Main profile instructions and child tool selection are **not sandboxing**. Normal configured extensions and explicitly supplied CLI extensions load in children, with the parent's project-trust decision. Runtime-only provider/tool registrations, CLI `--api-key` overrides, credentials supplied only to an embedded SDK session, and in-memory state in other extensions are not copied to child processes. Use Pi's stored or environment-based authentication for delegation. Other extensions remain responsible for their own restrictions; verify their subprocess behavior before using delegation in a restricted host.
109
89
 
110
- ## Bash analysis and permission management
90
+ Abort and shutdown terminate active children (including their process group on Unix). Windows termination is best-effort for descendants. Model-facing output is capped at 50,000 characters; tool details retain up to 1 MB. JSON/print mode supports subagents but has no profile picker or child terminal interaction. Avoid loading another extension that registers `subagent`, `/agent`, or `--agent` alongside this one.
111
91
 
112
- `lib/bash-policy.ts` implements a bounded literal-shell lexer/parser (16,000 characters, 256 tokens, 32 commands). It recognizes quoted words, command chains, pipelines, simple redirections, assignments, working-directory changes, wrappers/interpreters, and advisory destructive/network/shared-system/safeguard-bypass findings. Literal filesystem targets of supported commands/redirections receive path checks. This is not exhaustive command-effect analysis: it does not execute substitutions, inspect script bodies, or resolve arbitrary wrappers. Unsupported expansions, compound syntax, heredocs, descriptor redirections, and exhausted budgets require review/approval, never a narrow reusable grant.
92
+ ## AskQuestion
113
93
 
114
- Only standalone `pwd`, `pwd -L`, `pwd -P`, `true`, and `false` are automatic Bash fast paths outside Plan, and shell startup/function overrides disable that fast path. Chains, redirections, scripts, package commands, and Git commands are not automatically considered safe. Plan still blocks all Bash. A trusted shell/tool installation is assumed; this is not containment.
94
+ Existing `{ questions: [{ question, options, multiSelect?, allowOther? }] }` calls remain supported. New optional fields:
115
95
 
116
- ### Reusable grants
96
+ - Question `id`, `type` (`single`, `multi`, `text`), and `required` (defaults to true). Explicit text questions may omit `options`; legacy optionless questions require `allowOther: true`.
97
+ - Option `id`, `label`, and optional `description`. Missing IDs become `question-1` / `option-1`, etc.; IDs must be unique within their scope. Labels need not be unique.
98
+ - `defaultSelection`: option ID or array of IDs. `minSelections` / `maxSelections` apply to multi-select answers. Required questions always need an answer.
99
+ - `maxLength`: maximum text length in Unicode code points. Text is not trimmed; whitespace-only required answers are rejected. Text input is single-line, using Pi's native Input component.
117
100
 
118
- For this first bounded implementation, reusable Bash grants cover only **Git status queries** with a fixed option vocabulary: `--short`/`-s`, `--branch`/`-b`, `--porcelain[=v1|=v2]`, and `--untracked-files=no|normal|all`. Choose either the exact command or that narrow query family. Both are bound to the canonical working directory and a fingerprint of the resolved Git executable's metadata, relevant environment, Git configuration, and PieXis settings/permission configuration. A changed fingerprint requires fresh approval/review. A status query can update Git's optional index caches; it is not part of the automatic read-only builtin set.
101
+ Use ↑↓ to navigate, Space to toggle multiple choices, Enter to confirm, and Esc/Ctrl+C to cancel the interaction. For related questions, Tab/Shift+Tab revisits questions without losing drafts; all questions must be confirmed before the final review can be submitted. “Type something else” is available only with `allowOther`; its answer replaces, rather than mixes with, selected options. Selection-count limits do not apply to custom text.
119
102
 
120
- Configuration is inspected locally with bounded reads and never sent to the classifier. Unknown Git configuration keys, includes, aliases, hooks/fsmonitor/pager settings, shell startup overrides, script wrappers named Git, and linked worktrees decline reusable grants. Only a small set of inert core/user/remote/branch Git configuration keys is supported. Reuse is currently unavailable on Windows. Ordinary one-time approval or Auto review remains available when reuse is declined. Scripts (`npm test`, `make`, Python, shell scripts, etc.) do **not** receive reusable narrow grants; arbitrary dependency changes cannot therefore silently reuse such a grant. Full script-dependency analysis is not implemented.
103
+ Results have `status`: `answered`, `cancelled`, `aborted`, `requires_interaction`, or `error`. Both model-facing JSON and tool `details` contain the same result. Legacy `answers` and `cancelled` fields remain; each confirmed answer retains `question` and label-based `answer`, with added question `id`, `optionIds`, and `custom`. Optional empty answers are `""` or `[]`, never cancellation. Cancellation returns only already-confirmed answers. `cancelled` remains true for every non-answered outcome; use `status` to distinguish them.
121
104
 
122
- Use:
123
- - `/permissions` or `/permissions list` — inspect grants, IDs, mode, and denial count.
124
- - `/permissions revoke <id>` — revoke one grant.
125
- - `/permissions clear` — remove all grants.
105
+ Pi owns stdin, raw mode, cursor state, and focus restoration. Agent abort and session shutdown dismiss the screen; input closure/errors also settle it. Concurrent AskQuestion calls are explicitly rejected with `status: "error"`, in addition to Pi's sequential execution. Other extensions must respect Pi's UI ownership; this extension does not coordinate unrelated prompts. Non-TUI/non-TTY runs return `requires_interaction` without reading input.
126
106
 
127
- Revocation/clearing requires an idle agent. These commands never widen permissions or change modes. Grant IDs are session-local; there are no persisted allow rules.
107
+ ## Removed features
128
108
 
129
- ### Denial limits
109
+ PieXis no longer provides permission modes, tool approvals, classifier review, plan-to-execution handoffs, filtered search, or background worktree tasks. Their commands, shortcuts, flags, and configuration initialization have been removed. Tool execution follows Pi and any other installed extensions; PieXis does not provide sandboxing or access restrictions.
130
110
 
131
- Denials include a reason and suggest a genuinely narrower action, clarification, or explicit authorization—not changing tools to perform the same denied effect. By default, **3 identical denials or 6 total denials per user request** stop the run via Pi's abort API. Successful intervening calls do not erase the budget. Configure limits at launch with `--permission-repeat-limit 3` and `--permission-denial-limit 6` (integers 1–50); repository configuration cannot silently raise them.
111
+ Existing `piexis` settings, legacy configuration files, session records, and retained task worktrees are not deleted automatically. Old classifier, execution-model, and background-model settings are no longer read. Remove obsolete configuration manually if desired, preserving unrelated settings and any work you need. Remove explicit references to deleted extensions from your Pi resource settings or launch arguments.
132
112
 
133
- Once stopped, subsequent tool calls remain blocked except `AskQuestion` (and the deliberately launch-only Bypass mode). A newly delivered provenance-verified user request resets the budget; extension-injected messages and automatic retries do not. Pending sibling approvals cannot grant or execute after the stop. Already executing processes are subject to Pi's normal cancellation, not OS isolation.
113
+ ## PIXIS WebUI (preview)
134
114
 
135
- Without permission UI, approval-required actions are immediately denied. At the threshold PieXis emits a `permission_blocked` diagnostic on **stderr**, records a count-only session stop entry, and aborts the run without waiting for input or forcing the whole host process to exit. Pi JSON/RPC consumers must inspect the blocked/aborted outcome; this extension does not promise a nonzero JSON-mode exit status.
115
+ PIXIS 2.0 adds a local WebUI without replacing the Pi TUI. Each browser session observes a backend-owned, long-lived Pi RPC process. After installing or updating Piexis, start Pi inside the project and run:
136
116
 
137
- ## Background tasks
138
-
139
- `/task <work>` (or `background_task`) starts a worker from `HEAD` in a disposable **Git worktree**, with at most two running workers. The permission extension must be active. Worktrees isolate Git changes, **not host filesystem, process, credential, or network access**. Foreground uncommitted changes and the full conversation are not copied; a dirty foreground checkout produces a warning.
140
-
141
- ### Delegation and approvals
142
-
143
- - Status, cancellation, and diff inspection are recognized operations, including in Plan. Creation, application, and cleanup are denied in Plan/Don't Ask. Unknown custom tools remain unknown.
144
- - Manual/Accept Edits require one-time approval for agent-requested task creation. A direct `/task <work>` command supplies explicit user authorization. Auto reviews creation against verified user intent; direct task commands receive their own in-memory user-request identifier.
145
- - The proposed brief is untrusted action data. Optional `brief` fields include goal, context, constraints, files, acceptance criteria (`acceptanceCriteria`), decisions, and verification. Task plus brief is limited to 20,000 characters and is never silently truncated. Keep secrets out of descriptions: briefs go to the worker, may go to the classifier, and are stored in local task metadata.
146
- - Optional `paths` narrows file-tool scope to relative files/directories (default `["."]`, maximum 32 entries). Traversal, external symlink targets, credentials/protected path-tool writes, uncertain recursive searches, and tools outside the worker capability set are denied. There are no recursive task-delegation tools.
147
- - Every worker tool call goes through the parent broker and shared evaluator. Workers inherit no foreground session grants. Auto additionally reviews worker mutations against both the original user intent and the delegated brief. Manual approvals are one-time; missing UI, unavailable/expired brokers, and classifier failures deny rather than escalate.
148
- - New user input, mode changes, grant revocation, branch navigation, session replacement, and shutdown invalidate outstanding task authorizations and cancel queued/running workers. Existing worktrees are retained. Worker denial limits inherit the parent's launch settings and stop that worker, not unrelated tasks.
149
-
150
- ### Opt-in command execution
151
-
152
- Start Pi with `--background-bash` to expose worker Bash for tests/builds/debugging. This is **permission-controlled command execution**, not sandboxing. It uses the same parent approval/classifier pipeline. Bash requires whole-worktree scope; narrower `paths` cannot be combined with this flag. Known literal external/protected targets are rejected, but parsing/model review cannot constrain arbitrary script effects or descendants. Cancellation revokes future approvals and terminates the worker process; it does not promise that all spawned descendants have stopped.
153
-
154
- Workers load only their explicit guard extension and worker-only tool wrappers, with built-ins disabled, so a missing guard cannot fall back to unguarded tools. Project trust-gated resources and automatic context-file discovery are disabled; needed repository instructions can be read through the broker. Task text is passed as literal prompt data, not CLI options or `@file` arguments. Pi's trusted user-level runtime and authentication are still used.
155
-
156
- ### Inspect, apply, and clean
117
+ ```text
118
+ /webui
119
+ ```
157
120
 
158
- Use `/task status`, `/task review <id>`, `/task diff <id>`, `/task cancel <id>`, `/task apply <id>`, `/task clean <id>`, and `/task interrupted` (or their corresponding tools where available).
121
+ Pi displays a local URL containing a per-launch access token; open that URL in a browser. The TUI remains usable. The package also exposes a PowerShell-friendly command for starting outside Pi:
159
122
 
160
- `task_diff` snapshots actual tracked/untracked/deleted files using a **temporary index and object store**; it does not stage into the real index or write repository objects. Checkout and inspection use raw bytes rather than executing Git hooks or clean/smudge filters; this skips LFS hydration and normal checkout conversions. Submodules/special files and exhausted inspection budgets fail closed: 10,000 files, 4 MiB per file, 128 MiB total file bytes, and 8 MiB Git output. Active workers must stop before inspection. Diff display is bounded to 50,000 characters, and sensitive/configuration/symlink patch bodies are withheld rather than leaked through the inspection tool.
123
+ ```powershell
124
+ piexis-web --workspace C:\path\to\your-project
125
+ ```
161
126
 
162
- Apply always requires a user dialog showing the **actual changed paths**, sensitive/configuration warnings, and observed command results—even in Auto/Bypass. The patch digest and destination findings are checked again after confirmation, followed by Git apply validation. Worker prose cannot establish successful verification: observations come from post-execution tool records, not claimed test results or blocked tool-start events. Successful command completion is not proof of test coverage; verify again in the parent workspace after application. Repeatedly declined or headless agent-requested apply/cleanup actions use the foreground denial budget.
127
+ The server uses the requested workspace and starts Pi with documented `--mode rpc`. Pi must be installed, authenticated, and available as `pi` (or set `PIXIS_PI_BIN`). Use `.env.example` as the configuration reference. `npm run dev` remains available only for developing Piexis itself and serves the Vite development client on port 5173.
163
128
 
164
- Cleanup confirms deletion and validates the PieXis ownership marker, temporary directory, Git worktree, and repository before removal. Completed unapplied changes are retained rather than discarded. Headless apply/cleanup is denied. Trusted Pi/Git executables are assumed. These checks are not protection against arbitrary host code or filesystem races.
129
+ The current preview supports genuine streamed assistant text, tool lifecycle cards, abort, Pi extension dialog forwarding, multiple observed backend sessions, and read-only Git status. It binds the agent server to `127.0.0.1`, requires a per-launch token and exact browser origin, validates WebSocket messages, confines file paths before Git diffs, and never sends secrets to the browser. Localhost alone is not treated as authentication.
165
130
 
166
- Metadata lives at `<agent-dir>/piexis/tasks/<project-hash>.json`, not in the repository. Legacy `.pi/piexis-tasks.json` is imported if user-level state is absent; the source is retained. Interrupted worktrees can be reviewed/cleaned, but authorization, process handles, broker tokens, and user-intent snapshots are never restored from metadata. Fast tests use Git fixtures, mocked worker processes, and mocked reviewers. A separate [runtime suite](tests/runtime/README.md) checks actual Pi RPC/JSON flows, worker processes, and TUI permissions through a pseudo-terminal on Linux, using a deterministic local provider. Live-model accuracy, other platforms, and full visual/terminal compatibility remain unvalidated.
131
+ Current limitations: session list is limited to sessions created in the WebUI process (it does not yet enumerate historical Pi session files); file explorer, Monaco editor/diff rendering, and terminal panel are planned; Git controls are read-only. The existing Pi TUI remains unchanged and can run separately.
167
132
 
168
133
  ## Select package resources
169
134
 
170
- PieXis uses Pi's native package resource filtering; it does not add its own plugin manager. Run `pi config` (or `pi config --local` for a trusted project) to enable or disable discovered resources. In settings, the package object can narrow resources declared by this package:
135
+ Use Pi's native resource filtering with `pi config` (or `pi config --local` for a trusted project), or narrow resources in settings:
171
136
 
172
137
  ```json
173
138
  {
174
139
  "packages": [
175
140
  {
176
141
  "source": "npm:@preapexis/piexis",
177
- "extensions": ["extensions/mode.ts", "extensions/ask-question.ts"],
142
+ "extensions": ["extensions/ask-question.ts"],
178
143
  "skills": ["skills/code-review", "skills/diagnosing-bugs", "skills/safe-refactor"],
179
144
  "themes": []
180
145
  }
@@ -182,25 +147,14 @@ PieXis uses Pi's native package resource filtering; it does not add its own plug
182
147
  }
183
148
  ```
184
149
 
185
- Omit a resource type to load all resources of that type; use `[]` to load none; and use Pi glob exclusions such as `"!skills/docx/**"` to omit document workflows. This lets users keep permission controls while disabling the custom UI/theme, or retain coding skills while excluding DOCX/PDF/PPTX/XLSX skills. See Pi's package configuration documentation for the complete filtering syntax.
150
+ Omit a resource type to load all declared resources of that type; use `[]` to load none. Glob exclusions such as `"!skills/docx/**"` can omit document workflows.
186
151
 
187
152
  ## Development
188
153
 
189
154
  ```sh
155
+ npm ci --ignore-scripts
190
156
  npm run check
191
157
  npm test
192
- npm run test:runtime
193
- npm run eval:permissions -- --validate
194
158
  ```
195
159
 
196
- ### Local runtime checks
197
-
198
- `npm run test:runtime` launches the resolved peer dependency's packaged Pi CLI with all manifest extensions, disposable repositories and agent directories, and a deterministic loopback provider. It uses no real credentials or paid model calls. Tests cover permission dialogs, six-mode behavior, reviewer failures/timeouts, worker Bash opt-in, patch confirmation, stale approvals, cancellation, and shutdown. The Linux PTY check exercises the bundled UI and mode shortcuts; it requires `script` and `stty` and is explicitly skipped elsewhere. See [runtime validation results and limits](tests/runtime/README.md).
199
-
200
- ### Classifier evaluation
201
-
202
- [`eval/permission-cases.json`](eval/permission-cases.json) contains 26 synthetic, labeled cases covering all policy categories, paired authorization changes, injection attempts, and worker scope. The offline runner validates the corpus, exports unlabeled requests using the production reviewer prompt, and scores externally collected model responses. It never executes fixture commands or automatically contacts a provider.
203
-
204
- See [`eval/README.md`](eval/README.md) for the opt-in live-evaluation procedure, response format, coverage map, and remaining manual release checks. Raw false allows, host policy overrides, false denials, invalid responses, and missing cases are reported separately. Scorer unit tests are not evidence of live model accuracy; no live-model result is claimed.
205
-
206
- There is currently no `tests/mode-sandbox.test.mjs`; consequently `npm run test:sandbox` cannot establish sandbox coverage. Unit tests do not establish containment or protection of host paths/network.
160
+ Tests cover retained extension/resource contracts, agent configuration/routing/profile state, child-process protocol/cancellation, AskQuestion validation/lifecycle, recommended-tool setup/settings preservation, and skill routing. On Unix with Python 3, automated PTY tests drive the real Pi TUI renderer and native selector, checking keyboard interactions, startup choices, cancellation, cursor/raw-mode restoration, and listener cleanup. They use UI adapters rather than a live agent session; unsupported hosts explicitly skip these tests. Tests do not establish OS containment or live-model behavior. The permission evaluation, sandbox, and background-task runtime suites have been removed with those features.
@@ -0,0 +1,97 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import Fastify from "fastify";
5
+ import websocket from "@fastify/websocket";
6
+ import fastifyStatic from "@fastify/static";
7
+ import { z } from "zod";
8
+ import { SessionManager, type ManagedSession } from "./session-manager.ts";
9
+ import { gitDiff, gitStatus } from "./workspace.ts";
10
+ import type { ClientCommand, ServerMessage } from "../../../packages/shared/src/index.ts";
11
+
12
+ const port = Number(process.env.PIXIS_PORT || 4317);
13
+ const workspace = path.resolve(process.env.PIXIS_WORKSPACE || process.cwd());
14
+ const token = process.env.PIXIS_TOKEN || randomBytes(32).toString("base64url");
15
+ const allowedOrigin = process.env.PIXIS_ORIGIN || `http://127.0.0.1:${port}`;
16
+ const webRoot = path.resolve(process.env.PIXIS_WEB_ROOT || path.join(path.dirname(fileURLToPath(import.meta.url)), "../../web/dist"));
17
+ const clients = new Set<{ send(data: string): void; readyState: number }>();
18
+ const app = Fastify({ logger: { level: process.env.PIXIS_LOG_LEVEL || "warn" } });
19
+
20
+ const commandSchema = z.discriminatedUnion("type", [
21
+ z.object({ type: z.literal("list_sessions") }),
22
+ z.object({ type: z.literal("subscribe"), sessionId: z.string().uuid() }),
23
+ z.object({ type: z.literal("create_session") }),
24
+ z.object({ type: z.literal("prompt"), sessionId: z.string().uuid(), message: z.string().trim().min(1).max(100_000) }),
25
+ z.object({ type: z.literal("abort"), sessionId: z.string().uuid() }),
26
+ z.object({ type: z.literal("extension_ui_response"), sessionId: z.string().uuid(), id: z.string().min(1), value: z.string().optional(), confirmed: z.boolean().optional(), cancelled: z.boolean().optional() }),
27
+ z.object({ type: z.literal("git_status"), sessionId: z.string().uuid() }),
28
+ z.object({ type: z.literal("git_diff"), sessionId: z.string().uuid(), path: z.string().min(1).max(4096) })
29
+ ]);
30
+
31
+ function secureEqual(a: string, b: string) {
32
+ const left = createHash("sha256").update(a).digest();
33
+ const right = createHash("sha256").update(b).digest();
34
+ return timingSafeEqual(left, right);
35
+ }
36
+ function send(client: { send(data: string): void; readyState: number }, message: ServerMessage) {
37
+ if (client.readyState === 1) client.send(JSON.stringify(message));
38
+ }
39
+ function broadcast(message: ServerMessage) { for (const client of clients) send(client, message); }
40
+
41
+ const manager = new SessionManager(workspace, (session, type, data) => {
42
+ broadcast(type === "pi_event" ? { type, sessionId: session.id, event: data } : { type, sessionId: session.id, request: data });
43
+ broadcast({ type: "session", session: manager.summary(session) });
44
+ });
45
+
46
+ await app.register(websocket);
47
+ await app.register(fastifyStatic, { root: webRoot, wildcard: false });
48
+ app.get("/health", async () => ({ ok: true }));
49
+ app.get("/ws", { websocket: true }, (socket, request) => {
50
+ const origin = request.headers.origin;
51
+ const suppliedToken = (request.query as { token?: string }).token;
52
+ if (origin !== allowedOrigin || !suppliedToken || !secureEqual(suppliedToken, token)) {
53
+ socket.close(1008, "Unauthorized PIXIS client");
54
+ return;
55
+ }
56
+ clients.add(socket);
57
+ socket.on("close", () => clients.delete(socket));
58
+ socket.on("message", async (raw: Buffer) => {
59
+ try {
60
+ const command = commandSchema.parse(JSON.parse(raw.toString("utf8"))) as ClientCommand;
61
+ await handle(socket, command);
62
+ } catch (error) {
63
+ send(socket, { type: "error", message: error instanceof Error ? error.message : "Invalid request" });
64
+ }
65
+ });
66
+ });
67
+
68
+ async function handle(client: { send(data: string): void; readyState: number }, command: ClientCommand) {
69
+ if (command.type === "list_sessions") {
70
+ const first = manager.sessions.values().next().value as ManagedSession | undefined;
71
+ if (first) return snapshot(client, first);
72
+ return send(client, { type: "snapshot", sessions: [], sessionId: "", state: {}, messages: [] });
73
+ }
74
+ if (command.type === "create_session") {
75
+ const session = await manager.create();
76
+ broadcast({ type: "session", session: manager.summary(session) });
77
+ return snapshot(client, session);
78
+ }
79
+ const session = manager.get(command.sessionId);
80
+ if (command.type === "subscribe") return snapshot(client, session);
81
+ if (command.type === "prompt") await manager.prompt(session.id, command.message);
82
+ if (command.type === "abort") await manager.abort(session.id);
83
+ if (command.type === "extension_ui_response") session.client.send({ type: "extension_ui_response", id: command.id, value: command.value, confirmed: command.confirmed, cancelled: command.cancelled });
84
+ if (command.type === "git_status") send(client, { type: "workspace", sessionId: session.id, action: "git_status", data: await gitStatus(workspace) });
85
+ if (command.type === "git_diff") send(client, { type: "workspace", sessionId: session.id, action: "git_diff", data: await gitDiff(workspace, command.path) });
86
+ broadcast({ type: "session", session: manager.summary(session) });
87
+ }
88
+ function snapshot(client: { send(data: string): void; readyState: number }, session: ManagedSession) {
89
+ send(client, { type: "snapshot", sessions: [...manager.sessions.values()].map((item) => manager.summary(item)), sessionId: session.id, state: session.state, messages: session.messages });
90
+ }
91
+
92
+ const shutdown = async () => { manager.dispose(); await app.close(); };
93
+ process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown);
94
+ await app.listen({ port, host: "127.0.0.1" });
95
+ console.log(`PIXIS server listening on http://127.0.0.1:${port}`);
96
+ console.log(`Open http://127.0.0.1:${port}/#token=${token}`);
97
+ if (!process.env.PIXIS_TOKEN) console.log(`PIXIS token (set PIXIS_TOKEN to persist it): ${token}`);
@@ -0,0 +1,95 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { PiRpcClient } from "../../../packages/pi-integration/src/rpc-client.ts";
3
+ import type { SessionStatus, SessionSummary } from "../../../packages/shared/src/index.ts";
4
+
5
+ export interface ManagedSession {
6
+ id: string;
7
+ name: string;
8
+ status: SessionStatus;
9
+ client: PiRpcClient;
10
+ state: unknown;
11
+ messages: unknown[];
12
+ sessionFile?: string;
13
+ model?: string;
14
+ }
15
+
16
+ export class SessionManager {
17
+ readonly sessions = new Map<string, ManagedSession>();
18
+ constructor(private readonly cwd: string, private readonly emit: (session: ManagedSession, type: "pi_event" | "extension_ui", data: unknown) => void) {}
19
+
20
+ async create(): Promise<ManagedSession> {
21
+ const id = randomUUID();
22
+ const client = new PiRpcClient(this.cwd);
23
+ const session: ManagedSession = { id, name: "New session", status: "starting", client, state: {}, messages: [] };
24
+ this.sessions.set(id, session);
25
+ client.on("record", (record) => {
26
+ if (record.type === "extension_ui_request") {
27
+ this.emit(session, "extension_ui", record);
28
+ return;
29
+ }
30
+ this.#applyEvent(session, record);
31
+ this.emit(session, "pi_event", record);
32
+ });
33
+ client.on("exit", (error) => {
34
+ session.status = "failed";
35
+ this.emit(session, "pi_event", { type: "runtime_error", message: error.message });
36
+ });
37
+ try {
38
+ const response = await client.command({ type: "get_state" });
39
+ if (!response.success) throw new Error(response.error || "Pi rejected get_state");
40
+ session.state = response.data ?? {};
41
+ const data = response.data as { sessionId?: string; sessionFile?: string; sessionName?: string; model?: { id?: string } };
42
+ session.sessionFile = data.sessionFile;
43
+ session.name = data.sessionName || "New session";
44
+ session.model = data.model?.id;
45
+ const messages = await client.command({ type: "get_messages" });
46
+ if (messages.success) session.messages = ((messages.data as { messages?: unknown[] })?.messages ?? []);
47
+ session.status = "ready";
48
+ return session;
49
+ } catch (error) {
50
+ session.status = "failed";
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ async prompt(id: string, message: string): Promise<void> {
56
+ const session = this.get(id);
57
+ if (session.status === "running" || session.status === "cancelling") throw new Error("This session already has an active operation. Stop it before sending another prompt.");
58
+ session.status = "running";
59
+ const response = await session.client.command({ type: "prompt", message });
60
+ if (!response.success) {
61
+ session.status = "ready";
62
+ throw new Error(response.error || "Pi rejected prompt");
63
+ }
64
+ }
65
+
66
+ async abort(id: string): Promise<void> {
67
+ const session = this.get(id);
68
+ if (session.status !== "running") return;
69
+ session.status = "cancelling";
70
+ const response = await session.client.command({ type: "abort" });
71
+ if (!response.success) throw new Error(response.error || "Pi failed to stop");
72
+ }
73
+
74
+ get(id: string): ManagedSession {
75
+ const session = this.sessions.get(id);
76
+ if (!session) throw new Error("Unknown session");
77
+ return session;
78
+ }
79
+
80
+ summary(session: ManagedSession): SessionSummary {
81
+ return { id: session.id, name: session.name, status: session.status, model: session.model, sessionFile: session.sessionFile };
82
+ }
83
+
84
+ dispose(): void { for (const session of this.sessions.values()) session.client.dispose(); }
85
+
86
+ #applyEvent(session: ManagedSession, event: Record<string, unknown>): void {
87
+ if (event.type === "agent_start") session.status = "running";
88
+ if (event.type === "agent_settled") session.status = event.aborted === true ? "ready" : "ready";
89
+ if (event.type === "message_end") {
90
+ const message = event.message as { role?: string } | undefined;
91
+ if (message?.role === "assistant" || message?.role === "user") session.messages.push(event.message);
92
+ }
93
+ if (event.type === "session_info_changed" && typeof event.name === "string") session.name = event.name;
94
+ }
95
+ }
@@ -0,0 +1,39 @@
1
+ import { execFile } from "node:child_process";
2
+ import { realpath } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import path from "node:path";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ export async function workspacePath(root: string, candidate: string): Promise<string> {
9
+ if (!candidate || path.isAbsolute(candidate) || candidate.includes("\0")) throw new Error("Invalid workspace path");
10
+ const base = await realpath(root);
11
+ const resolved = path.resolve(base, candidate);
12
+ if (resolved !== base && !resolved.startsWith(`${base}${path.sep}`)) throw new Error("Path escapes workspace");
13
+ const actual = await realpath(resolved);
14
+ if (actual !== base && !actual.startsWith(`${base}${path.sep}`)) throw new Error("Path escapes workspace");
15
+ return actual;
16
+ }
17
+
18
+ export async function git(root: string, args: string[]): Promise<string> {
19
+ try {
20
+ const { stdout } = await execFileAsync("git", ["-C", root, ...args], { windowsHide: true, maxBuffer: 2 * 1024 * 1024 });
21
+ return stdout;
22
+ } catch (error: unknown) {
23
+ const message = error instanceof Error ? error.message : "Git command failed";
24
+ throw new Error(message.includes("not a git repository") ? "This workspace is not a Git repository." : message);
25
+ }
26
+ }
27
+
28
+ export async function gitStatus(root: string) {
29
+ const [branch, porcelain] = await Promise.all([git(root, ["branch", "--show-current"]), git(root, ["status", "--porcelain=v1"])]);
30
+ return {
31
+ branch: branch.trim() || "detached HEAD",
32
+ files: porcelain.split("\n").filter(Boolean).map((line) => ({ status: line.slice(0, 2).trim() || "?", path: line.slice(3) }))
33
+ };
34
+ }
35
+
36
+ export async function gitDiff(root: string, relativePath: string) {
37
+ await workspacePath(root, relativePath);
38
+ return git(root, ["diff", "--no-ext-diff", "--", relativePath]);
39
+ }