specpi 0.13.0 → 0.16.0
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.
- package/CHANGELOG.md +25 -0
- package/NPM_RELEASE.md +2 -0
- package/README.md +13 -7
- package/SECURITY_MODEL.md +34 -4
- package/THIRD_PARTY.md +6 -0
- package/docs/delegation/README.md +52 -9
- package/docs/delegation/design.md +1 -1
- package/docs/delegation/protocol.md +22 -5
- package/extensions/command-guard/index.ts +4 -1
- package/extensions/delegation/core.mjs +13 -2
- package/extensions/delegation/extension.mjs +89 -12
- package/extensions/delegation/managed-files.mjs +1 -0
- package/extensions/delegation/native.mjs +8 -2
- package/extensions/delegation/presentation.mjs +2 -1
- package/extensions/delegation/protocol.mjs +13 -2
- package/extensions/delegation/provider.mjs +3 -2
- package/extensions/delegation/settings.mjs +140 -0
- package/extensions/spec.ts +9 -0
- package/extensions/tool-wishlist/index.ts +7 -1
- package/extensions/tool-wishlist/verification.mjs +1 -0
- package/extensions/workflow-controls/index.ts +2 -2
- package/package.json +1 -1
- package/scripts/check-package.mjs +36 -20
- package/scripts/check-syntax.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-09-05
|
|
4
|
+
|
|
5
|
+
- Enable experimental read-only delegation by default at the first session start of each Pi process, including TUI, RPC, print and JSON modes. Startup preflights the host without launching workers or model inference; selective review/scout admission, Guard checks, source restrictions and resource ceilings remain unchanged.
|
|
6
|
+
- Keep `/delegate off` and safety revocations effective through reloads and session switches. `/delegate on` explicitly re-enables dispatch; restarting Pi reapplies the on default. Invalid settings, unsupported providers and locked, unready or ambiguous Guard policies still block activation.
|
|
7
|
+
- Update delegation guidance and release references, and add startup/default-on regression coverage alongside real-Pi lifecycle checks. Restart Pi after updating SpecPi to load the changed delegation runtime.
|
|
8
|
+
|
|
9
|
+
## 0.15.0 - 2026-09-05
|
|
10
|
+
|
|
11
|
+
- Add **SpecPi Chat 0.3.1**, a separately packaged VS Code sidebar with streamed Pi replies, safe Markdown, expanded thinking and collapsed tool output by default, model/thinking controls, exact approvals, and a compact composer. It reuses the user's Pi configuration without managing credentials or installing the harness.
|
|
12
|
+
- Add searchable extension-owned history, rename and reversible archive, and independent live conversations. Switching chats or folders preserves background work, approvals, drafts, attachments, usage, and view position. Stop/Disconnect target the selected conversation; parallel chats share workspace files rather than isolated worktrees.
|
|
13
|
+
- Add validated file/image attachment, screenshot paste/drop, bounded inline images, workspace code links and image previews, explicit queued-image recovery, visible transcript search/copy/export, usage/cost reporting, and native Git diff review. Branching and earlier-prompt editing preserve the source conversation and never undo code files or automatically send restored drafts.
|
|
14
|
+
- Start Command Guard in Guard mode in RPC without an unreadable startup selector; retain explicit `/guard` choices after readiness. Route task handoffs, challenge reports, and wishlist reports through RPC-capable dialogs, and disclose terminal-only display controls.
|
|
15
|
+
- Wait for legacy Pi startup fallbacks without approving early dialogs. Reset interrupted-run state on reconnect and never restore accepted prompts after a later refresh failure.
|
|
16
|
+
- Show installed Codex Usage and pi-usage (including Anthropic) reports in Chat's compact, expandable Limits row, separate from conversation tokens/cost. Reuse bounded Pi status events without provider queries, credential/cache reads, or changes to the default package list.
|
|
17
|
+
- Scope Chat's Pi-state filename restrictions to Pi/Chat storage so ordinary authentication, session, and history source files remain usable; retain global credential/key protection and canonical-path checks.
|
|
18
|
+
- Fix composer `/model` selection, read-only usage during active work, and stale Stop cleanup errors after reconnect. Align README and Pages installation examples with the separately versioned Chat artifact.
|
|
19
|
+
- Add dependency-free local VSIX packaging, isolated real-Pi and native VS Code tests, rendered Chat checks in CI, and editor-extension syntax/source-inventory coverage. No npm/Marketplace publication or automatic editor installation is included.
|
|
20
|
+
|
|
21
|
+
## 0.14.0 - 2026-09-05
|
|
22
|
+
|
|
23
|
+
- Raise delegation's default job timeout from 2 to 10 minutes, including the provider adapter. Add `/delegate timeout <minutes>` (1–60) and `reset` with an atomic, backed-up preference across restarts. Batch timeouts scale with the job window; call quotas, original follow-up deadlines and settling ownership remain unchanged. Restart Pi after updating the delegation runtime.
|
|
24
|
+
- Support platform aliases in the selected Pi agent-directory path while rejecting links inside preference state. Bound encoded backups separately so every accepted settings file can be saved repeatedly.
|
|
25
|
+
- Keep `.mts` declarations on LF checkouts and validate Pages permissions with either LF or CRLF input so release checks remain portable on Windows.
|
|
26
|
+
- Run timeout persistence fixtures across release platforms, isolate the package help probe, and verify that settings survive failed updates and every managed delegation file is removed on uninstall.
|
|
27
|
+
|
|
3
28
|
## 0.13.0 - 2026-09-05
|
|
4
29
|
|
|
5
30
|
- Add bounded, best-effort sanitized browser exceptions, console errors, failed requests, and HTTP error diagnostics with explicit cursor/loss/clear semantics and ephemeral retention.
|
package/NPM_RELEASE.md
CHANGED
|
@@ -22,6 +22,8 @@ Verify the registry bytes and metadata immediately, then configure trusted publi
|
|
|
22
22
|
|
|
23
23
|
1. Select a version that has never appeared on npm. npm versions are immutable.
|
|
24
24
|
2. Update `package.json`, `CHANGELOG.md`, `README.md`, `site/index.html`, `site/wiki/index.html`, `site/single-agent/index.html`, and the delegation guide to the same version. Keep historical changelog entries intact and remove stale unreleased-status wording.
|
|
25
|
+
If Chat changes are included, increment its independent version in `vscode/package.json` and `vscode/CHANGELOG.md`, and align the VSIX filenames and feature descriptions in the root README, extension README, and Pages guides. Build it with `npm --prefix vscode run package`, run `npm --prefix vscode run test:render` and the isolated `npm --prefix vscode run test:vsix`, and inspect the VSIX allowlist. The editor extension is not part of the npm tarball; creating this local artifact does not publish it or authorize a Marketplace release.
|
|
26
|
+
|
|
25
27
|
3. For a stable release, add a dated changelog heading. Use a prerelease version when the package should not receive the `latest` dist-tag.
|
|
26
28
|
4. Install the pinned development tools without lifecycle scripts or peers:
|
|
27
29
|
|
package/README.md
CHANGED
|
@@ -24,13 +24,19 @@ SpecPi adds task contracts, workflow controls, and a local improvement loop to P
|
|
|
24
24
|
|
|
25
25
|
Collection is disabled until explicitly enabled. Reports are sanitized, bounded, deduplicated by task, and never uploaded. Later evidence can reopen an item for review, but never restarts implementation automatically.
|
|
26
26
|
|
|
27
|
-
Version `0.
|
|
27
|
+
Version `0.16.0` enables bounded read-only delegation by default at Pi startup. One agent still owns edits and verification; worker use remains selective. SpecPi Chat, browser checks, and the human-selected improvement workflow remain available. See the [release notes](CHANGELOG.md) for the change list.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## SpecPi Chat for VS Code
|
|
30
|
+
|
|
31
|
+
The `vscode/` source contains **SpecPi Chat 0.3.2**, a separately packaged native sidebar with streamed replies, tool results, file and image attachments, model/thinking controls, Pi approvals, and searchable workspace conversation history. Rename or archive chats, switch between independent live conversations, branch or edit an earlier prompt, search and export visible messages, inspect usage, and review changes through VS Code's native diff tools. Switching chats preserves background work, approvals, drafts, and attachments. Connected conversations share workspace files; use separate worktrees for isolated writers. Opening the sidebar does not start Pi.
|
|
32
|
+
|
|
33
|
+
From a repository checkout, build a local VSIX with `npm --prefix vscode run package`, then install `.specpi-test/vscode/specpi-chat-0.3.2.vsix` through **Extensions: Install from VSIX…**. See the [extension guide](https://github.com/TannerMidd/SpecPi/blob/main/vscode/README.md) for prerequisites, image/provider limits, commands, privacy, and testing. Pi owns provider authentication and tool execution. Stop and Disconnect affect the selected conversation only; branching does not undo code changes. The extension is separate from the SpecPi npm package and has not been published to a marketplace.
|
|
34
|
+
|
|
35
|
+
## Selective delegation
|
|
30
36
|
|
|
31
37
|
One agent owns edits and verifies results. Experimental delegation adds up to two read-only Pi workers: `review` checks a frozen artifact; `scout` answers a focused question using selected sources. Workers cannot write, run shell commands, browse the web, or delegate further.
|
|
32
38
|
|
|
33
|
-
Delegation is **
|
|
39
|
+
Delegation is **on by default at Pi startup**, subject to compatibility, settings, and Guard checks. Startup does not launch workers or model inference. Use `/delegate status` to inspect work and limits, `/delegate off` to revoke it, and `/delegate on` to re-enable it. Off and safety revocations survive reloads and session switches; restarting Pi reapplies the on default. Research informed the design; SpecPi quality, speed, and cost gains remain unmeasured.
|
|
34
40
|
|
|
35
41
|
See [setup and limits](docs/delegation/README.md) or [how the research shaped the architecture](https://tannermidd.github.io/SpecPi/single-agent/).
|
|
36
42
|
|
|
@@ -55,14 +61,14 @@ specpi doctor
|
|
|
55
61
|
Pin the reusable CLI when installing a reviewed release, or inspect its plan without retaining a global CLI installation:
|
|
56
62
|
|
|
57
63
|
```bash
|
|
58
|
-
npm install --global specpi@0.
|
|
59
|
-
npx --package specpi@0.
|
|
64
|
+
npm install --global specpi@0.16.0
|
|
65
|
+
npx --package specpi@0.16.0 specpi plan
|
|
60
66
|
```
|
|
61
67
|
|
|
62
68
|
For a source-audited installation, clone the exact release:
|
|
63
69
|
|
|
64
70
|
```bash
|
|
65
|
-
git clone --branch v0.
|
|
71
|
+
git clone --branch v0.16.0 --depth 1 https://github.com/TannerMidd/SpecPi.git
|
|
66
72
|
cd SpecPi
|
|
67
73
|
./specpi plan
|
|
68
74
|
./specpi install
|
|
@@ -167,7 +173,7 @@ Use `/task clear` before recording an unrelated task. Within a session, repeated
|
|
|
167
173
|
|
|
168
174
|
Use `/experiment start` when an independent review or trial justifies a separate worktree. Open the reported path in another Pi session. SpecPi does not launch an agent, copy dirty base changes, commit, merge, or touch remotes.
|
|
169
175
|
|
|
170
|
-
SpecPi
|
|
176
|
+
SpecPi's default workflow uses one writer per working directory. Its experimental delegation adds bounded read-only Pi sessions. SpecPi Chat also supports independent conversations that you start explicitly; connected chats can continue concurrently in the same workspace. They share workspace files rather than receiving separate worktrees. A parent determines what context a delegated child receives and verifies what returns, so either handoff can omit a material constraint. Parallel writers also introduce conflicting assumptions and increase review work.
|
|
171
177
|
|
|
172
178
|
## Improvement loop
|
|
173
179
|
|
package/SECURITY_MODEL.md
CHANGED
|
@@ -6,19 +6,19 @@ This document describes SpecPi's architecture-level security assumptions, enforc
|
|
|
6
6
|
|
|
7
7
|
### Experimental native delegation
|
|
8
8
|
|
|
9
|
-
Delegation is a native extension discovered through the ordinary Pi package and SpecPi lifecycle. Start `pi` normally;
|
|
9
|
+
Delegation is a native extension discovered through the ordinary Pi package and SpecPi lifecycle. Start `pi` normally; delegation is enabled by default at the first session start of each Pi process, including TUI, RPC, print and JSON modes, after settings, host and Guard checks. Startup launches no workers or model inference, but Pi-owned authentication/OAuth preparation may occur. The model-facing schema is present only while dispatch is enabled. It adds no launcher, separate host process, service or trust override. Parent Pi retains normal resource discovery, trust decisions and proxy policy. Delegation checks required public SDK capabilities, not exact version identifiers. Missing session/runtime/settings/thinking APIs prevent activation; session construction and every request still enforce the tool, model and resource policy. API presence is not proof of every future SDK behavior or a passing integration receipt. Normal installation's minimum Pi version and 0.84.4 bootstrap pin are separate contracts.
|
|
10
10
|
|
|
11
11
|
Each worker is an SDK `createAgentSession` instance with in-memory session storage. Pi owns its model/tool loop. A fresh Pi `ModelRuntime` uses standard authentication, environment and `models.json` resolution. Child transport and thinking budgets come from configured global settings; project settings are not loaded. Parent model/thinking are explicit with Pi's supported-level clamping. SpecPi does not extract credentials, copy authentication state or inspect private runtime fields. Preflight rejects runtime-only authentication, selected extension-registered provider overrides, model-specific headers, startup proxy configuration and mismatched safe model descriptors because those routes cannot be faithfully reconstructed. These rejections leave parent configuration unchanged.
|
|
12
12
|
|
|
13
13
|
This is not full parent inference parity: parent request hooks, ephemeral runtime settings and session affinity are not automatically inherited. Keep delegation disabled if those inherited controls are required for every request. Children load no ambient extensions, skills, AGENTS files or parent transcript. Their only tools are selected-source list/read/literal-search; shell, write, arbitrary plugin, live-web and nested delegation tools are absent. Trusted parent extensions remain privileged in the same process. This is an application boundary, not an OS sandbox against malicious extensions.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The startup default activates the documented experimental calls/time policy. Human `/delegate off` revokes it and `/delegate on` re-enables it. Off and safety revocations survive reloads and session switches; restarting Pi reapplies the on default. No on/off preference is persisted. Admission supports only frozen `review` and bounded `scout` analysis. Each job receives its assigned requirement IDs and fixed global constraints. Review needs inline context or selected files; scouts need selected files. A declared parallel benefit requires useful parent work. Structural checks reject unsupported mode/benefit combinations and duplicate normalized questions; they do not prove semantic independence or improved outcomes. Command Guard still intercepts the parent tool; Strict approval binds its capability summary to the exact call and policy generation. Guard, task, scope, model, session and branch changes revoke old generations; normal leaf advancement does not. Model requests cannot enable delegation, change models, raise ceilings or grant tools. Parent acceptance does not mint human authorization, actual verification or wishlist selection.
|
|
16
16
|
|
|
17
|
-
Command Guard is optional for delegation.
|
|
17
|
+
Command Guard is optional for delegation. Activation works when Guard is absent or Off; read-only snapshot tools, admission policy and resource limits are enforced by delegation itself. An installed Guard's Strict approvals and explicit locks still apply. A responder with an unready policy, or multiple responders, blocks activation with a specific error. Guard installation, removal or mode changes invalidate the enabled generation before further worker calls.
|
|
18
18
|
|
|
19
19
|
Guard responders reattach on each session startup, including reuse of an extension instance. Invalid commands, declined confirmations and no-op mode requests do not revoke delegation. Accepted changes and critical locks do; an asynchronous confirmation must still match the active Guard generation before it can commit.
|
|
20
20
|
|
|
21
|
-
The human activation choice follows subsequent parent provider/model and thinking selections. A selection revokes old generations and preflights the new host before automatically resuming dispatch; unsupported selections pause without falling back to the previous model. No job is automatically retried and no quota or settling slot is reset. New model generations invalidate old Strict approval fingerprints. Concurrent setup is bound to the latest selection and extension instance; completion cannot undo explicit off, Guard revocation or session/task/scope changes. Status distinguishes the requested on/off choice from whether dispatch is ready.
|
|
21
|
+
The default or human activation choice follows subsequent parent provider/model and thinking selections. A selection revokes old generations and preflights the new host before automatically resuming dispatch; unsupported selections pause without falling back to the previous model. No job is automatically retried and no quota or settling slot is reset. New model generations invalidate old Strict approval fingerprints. Concurrent setup is bound to the latest selection and extension instance; completion cannot undo explicit off, Guard revocation or session/task/scope changes. Status distinguishes the requested on/off choice from whether dispatch is ready.
|
|
22
22
|
|
|
23
23
|
The broker reads only explicitly selected, bounded regular text files under the fixed canonical working root captured for the Pi process, with private-path, traversal, symlink/junction, hardlink, binary and size checks. Restart Pi to change this root or load a new delegation runtime version. It rechecks source identity/content and validates evidence line ranges. Each job sees only its selected IDs. These checks assume a trusted local filesystem: portable Node APIs do not establish an atomic OS snapshot against hostile filesystem races, and filenames cannot reveal secrets embedded in ordinary source files. No live web adapter is included.
|
|
24
24
|
|
|
@@ -30,6 +30,10 @@ Incremental stream checks count recognized delta bytes and bound event structure
|
|
|
30
30
|
|
|
31
31
|
The same controller and bounded idempotency journal remain in memory for the Pi process lifetime, including `/reload`, session switches and off/on. Successful spending and final-disposition receipts are retained (at most 20 under the fixed ceilings); cancellation and nonfinal assessments have a separate 128-entry oldest-first cache. Failures reserve no keys. Cache churn cannot evict spending receipts, reset quotas or disable cancellation. Limits are two active workers, four batches and 32 SDK invocations per process; two jobs and 8 invocations per batch; four invocations per logical job including follow-up. Invalidation does not reset these counters or release settling work. Completed reports retain their source bindings after the deadline, but child sessions are released at the deadline and later follow-up is rejected. These numeric limits are engineering choices, not empirical optima.
|
|
32
32
|
|
|
33
|
+
Logical jobs default to 10 minutes and batches to 20 minutes. Human `/delegate timeout <minutes>` persists a whole-minute value from 1–60 while delegation is off; the batch window is twice the job window. Queue and follow-up time count from admission. SDK request timeouts and adapter abort timers use the remaining job window, with a 60-minute ceiling. Changes revoke old policy bindings, never extend old jobs or reset quotas/settling slots, and are absent from the model-facing schema. The effective time policy is included in Guard fingerprints and packet digests. Longer windows also allow longer in-memory input/child retention; they do not add calls, retries or billing guarantees.
|
|
34
|
+
|
|
35
|
+
Only the numeric timeout preference is persisted under `<agent-dir>/specpi/delegation/settings.json`; its previous content and SHA-256 are backed up in `settings.json.bak`. The human-selected agent directory is canonicalized once, including platform aliases; owned SpecPi subdirectories and preference files still reject links. Reads reject malformed/nonregular settings and are bounded to 4 KiB; the encoded backup has a separate 32 KiB bound. Saves use exclusive staging and atomic promotion. Invalid settings block activation. This store does not read Pi authentication, settings or sessions. The process loads it once; manual or other-process edits require restart, and concurrent saves are last-writer-wins. As with snapshots, this is a trusted-local-filesystem contract, not protection against hostile filesystem races. User-owned preferences survive uninstall. Requested file/directory modes are 0600/0700; Windows relies on profile ACLs.
|
|
36
|
+
|
|
33
37
|
Shared snapshot text is destroyed once no job can continue; failed attempts retain original expiry cleanup. Packet/job-input references are dropped after owned workers settle. Metadata and digests remain for completed receipt freshness. Starting the next accepted batch retires previous reports; invalidation retires old generations after settlement. Retired batch objects are pruned, leaving only bounded state summaries and idempotency/usage counters. Replay cannot restore their reports, inputs, sessions or allowances. JavaScript strings, provider buffers and values already returned to Pi cannot be securely erased.
|
|
34
38
|
|
|
35
39
|
Selected context reaches the configured provider; normal parent tool results may be retained by Pi. Child sessions use memory only, with no child session database, raw metrics journal, credential copy, automatic resume or policy learning, or secure memory-erasure guarantee. See the [implemented protocol](docs/delegation/protocol.md) for states and receipts. SDK integration and synthetic-provider fixtures concern the runtime contract; their existence does not establish full parent inference parity or measured outcome gains.
|
|
@@ -152,6 +156,32 @@ The GitHub Pages workflow publishes the checked-in `site/` directory. Its deploy
|
|
|
152
156
|
|
|
153
157
|
Repository checks, smoke tests, checksums, closed capability validators, and browser comparisons provide evidence for documented behavior. They reduce regression risk but do not prove the absence of vulnerabilities or establish cryptographic provenance for dependencies and releases.
|
|
154
158
|
|
|
159
|
+
## SpecPi Chat editor integration
|
|
160
|
+
|
|
161
|
+
The optional `vscode/` extension is distributed separately from the SpecPi npm installation. It runs in the VS Code workspace extension host and requires a trusted filesystem workspace before launching Pi or collecting file context. Only application-level settings choose executable paths; the launcher resolves native executables or known adjacent Pi JavaScript entry points and never executes a shell command string. Pi runs in the explicitly selected workspace with its normal installed extensions and provider configuration. VS Code trust does not replace Pi project trust, and the extension does not pass a flag to bypass Pi's trust decisions or Command Guard.
|
|
162
|
+
|
|
163
|
+
The sidebar uses Pi's documented JSONL RPC protocol. Pi owns provider authentication and tool execution; the extension does not read, copy, or manage authentication files. RPC model metadata is projected to display fields. Approvals and selection/input/editor requests are displayed as text and require an exact response for the requesting conversation's active request. Switching the displayed conversation preserves outstanding requests; background requests appear as needing input and never receive automatic approval. Stop, disconnect, runtime session transitions, and expiry cancel the affected conversation's requests. Expired or wrong-conversation responses cannot authorize later requests. In RPC, Command Guard starts in Guard mode without an initial selection dialog because Pi cannot read dialog responses until its startup handlers return; explicit `/guard` commands remain available after startup. Unsupported terminal-only components are disclosed rather than treated as functional editor controls.
|
|
164
|
+
|
|
165
|
+
Chat allows up to 90 seconds for an initial read-only RPC readiness probe before sending normal requests. This accommodates the 30-second startup fallback in older installed Guard versions without extending later request deadlines. Dialogs arriving before readiness receive cancellation only, never an approval or mode selection; the sidebar explains the limitation and retains a notice after connection. An extension with an unbounded startup dialog can still prevent readiness. Chat times out with recovery guidance instead of bypassing that extension or changing the user's installed harness.
|
|
166
|
+
|
|
167
|
+
The webview loads only packaged scripts/styles under a strict content security policy, forbids network resources, and renders model/tool/file text without interpreting embedded HTML. Images use validated data URLs under `img-src data:`; remote image URLs are never fetched by the webview. HTTP(S) links, including external image links, require a human click and pass through VS Code's external-link API. Clicked code references use a separate host action: paths are parsed, checked against the selected workspace, canonicalized, and restricted to regular non-sensitive files before VS Code opens the file and reveals a clamped line/range. Clicked workspace Markdown image references use the same path boundary and the image validator before a preview is returned. Arbitrary command and URI handlers are not executed. Raw paths preserve literal percent signs; Markdown link destinations and explicit local file URLs are decoded once before filesystem validation.
|
|
168
|
+
|
|
169
|
+
Text/code context requires an explicit file/selection action, is size-bounded, rejects binary and known sensitive paths, and checks canonical workspace containment. Workspace file suggestions enumerate a bounded set of names; choosing a suggestion invokes the same attachment validation before reading contents. Dropped workspace URIs are validated independently of their browser representation. Native image selection, clipboard images, and dropped image files are explicit attachment actions and may supply images from outside the selected workspace; filesystem image sources still require ordinary files and pass sensitive-path checks. Pi-state names such as `auth`, `trust`, `sessions`, `missions`, and `history` are reserved inside `.pi`, Chat-owned storage, and the configured Pi agent directory (including its canonical target), not arbitrary source directories. Global credential/key patterns remain blocked, including `auth.json`. Relative `PI_CODING_AGENT_DIR` overrides retain conservative name blocking because they resolve in the child workspace; use an absolute path or `~/…` to obtain directory-scoped filtering. These filename checks and format validators are not content-based secret detectors. Manually typed messages, ordinary files, and images may contain sensitive content, and the extension does not promise image metadata removal. Prompts and attachments are sent to the provider selected in Pi when the user sends them.
|
|
170
|
+
|
|
171
|
+
Image content is checked for supported PNG, JPEG, GIF, or WebP structure, declared MIME agreement, dimensions, and bounded base64 size. Each image is limited to 5 MiB, 16,384 pixels per side, and 40 million pixels. A prompt permits eight mixed attachments and 20 MiB of image bytes. The display retains at most 32 images / 20 MiB and reports omitted content; this is separate from Pi's persisted history. RPC records and buffered writes have independent 64 MiB caps. Recognized oversized history responses with a matching pending request are drained without accumulating the remaining bytes, then rejected without terminating Pi. The sidebar reports omitted history and remains usable; other oversized protocol records terminate the connection. A large historical transcript can exceed the RPC cap even when its original prompts were individually valid. Media caches remain in memory, are referenced by opaque identifiers while streaming, and are released as content is no longer retained. No remote image fetch, image processing service, or new image dependency is introduced. Pi/provider image limits and behavior remain upstream contracts.
|
|
172
|
+
|
|
173
|
+
Connection teardown resets transient run bookkeeping without discarding displayed partial text. An accepted prompt is never restored because a later status refresh failed, and the Refresh status action performs read-only RPC queries instead of replaying work. RPC request IDs correlate replies; they do not make retries idempotent. A lost connection before acknowledgement leaves the prompt outcome uncertain.
|
|
174
|
+
|
|
175
|
+
Image prompts require a model advertising image input. Rejection retains the unsent draft and attachments. Image-bearing slash commands are rejected because Pi extension commands can acknowledge execution without consuming image arguments. Stop clears queued work before aborting. Pi's queue-clear response contains text only, so Chat retains bounded in-memory image snapshots for its accepted queued submissions and restores them only after an exact, unambiguous match and an explicit user action. It does not attach discarded or unmatched images to a later prompt, automatically resend recovered work, or treat a missing acknowledgement as proof of failure.
|
|
176
|
+
|
|
177
|
+
Pi persists chats, including sent image blocks, in a workspace-specific directory under the extension's VS Code storage. A bounded atomic catalog stores only identifiers, titles, timestamps, archive flags, and paths of conversations created there. It never scans or opens terminal Pi history or writes transcripts to VS Code settings, logs, telemetry, or persistent webview state. Each conversation's live projection, draft, and attachment snapshots stay in memory with per-conversation bounds. Resume is limited to catalogued regular files inside the owned session directory. Renaming and reversible archiving change catalog metadata without opening transcripts; archiving neither deletes files nor stops a live process. Uninstalling the extension does not promise secure deletion of VS Code storage. POSIX permissions are requested where supported; Windows relies on the profile's ACLs. As elsewhere in SpecPi, local filesystem races, a compromised extension host, an untrusted Pi executable, or same-user tampering are outside the isolation guarantee.
|
|
178
|
+
|
|
179
|
+
Conversation edits select entries from the active Pi tree, revalidate the selection before mutation, and fork before the chosen user prompt. Exact original text and validated image blocks are captured before forking because Pi's fork response returns text only. The original conversation is retained, restored drafts require another send action, and conversation forks/clones do not restore or checkpoint workspace files. Actions are scoped to the active workspace, connection, and session revision. Export opens an unsaved Markdown document and Copy Conversation writes the visible Markdown to the OS clipboard only on explicit request. Both omit image bytes and represent images as placeholders; they do not reconstruct truncated or hidden history. Usage displays report Pi measurements, preserving unavailable values rather than inventing totals. The separate provider Limits row projects bounded strings from the known Codex Usage and pi-usage status keys, strips terminal/control sequences, and inserts text only. It does not parse numeric quotas, combine budgets with conversation cost, read provider credentials or usage-cache files, or add provider requests. Reports stay with their live connection and are cleared on disconnect, exit, and reconnect; refreshing and cache age remain upstream plugin responsibilities. Opening the details has no RPC or provider side effect. Existing `/usage` notifications and exact confirmation dialogs continue through the normal UI protocol; no automatic reset consumption or terminal-footer emulation is added.
|
|
180
|
+
|
|
181
|
+
Native change review reads VS Code's Git API, validates selected workspace paths, and opens the corresponding working-tree or staged view through the built-in diff/editor actions. It does not execute a shell command, stage, commit, discard, or restore files. Git integration availability and the filesystem race boundary still apply. The sidebar coordinator keeps independent Pi connections for explicitly opened conversations. Switching conversations or folders only changes the selected view. Stop and Disconnect target the selected process; extension-host shutdown stops all owned processes. User-initiated parallel conversations share their workspace files and are not isolated worktrees. Branching uses Pi's pre-runtime `--fork` to copy into a new owned session before extensions can write, then performs any active-branch or prompt selection there; the original process and session remain unchanged.
|
|
182
|
+
|
|
183
|
+
The extension introduces no network server, telemetry, runtime package dependency, or automatic model request when a view opens. Pi itself may contact its configured services when connected. Tests use synthetic homes, agent directories, providers, workspaces and profiles, plus an installed VS Code extension host; they do not access the user's live Pi credentials or conversations. Live provider availability and billing are outside the offline test evidence.
|
|
184
|
+
|
|
155
185
|
## Supply-chain assumptions
|
|
156
186
|
|
|
157
187
|
SpecPi pins reviewed executable package versions and the browser dependency graph. These controls improve repeatability and make version changes reviewable. They do not prove that a registry, publisher account, package artifact, downloaded browser, GitHub Action tag, or invoking host is uncompromised. Releases are not described as reproducible or cryptographically signed unless a future release adds and documents those mechanisms.
|
package/THIRD_PARTY.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Third-party components
|
|
2
2
|
|
|
3
|
+
SpecPi Chat is a separate VS Code extension built with the public VS Code Webview View API and Pi `0.84.4`'s documented RPC protocol. VS Code and the installed Pi runtime retain their upstream licenses; neither is bundled in the VSIX. The extension and its ZIP/VSIX packager use Node built-ins and first-party source only, with no added runtime or development dependency. Its tests reuse the repository's pinned Playwright and Pi packages. Provider authentication, requests, billing, and normal Pi background behavior remain owned by the configured Pi runtime.
|
|
4
|
+
|
|
5
|
+
Chat's provider Limits row recognizes the public RPC `setStatus` presentation contracts of `@llblab/pi-codex-usage` **0.9.3** (`aa-codex-usage`) and `@sreetej510/pi-usage` **0.9.9** (`provider-usage`). Public source was checked at commits `1a97ee5560c5d7dd3b99ce34e2dcfe456592f8a0` and `9dee3b590f736c723011e3bc3717b3f0d6e8072b`, respectively. Tests reproduce their status/notification contracts through synthetic local Pi RPC, not live accounts or upstream provider requests. Text retains each plugin's quota semantics rather than converting or aggregating measurements. Neither plugin is bundled in the VSIX; this compatibility change does not install pi-usage or change SpecPi's existing package pins. Plugins retain ownership of authentication, caching, refresh/backoff, and explicit reset-consumption confirmation.
|
|
6
|
+
|
|
7
|
+
Chat uses Pi's image content blocks, conversation tree/fork/clone operations, and session statistics, plus the installed VS Code Git extension's public API for diff review. Version 0.3.0 retains a separate Pi process for each connected conversation and uses Pi's `--fork <session>` CLI option to copy branch history before runtime extensions initialize; the original live session remains untouched. Image container validation and base64 framing are first-party code; display uses VS Code's embedded browser image decoders. No image codec, provider SDK, export package, or remote image service is added. Local acceptance of PNG, JPEG, GIF, or WebP does not establish that every configured provider accepts the same formats or animation behavior. RPC records and buffered writes are capped at 64 MiB; image prompts and rendered media have separate, smaller limits documented in the extension guide.
|
|
8
|
+
|
|
3
9
|
Delegation loads `clampThinkingLevel` from the Pi SDK when exported there, otherwise
|
|
4
10
|
from the public `@earendil-works/pi-ai/compat` subpath declared in Pi's
|
|
5
11
|
[package exports](https://github.com/earendil-works/pi/blob/main/packages/ai/package.json).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Bounded delegation
|
|
2
2
|
|
|
3
|
-
Status: experimental in SpecPi 0.
|
|
3
|
+
Status: experimental in SpecPi 0.16.0. Enabled by default at Pi startup.
|
|
4
4
|
The package remains `specpi`; no separate npm package or background service is required.
|
|
5
5
|
|
|
6
6
|
SpecPi keeps one agent responsible for changes and acceptance. This extension adds
|
|
@@ -62,12 +62,16 @@ those inherited controls for every request must keep delegation disabled. Receip
|
|
|
62
62
|
bind supported model and source descriptors; they cannot certify an unchanged remote
|
|
63
63
|
service or every configuration change behind a stable provider identity.
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## Control delegation
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
The first session start of each Pi process enables delegation after settings, host and
|
|
68
|
+
Guard checks, in TUI, RPC, print and JSON modes. Startup does not launch workers or
|
|
69
|
+
model inference. Preflight may perform Pi-owned authentication/OAuth preparation.
|
|
70
|
+
Use one agent for small or sequential work; delegate only a justified independent question.
|
|
71
|
+
|
|
72
|
+
In an interactive session:
|
|
68
73
|
|
|
69
74
|
```text
|
|
70
|
-
/delegate on
|
|
71
75
|
/delegate status
|
|
72
76
|
/delegate limits
|
|
73
77
|
/delegate cancel <batchId>
|
|
@@ -93,9 +97,12 @@ it does not retain or display live child reasoning. RPC and print mode keep the
|
|
|
93
97
|
structured tool responses and do not mount terminal widgets. The UI uses Pi's public
|
|
94
98
|
[widget and tool-rendering APIs](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md).
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
The startup default enables the documented experimental calls/time envelope. Use
|
|
101
|
+
`/delegate off` to revoke it and `/delegate on` to explicitly re-enable it. Off and
|
|
102
|
+
safety revocations survive `/reload` and session switches; restarting Pi reapplies the
|
|
103
|
+
on default. No on/off preference is written to disk. There is no model-call
|
|
104
|
+
permission toggle in the model-facing tool. `limits` is read-only; prompts cannot
|
|
105
|
+
change timeouts or raise other ceilings. Turning delegation off, changing guard policy, switching
|
|
99
106
|
sessions or models, navigating branches, and changing task/scope bindings revoke the
|
|
100
107
|
current generation. Off/on, `/reload` and session switches do not reset the Pi process's
|
|
101
108
|
counters or free requests that are still settling. The same in-memory controller remains
|
|
@@ -108,7 +115,7 @@ unsettled slots and consumed quotas, and checks the new host before resuming dis
|
|
|
108
115
|
Old jobs are not retried. An unsupported selection pauses delegation with a reason;
|
|
109
116
|
selecting a compatible model resumes it automatically. `/delegate off` remains off
|
|
110
117
|
through later model changes. Guard, task/scope and session lifecycle changes still
|
|
111
|
-
revoke activation. Status separates the
|
|
118
|
+
revoke activation. Status separates the default or human `requested` choice from `enabled`
|
|
112
119
|
dispatch, with `updating` and `pauseReason` for model setup.
|
|
113
120
|
|
|
114
121
|
While delegation is off, its tool is removed from the parent's active tool list.
|
|
@@ -123,6 +130,42 @@ the error identifies that state. `/delegate status` reports the observed Guard s
|
|
|
123
130
|
Worker tool restrictions and resource limits are enforced independently of Guard. A worker result
|
|
124
131
|
cannot authorize a write, a commit, a deployment, or an improvement.
|
|
125
132
|
|
|
133
|
+
## Configure the timeout
|
|
134
|
+
|
|
135
|
+
The default is **10 minutes per logical job** (previously 2 minutes). In Pi:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
/delegate off
|
|
139
|
+
/delegate timeout 15
|
|
140
|
+
/delegate on
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`/delegate timeout` shows the current value; `/delegate timeout reset` saves the
|
|
144
|
+
10-minute default. Tab completion suggests common values. Use whole minutes from
|
|
145
|
+
**1 to 60**; there is no unlimited setting. The batch deadline is twice the job
|
|
146
|
+
timeout (20 minutes by default), so it does not truncate the configured job window.
|
|
147
|
+
Both deadlines start at batch admission and include queue and follow-up time; a
|
|
148
|
+
follow-up never gets a fresh timeout. Provider requests use the remaining job window,
|
|
149
|
+
not a separate two-minute cap. Provider-side limits may still end requests sooner.
|
|
150
|
+
|
|
151
|
+
Changes require delegation to be off, including when model setup is pending or paused.
|
|
152
|
+
They apply to this process and future Pi starts; they cannot extend old jobs, reset
|
|
153
|
+
call quotas or free requests still settling. `/delegate on`, `status`, `limits` and
|
|
154
|
+
Strict Guard policy summaries display the effective timeout. Only the human command
|
|
155
|
+
can configure it; the model-facing tool has no timeout-setting operation.
|
|
156
|
+
|
|
157
|
+
The preference is stored in `<agent-dir>/specpi/delegation/settings.json`, where
|
|
158
|
+
`<agent-dir>` is `PI_CODING_AGENT_DIR` or `~/.pi/agent`. It contains only
|
|
159
|
+
`{"schema":1,"timeoutMinutes":15}`. Saves atomically replace this file and keep the
|
|
160
|
+
previous contents plus their SHA-256 in `settings.json.bak`. No Pi settings,
|
|
161
|
+
authentication, sessions or history are read or changed by this preference store.
|
|
162
|
+
The human-selected agent directory is resolved once, supporting platform path aliases.
|
|
163
|
+
Preference files and SpecPi subdirectories must not be links. Malformed, oversized or
|
|
164
|
+
unreadable settings block activation rather than silently using another timeout. Repair them manually
|
|
165
|
+
and restart Pi. Manual edits and changes from another Pi process take effect on
|
|
166
|
+
restart; `/reload` preserves the current process policy and counters. The preference
|
|
167
|
+
survives uninstall as user-owned configuration.
|
|
168
|
+
|
|
126
169
|
## Admit a specific purpose
|
|
127
170
|
|
|
128
171
|
| Mode | Required structure | Context and tools |
|
|
@@ -165,7 +208,7 @@ and counters. [Protocol and executable examples](protocol.md) define the exact f
|
|
|
165
208
|
| Batches / jobs | 4 batches per Pi process; one unresolved batch; 2 jobs per batch |
|
|
166
209
|
| SDK model invocations | 32 per Pi process, 8 per batch; 4 per logical job including follow-up |
|
|
167
210
|
| Follow-ups / retries | 1 changed-input follow-up per job; provider and session retries disabled |
|
|
168
|
-
| Time |
|
|
211
|
+
| Time | 10 minutes per job by default (human configurable 1–60); batch twice that; queue/follow-up included |
|
|
169
212
|
| Packet / child context | 256 KiB, checked before dispatch |
|
|
170
213
|
| Selected sources | 200 files and 8 MiB per batch |
|
|
171
214
|
| Tools | 12 calls and 64 KiB total returned JSON per logical job |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Status: archived target architecture, not the implemented runtime contract.
|
|
4
4
|
|
|
5
|
-
The experimental implementation is part of `specpi` and is
|
|
5
|
+
The experimental implementation is part of `specpi` and is enabled by default at Pi startup.
|
|
6
6
|
Read the [implemented guide](README.md) and [calls/time protocol](protocol.md) for
|
|
7
7
|
supported commands, tested API compatibility, limits and trust assumptions. This
|
|
8
8
|
document preserves the original broader proposal, including unimplemented live-web,
|
|
@@ -4,8 +4,12 @@ This is the implemented in-process API. It has no HTTP listener, daemon, child p
|
|
|
4
4
|
or child session store. The broader [target protocol](design-protocol.md) remains a
|
|
5
5
|
proposal; its stronger transport/attempt/cost gates are not supplied by this version.
|
|
6
6
|
|
|
7
|
-
The extension loads through normal `pi` package discovery and
|
|
8
|
-
the
|
|
7
|
+
The extension loads through normal `pi` package discovery and enables delegation at
|
|
8
|
+
the first session start of each Pi process, including noninteractive modes. Startup
|
|
9
|
+
preflights the host but launches no workers or model inference; Pi-owned authentication
|
|
10
|
+
preparation may occur. `/delegate off` and safety revocations survive reloads and session
|
|
11
|
+
switches; `/delegate on` re-enables it, and restarting Pi reapplies the on default.
|
|
12
|
+
Compatibility is checked through required public SDK
|
|
9
13
|
capabilities; there is no exact-version allowlist. Missing APIs prevent activation and
|
|
10
14
|
are named in the error. The runtime also verifies the created session's thinking,
|
|
11
15
|
tools and streaming interface. Tested versions are evidence, not an activation gate.
|
|
@@ -18,7 +22,7 @@ overrides, model-specific headers, startup proxy configuration and safe model-de
|
|
|
18
22
|
mismatches fail preflight. Parent request hooks,
|
|
19
23
|
ephemeral runtime settings, session affinity and ambient resources are not inherited.
|
|
20
24
|
|
|
21
|
-
Command Guard is optional. Absent and Off states permit
|
|
25
|
+
Command Guard is optional. Absent and Off states permit activation; an installed
|
|
22
26
|
Guard's Strict approvals and explicit locks remain enforced. Unready or duplicate
|
|
23
27
|
Guard responders prevent activation with a specific error. Guard state changes revoke
|
|
24
28
|
the current delegation generation. Snapshot tools and resource limits are enforced
|
|
@@ -36,9 +40,22 @@ data are rejected. The host creates identities and receipts; workers cannot supp
|
|
|
36
40
|
The protocol identifier `bounded-pi-sessions-v1` and inference contract
|
|
37
41
|
`pi-agent-session-v1` describe the host implementation, not model-selected options.
|
|
38
42
|
|
|
43
|
+
## Human timeout preference
|
|
44
|
+
|
|
45
|
+
Logical jobs default to 10 minutes; batches default to 20 minutes. Human
|
|
46
|
+
`/delegate timeout <minutes>` saves a whole-minute value from 1 to 60 while delegation
|
|
47
|
+
is off; `timeout reset` saves 10. The batch window is twice the job window.
|
|
48
|
+
Deadlines start at admission and include queue and the single follow-up. Provider
|
|
49
|
+
request timeouts and the adapter abort timer honor the remaining job window, capped
|
|
50
|
+
at 60 minutes. Settings changes invalidate old policy bindings without resetting
|
|
51
|
+
quotas or settling slots. No tool operation or packet field can change the timeout.
|
|
52
|
+
See the [configuration guide](README.md#configure-the-timeout) for persistence and
|
|
53
|
+
recovery. Status limits, packet digests and Guard fingerprints bind the effective
|
|
54
|
+
policy rather than assuming the shipped default.
|
|
55
|
+
|
|
39
56
|
## Submit a batch
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
When delegation is enabled, the parent calls the `delegate` tool:
|
|
42
59
|
|
|
43
60
|
```json
|
|
44
61
|
{
|
|
@@ -235,7 +252,7 @@ maximum. These are experiment limits, not research-derived optimal values.
|
|
|
235
252
|
Human off/on, task changes,
|
|
236
253
|
branch navigation, model selection, guard changes and reloads revoke old generations;
|
|
237
254
|
they do not create a new resource allowance. Normal parent turns do not revoke a job.
|
|
238
|
-
Model and thinking selections retain the human
|
|
255
|
+
Model and thinking selections retain the default or human activation choice. The extension
|
|
239
256
|
preflights the latest selected host and resumes dispatch automatically, without
|
|
240
257
|
replaying old jobs. Unsupported selections pause dispatch and report `pauseReason`;
|
|
241
258
|
a compatible selection resumes it. Status exposes `requested`, `updating` and
|
|
@@ -229,7 +229,10 @@ export default function registerCommandGuard(
|
|
|
229
229
|
const startupGeneration = state.generation;
|
|
230
230
|
try {
|
|
231
231
|
subscribeGuardState();
|
|
232
|
-
|
|
232
|
+
// Pi RPC binds session_start before it starts reading UI responses.
|
|
233
|
+
// Begin guarded without a startup dialog; /guard remains available
|
|
234
|
+
// for explicit changes once the RPC client is connected.
|
|
235
|
+
const choice = ctx.hasUI && ctx.mode !== "rpc" ? await startupChoice(ctx, startupTimeoutMs) : undefined;
|
|
233
236
|
if (state.generation !== startupGeneration) {
|
|
234
237
|
return;
|
|
235
238
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createSnapshot } from "./snapshot.mjs";
|
|
3
|
-
import { LIMITS, digest, validateOperation } from "./protocol.mjs";
|
|
3
|
+
import { LIMITS, digest, timeoutLimits, validateOperation } from "./protocol.mjs";
|
|
4
4
|
import { runWorker } from "./worker.mjs";
|
|
5
5
|
import { DelegationError, publicErrorMessage } from "./errors.mjs";
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ export function createDelegationController({
|
|
|
17
17
|
worker = runWorker,
|
|
18
18
|
getGuard = () => "absent",
|
|
19
19
|
}) {
|
|
20
|
-
|
|
20
|
+
let policy = Object.freeze(
|
|
21
21
|
Object.fromEntries(
|
|
22
22
|
Object.entries(LIMITS).map(([key, maximum]) => {
|
|
23
23
|
const value = limits[key] ?? maximum;
|
|
@@ -707,6 +707,17 @@ export function createDelegationController({
|
|
|
707
707
|
status,
|
|
708
708
|
presentation,
|
|
709
709
|
invalidate,
|
|
710
|
+
// Human command only; never exposed through execute() or the model schema.
|
|
711
|
+
setTimeoutMinutes(minutes) {
|
|
712
|
+
const times = timeoutLimits(minutes);
|
|
713
|
+
if (enabled) {
|
|
714
|
+
throw new DelegationError("Turn delegation off before changing its timeout: /delegate off");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
invalidate("timeout policy changed");
|
|
718
|
+
policy = Object.freeze({ ...policy, ...times });
|
|
719
|
+
changed();
|
|
720
|
+
},
|
|
710
721
|
enable() {
|
|
711
722
|
const host = getHost();
|
|
712
723
|
const guard = getGuard();
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { DelegationError, publicErrorMessage } from "./errors.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { createDelegationController } from "./core.mjs";
|
|
4
|
+
import { DEFAULT_TIMEOUT_MINUTES, timeoutLimits } from "./protocol.mjs";
|
|
4
5
|
import { createLivePanel, createToolRenderers, readableLimits, readableStatus } from "./presentation.mjs";
|
|
5
6
|
|
|
7
|
+
const USAGE = "Usage: /delegate [on|off|status|limits|timeout [<minutes>|reset]|cancel <batchId>]";
|
|
8
|
+
|
|
6
9
|
const integer = { type: "integer", minimum: 0 };
|
|
7
10
|
const string = { type: "string" };
|
|
8
11
|
const strings = { type: "array", items: string };
|
|
@@ -92,7 +95,7 @@ export const DELEGATE_SCHEMA = {
|
|
|
92
95
|
/** The native entry supplies a preflighted Pi child-session host for the active context. */
|
|
93
96
|
export function createDelegationExtension(
|
|
94
97
|
getHost,
|
|
95
|
-
{ root = process.cwd(), controllerOptions = {}, prepareContext = () => {}, presentation } = {},
|
|
98
|
+
{ root = process.cwd(), controllerOptions = {}, prepareContext = () => {}, presentation, timeoutStore } = {},
|
|
96
99
|
) {
|
|
97
100
|
let currentPi;
|
|
98
101
|
let currentContext;
|
|
@@ -101,6 +104,7 @@ export function createDelegationExtension(
|
|
|
101
104
|
let detach = () => {};
|
|
102
105
|
|
|
103
106
|
let requested = false;
|
|
107
|
+
let startupActivation = true;
|
|
104
108
|
let requestedGuard;
|
|
105
109
|
let boundHost;
|
|
106
110
|
let pending;
|
|
@@ -130,6 +134,14 @@ export function createDelegationExtension(
|
|
|
130
134
|
getGuard,
|
|
131
135
|
onChange: updatePresentation,
|
|
132
136
|
});
|
|
137
|
+
let timeoutLoaded = !timeoutStore;
|
|
138
|
+
const loadTimeout = () => {
|
|
139
|
+
if (!timeoutLoaded) {
|
|
140
|
+
controller.setTimeoutMinutes(timeoutStore.load());
|
|
141
|
+
timeoutLoaded = true;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
133
145
|
const panel = presentation ? createLivePanel(() => controller.presentation(), presentation) : undefined;
|
|
134
146
|
|
|
135
147
|
const status = () => ({
|
|
@@ -155,7 +167,11 @@ export function createDelegationExtension(
|
|
|
155
167
|
panel?.update();
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
function invalidate(reason) {
|
|
170
|
+
function invalidate(reason, preserveStartupDefault = false) {
|
|
171
|
+
if (!preserveStartupDefault) {
|
|
172
|
+
startupActivation = false;
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
requested = false;
|
|
160
176
|
pending = undefined;
|
|
161
177
|
boundHost = undefined;
|
|
@@ -186,7 +202,7 @@ export function createDelegationExtension(
|
|
|
186
202
|
const isBound = () => issuedEpoch === bindingEpoch;
|
|
187
203
|
toolsReady = false;
|
|
188
204
|
prepareContext(undefined, true);
|
|
189
|
-
invalidate("runtime factory rebound");
|
|
205
|
+
invalidate("runtime factory rebound", true);
|
|
190
206
|
currentPi = pi;
|
|
191
207
|
currentContext = undefined;
|
|
192
208
|
const refreshSelection = async (ctx) => {
|
|
@@ -325,7 +341,7 @@ export function createDelegationExtension(
|
|
|
325
341
|
});
|
|
326
342
|
}
|
|
327
343
|
|
|
328
|
-
pi.on("session_start", (_event, ctx) => {
|
|
344
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
329
345
|
if (!isBound()) {
|
|
330
346
|
return;
|
|
331
347
|
}
|
|
@@ -333,12 +349,27 @@ export function createDelegationExtension(
|
|
|
333
349
|
currentContext = ctx;
|
|
334
350
|
toolsReady = true;
|
|
335
351
|
panel?.bind(ctx);
|
|
336
|
-
invalidate("session started or resources reloaded");
|
|
352
|
+
invalidate("session started or resources reloaded", true);
|
|
337
353
|
try {
|
|
354
|
+
loadTimeout();
|
|
338
355
|
prepareContext(ctx, true);
|
|
356
|
+
// Consume the default only after synchronous preparation, which
|
|
357
|
+
// may revoke it. Off and safety events cancel it even before startup.
|
|
358
|
+
const activate = startupActivation;
|
|
359
|
+
startupActivation = false;
|
|
360
|
+
if (activate && isBound()) {
|
|
361
|
+
requested = true;
|
|
362
|
+
requestedGuard = getGuard();
|
|
363
|
+
await refreshSelection(ctx);
|
|
364
|
+
if (isBound() && requested && pauseReason) {
|
|
365
|
+
ctx.ui.notify(`Delegation paused: ${pauseReason}`, "warning");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
339
368
|
} catch (error) {
|
|
340
369
|
pauseReason = publicErrorMessage(error);
|
|
341
370
|
ctx.ui.notify(`Delegation unavailable: ${pauseReason}`, "warning");
|
|
371
|
+
} finally {
|
|
372
|
+
startupActivation = false;
|
|
342
373
|
}
|
|
343
374
|
});
|
|
344
375
|
pi.on("session_shutdown", () => {
|
|
@@ -368,9 +399,23 @@ export function createDelegationExtension(
|
|
|
368
399
|
}
|
|
369
400
|
});
|
|
370
401
|
pi.registerCommand("delegate", {
|
|
371
|
-
description:
|
|
402
|
+
description:
|
|
403
|
+
"Control read-only delegation (on at Pi startup), inspect limits, or save timeout <minutes> (1–60; reset: 10)",
|
|
372
404
|
getArgumentCompletions: (prefix) =>
|
|
373
|
-
[
|
|
405
|
+
[
|
|
406
|
+
"on",
|
|
407
|
+
"off",
|
|
408
|
+
"status",
|
|
409
|
+
"limits",
|
|
410
|
+
"timeout",
|
|
411
|
+
"timeout 5",
|
|
412
|
+
"timeout 10",
|
|
413
|
+
"timeout 15",
|
|
414
|
+
"timeout 30",
|
|
415
|
+
"timeout 60",
|
|
416
|
+
"timeout reset",
|
|
417
|
+
"cancel",
|
|
418
|
+
]
|
|
374
419
|
.filter((value) => value.startsWith(prefix.trim()))
|
|
375
420
|
.map((value) => ({ value, label: value })),
|
|
376
421
|
handler: async (args, ctx) => {
|
|
@@ -383,8 +428,8 @@ export function createDelegationExtension(
|
|
|
383
428
|
currentContext = ctx;
|
|
384
429
|
const [action = "status", id, extra] = args.trim().split(/\s+/u).filter(Boolean);
|
|
385
430
|
try {
|
|
386
|
-
if (extra || (id &&
|
|
387
|
-
throw new DelegationError(
|
|
431
|
+
if (extra || (id && !["cancel", "timeout"].includes(action))) {
|
|
432
|
+
throw new DelegationError(USAGE);
|
|
388
433
|
}
|
|
389
434
|
|
|
390
435
|
if (action === "on") {
|
|
@@ -392,6 +437,7 @@ export function createDelegationExtension(
|
|
|
392
437
|
throw new DelegationError("Delegation activation requires a human interactive command");
|
|
393
438
|
}
|
|
394
439
|
|
|
440
|
+
loadTimeout();
|
|
395
441
|
requested = true;
|
|
396
442
|
requestedGuard = getGuard();
|
|
397
443
|
await refreshSelection(ctx);
|
|
@@ -408,7 +454,38 @@ export function createDelegationExtension(
|
|
|
408
454
|
|
|
409
455
|
syncActiveTool(true);
|
|
410
456
|
ctx.ui.notify(
|
|
411
|
-
`Delegation enabled: Pi child sessions for review and scout, ${state.model.provider}/${state.model.id}, thinking ${state.model.thinkingLevel ?? "Pi configured"}. At most 2 workers, 4 batches and 32 SDK inference calls per Pi process;
|
|
457
|
+
`Delegation enabled: Pi child sessions for review and scout, ${state.model.provider}/${state.model.id}, thinking ${state.model.thinkingLevel ?? "Pi configured"}. At most 2 workers, 4 batches and 32 SDK inference calls per Pi process; ${state.limits.jobMs / 60_000} minutes per job, ${state.limits.batchMs / 60_000} minutes per batch. Configure with /delegate timeout <minutes> while off (saved across restarts). Workers see only supplied text and selected snapshots. Pi owns configured authentication; temporary parent provider/auth overrides and parent hooks are not inherited. No shell, edits, recursion, automatic retries or compaction. /delegate off cancels workers; SDK requests still settling retain their slots. These limits do not guarantee remote termination or a billing cap.`,
|
|
458
|
+
"info",
|
|
459
|
+
);
|
|
460
|
+
} else if (action === "timeout") {
|
|
461
|
+
if (id) {
|
|
462
|
+
if (!ctx.hasUI) {
|
|
463
|
+
throw new DelegationError("Timeout changes require a human interactive command");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (requested || pending || controller.status().enabled) {
|
|
467
|
+
throw new DelegationError(
|
|
468
|
+
"Turn delegation off before changing its timeout: /delegate off",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const minutes =
|
|
473
|
+
id === "reset" ? DEFAULT_TIMEOUT_MINUTES : /^\d+$/u.test(id) ? Number(id) : NaN;
|
|
474
|
+
timeoutLimits(minutes);
|
|
475
|
+
if (!timeoutStore) {
|
|
476
|
+
throw new DelegationError("Persistent delegation settings are unavailable");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
timeoutStore.save(minutes);
|
|
480
|
+
controller.setTimeoutMinutes(minutes);
|
|
481
|
+
timeoutLoaded = true;
|
|
482
|
+
} else {
|
|
483
|
+
loadTimeout();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const limits = controller.status().limits;
|
|
487
|
+
ctx.ui.notify(
|
|
488
|
+
`Delegation timeout: ${limits.jobMs / 60_000} minutes per job; ${limits.batchMs / 60_000} minutes per batch, including queue and follow-up time. ${id ? "Saved for this process and future Pi starts. " : ""}Use /delegate timeout <minutes> (1–60) or /delegate timeout reset (10). Changes require /delegate off; call budgets do not reset.`,
|
|
412
489
|
"info",
|
|
413
490
|
);
|
|
414
491
|
} else if (action === "off") {
|
|
@@ -428,7 +505,7 @@ export function createDelegationExtension(
|
|
|
428
505
|
const state = status();
|
|
429
506
|
ctx.ui.notify(action === "limits" ? readableLimits(state) : readableStatus(state), "info");
|
|
430
507
|
} else {
|
|
431
|
-
throw new DelegationError(
|
|
508
|
+
throw new DelegationError(USAGE);
|
|
432
509
|
}
|
|
433
510
|
} catch (error) {
|
|
434
511
|
ctx.ui.notify(publicErrorMessage(error), "error");
|
|
@@ -439,7 +516,7 @@ export function createDelegationExtension(
|
|
|
439
516
|
name: "delegate",
|
|
440
517
|
label: "Delegate",
|
|
441
518
|
description:
|
|
442
|
-
"Delegate an independent frozen review or substantial selected-source analysis to a real Pi child session.
|
|
519
|
+
"Delegate an independent frozen review or substantial selected-source analysis to a real Pi child session. Enabled by default at Pi startup; respect /delegate off and safety revocations. review: check artifacts against assigned requirements in fresh context; scout: answer a distinct evidence question over selected sources. Prefer one worker and parent-only execution for small, sequential or routine work. No shell, edits or live web. run returns immediately; collect waits for advisory evidence, then resolve findings after verification. One changed-input follow_up shares the original budget/deadline. Never grants permission or proves task completion.",
|
|
443
520
|
parameters: DELEGATE_SCHEMA,
|
|
444
521
|
...(presentation ? createToolRenderers(presentation) : {}),
|
|
445
522
|
execute: async (_id, input, signal, _update, ctx) => {
|
|
@@ -4,9 +4,10 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { createDelegationExtension } from "./extension.mjs";
|
|
5
5
|
import { createNativePiHost, getPiSessionCompatibilityError } from "./provider.mjs";
|
|
6
6
|
import { DelegationError } from "./errors.mjs";
|
|
7
|
+
import { createTimeoutStore } from "./settings.mjs";
|
|
7
8
|
|
|
8
9
|
const stateKey = Symbol.for("specpi.delegation.native.v1");
|
|
9
|
-
const revision =
|
|
10
|
+
const revision = 9;
|
|
10
11
|
|
|
11
12
|
export async function withPiCompatibility(sdk, loadCompatibility) {
|
|
12
13
|
if (typeof sdk.clampThinkingLevel === "function") {
|
|
@@ -121,7 +122,12 @@ function createState(root, sdk, presentation) {
|
|
|
121
122
|
}
|
|
122
123
|
};
|
|
123
124
|
|
|
124
|
-
const extensionFactory = createDelegationExtension(() => host, {
|
|
125
|
+
const extensionFactory = createDelegationExtension(() => host, {
|
|
126
|
+
root,
|
|
127
|
+
prepareContext,
|
|
128
|
+
presentation,
|
|
129
|
+
timeoutStore: createTimeoutStore(),
|
|
130
|
+
});
|
|
125
131
|
const factory = (pi) => {
|
|
126
132
|
getThinkingLevel = () => pi.getThinkingLevel();
|
|
127
133
|
extensionFactory(pi);
|
|
@@ -145,6 +145,7 @@ export function readableStatus(state) {
|
|
|
145
145
|
`Delegation ${mode} · ${state.active}/${state.limits.concurrency} workers active`,
|
|
146
146
|
`Process budget: ${state.sessionCalls}/${state.limits.sessionCalls} model calls · ${state.sessionBatches}/${state.limits.sessionBatches} batches used`,
|
|
147
147
|
`Command Guard: ${plain(state.guard)}`,
|
|
148
|
+
`Timeout: ${state.limits.jobMs / 60_000} minutes per job · ${state.limits.batchMs / 60_000} minutes per batch (/delegate timeout)`,
|
|
148
149
|
];
|
|
149
150
|
if (state.model) {
|
|
150
151
|
lines.push(
|
|
@@ -176,7 +177,7 @@ export function readableLimits(state) {
|
|
|
176
177
|
return ` ${label}: ${value}${unit}`;
|
|
177
178
|
});
|
|
178
179
|
|
|
179
|
-
return `${readableStatus(state)}\
|
|
180
|
+
return `${readableStatus(state)}\nEffective limits:\n${limits.join("\n")}\nBudgets do not reset on off/on or reload. Cancellation is best effort; no billing cap.`;
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
export function createToolRenderers({ truncateToWidth, wrapTextWithAnsi }) {
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { DelegationError } from "./errors.mjs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
|
|
4
|
+
export const DEFAULT_TIMEOUT_MINUTES = 10;
|
|
5
|
+
export const MAX_TIMEOUT_MINUTES = 60;
|
|
6
|
+
export const MAX_JOB_MS = MAX_TIMEOUT_MINUTES * 60_000;
|
|
7
|
+
|
|
8
|
+
export function timeoutLimits(minutes) {
|
|
9
|
+
if (!Number.isSafeInteger(minutes) || minutes < 1 || minutes > MAX_TIMEOUT_MINUTES) {
|
|
10
|
+
throw new DelegationError(`Delegation timeout must be a whole number from 1 to ${MAX_TIMEOUT_MINUTES} minutes`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return { jobMs: minutes * 60_000, batchMs: minutes * 120_000 };
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
export const LIMITS = Object.freeze({
|
|
5
17
|
concurrency: 2,
|
|
6
18
|
sessionBatches: 4,
|
|
@@ -8,8 +20,7 @@ export const LIMITS = Object.freeze({
|
|
|
8
20
|
batchJobs: 2,
|
|
9
21
|
batchCalls: 8,
|
|
10
22
|
jobCalls: 4,
|
|
11
|
-
|
|
12
|
-
batchMs: 300_000,
|
|
23
|
+
...timeoutLimits(DEFAULT_TIMEOUT_MINUTES),
|
|
13
24
|
contextBytes: 256 * 1024,
|
|
14
25
|
retainedResponseBytes: 256 * 1024,
|
|
15
26
|
toolBytes: 64 * 1024,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DelegationError } from "./errors.mjs";
|
|
2
|
+
import { MAX_JOB_MS } from "./protocol.mjs";
|
|
2
3
|
|
|
3
4
|
const MAX_CONTEXT_BYTES = 256 * 1024;
|
|
4
5
|
const MAX_OUTPUT_TOKENS = 8192;
|
|
@@ -513,7 +514,7 @@ export function createNativePiHost(ctx, { id, isCurrent, sdk, thinkingLevel } =
|
|
|
513
514
|
),
|
|
514
515
|
maxTokens: Math.min(MAX_OUTPUT_TOKENS, childModel.maxTokens, run.controls.limits.outputTokens),
|
|
515
516
|
maxRetries: 0,
|
|
516
|
-
timeoutMs: Math.max(1, Math.min(
|
|
517
|
+
timeoutMs: Math.max(1, Math.min(MAX_JOB_MS, run.controls.deadline - Date.now())),
|
|
517
518
|
onPayload: async (payload, providerModel) => {
|
|
518
519
|
assertRun();
|
|
519
520
|
const result = (await options.onPayload?.(payload, providerModel)) ?? payload;
|
|
@@ -637,7 +638,7 @@ export function createNativePiHost(ctx, { id, isCurrent, sdk, thinkingLevel } =
|
|
|
637
638
|
active = run;
|
|
638
639
|
const cancel = () => fail(new DelegationError("Delegation session was cancelled or expired"));
|
|
639
640
|
controls.signal.addEventListener("abort", cancel, { once: true });
|
|
640
|
-
const timer = setTimeout(cancel, Math.max(1, Math.min(
|
|
641
|
+
const timer = setTimeout(cancel, Math.max(1, Math.min(MAX_JOB_MS, controls.deadline - Date.now())));
|
|
641
642
|
timer.unref?.();
|
|
642
643
|
try {
|
|
643
644
|
assertRun();
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { DEFAULT_TIMEOUT_MINUTES, timeoutLimits } from "./protocol.mjs";
|
|
6
|
+
import { DelegationError } from "./errors.mjs";
|
|
7
|
+
|
|
8
|
+
const MAX_SETTINGS_BYTES = 4096;
|
|
9
|
+
const MAX_BACKUP_BYTES = 32 * 1024;
|
|
10
|
+
|
|
11
|
+
function regularFile(file, maxBytes = MAX_SETTINGS_BYTES) {
|
|
12
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
13
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > maxBytes)) {
|
|
14
|
+
throw new Error("Unsupported settings file");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return stat;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function directory(dir, create = false) {
|
|
21
|
+
const parent = path.dirname(dir);
|
|
22
|
+
if (parent !== dir) {
|
|
23
|
+
directory(parent, create);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let stat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
27
|
+
if (!stat && create) {
|
|
28
|
+
fs.mkdirSync(dir, { mode: 0o700 });
|
|
29
|
+
stat = fs.lstatSync(dir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) {
|
|
33
|
+
throw new Error("Unsupported settings directory");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve the human-selected agent root, including platform aliases, once.
|
|
38
|
+
// Missing descendants stay non-mutating on load and are checked before creation.
|
|
39
|
+
function canonicalAgentDirectory(dir) {
|
|
40
|
+
if (fs.lstatSync(dir, { throwIfNoEntry: false })) {
|
|
41
|
+
const resolved = fs.realpathSync.native(dir);
|
|
42
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
43
|
+
throw new Error("Unsupported agent directory");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parent = path.dirname(dir);
|
|
50
|
+
if (parent === dir) {
|
|
51
|
+
throw new Error("Agent directory root is unavailable");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return path.join(canonicalAgentDirectory(parent), path.basename(dir));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function atomicWrite(file, content, maxBytes = MAX_SETTINGS_BYTES) {
|
|
58
|
+
regularFile(file, maxBytes);
|
|
59
|
+
if (Buffer.byteLength(content, "utf8") > maxBytes) {
|
|
60
|
+
throw new Error("Settings write exceeds its bound");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(temporary, content, { flag: "wx", mode: 0o600 });
|
|
66
|
+
fs.renameSync(temporary, file);
|
|
67
|
+
} finally {
|
|
68
|
+
fs.rmSync(temporary, { force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Own preference only; never reads Pi settings, credentials or session state. */
|
|
73
|
+
export function createTimeoutStore(
|
|
74
|
+
agentDir = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"),
|
|
75
|
+
) {
|
|
76
|
+
const requestedAgentDir = path.resolve(agentDir);
|
|
77
|
+
let resolvedAgentDir;
|
|
78
|
+
const location = () => {
|
|
79
|
+
resolvedAgentDir ??= canonicalAgentDirectory(requestedAgentDir);
|
|
80
|
+
const dir = path.join(resolvedAgentDir, "specpi", "delegation");
|
|
81
|
+
|
|
82
|
+
return { dir, file: path.join(dir, "settings.json") };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const read = () => {
|
|
86
|
+
const { dir, file } = location();
|
|
87
|
+
directory(dir);
|
|
88
|
+
if (!regularFile(file)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const content = fs.readFileSync(file, "utf8");
|
|
93
|
+
const settings = JSON.parse(content);
|
|
94
|
+
if (
|
|
95
|
+
!settings ||
|
|
96
|
+
settings.schema !== 1 ||
|
|
97
|
+
Object.keys(settings).length !== 2 ||
|
|
98
|
+
!Object.hasOwn(settings, "timeoutMinutes")
|
|
99
|
+
) {
|
|
100
|
+
throw new Error("Invalid settings schema");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
timeoutLimits(settings.timeoutMinutes);
|
|
104
|
+
|
|
105
|
+
return { content, minutes: settings.timeoutMinutes };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
load() {
|
|
110
|
+
try {
|
|
111
|
+
return read()?.minutes ?? DEFAULT_TIMEOUT_MINUTES;
|
|
112
|
+
} catch {
|
|
113
|
+
throw new DelegationError(
|
|
114
|
+
"Cannot read delegation settings. Check <agent-dir>/specpi/delegation/settings.json (schema 1, timeoutMinutes: integer 1–60) and its permissions; restart Pi after repairing it.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
save(minutes) {
|
|
119
|
+
timeoutLimits(minutes);
|
|
120
|
+
try {
|
|
121
|
+
const previous = read();
|
|
122
|
+
const { dir, file } = location();
|
|
123
|
+
directory(dir, true);
|
|
124
|
+
if (previous) {
|
|
125
|
+
atomicWrite(
|
|
126
|
+
`${file}.bak`,
|
|
127
|
+
`${JSON.stringify({ sha256: createHash("sha256").update(previous.content).digest("hex"), content: previous.content })}\n`,
|
|
128
|
+
MAX_BACKUP_BYTES,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
atomicWrite(file, `${JSON.stringify({ schema: 1, timeoutMinutes: minutes })}\n`);
|
|
133
|
+
} catch {
|
|
134
|
+
throw new DelegationError(
|
|
135
|
+
"Cannot save delegation settings. Check <agent-dir>/specpi/delegation/settings.json and its permissions; the active timeout is unchanged.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/extensions/spec.ts
CHANGED
|
@@ -634,6 +634,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
634
634
|
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
635
635
|
},
|
|
636
636
|
handler: async (args, ctx) => {
|
|
637
|
+
if (ctx.mode === "rpc") {
|
|
638
|
+
ctx.ui.notify(
|
|
639
|
+
"/spec controls the interactive terminal interface. SpecPi Chat manages its own response and tool display.",
|
|
640
|
+
"info",
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
637
646
|
lastContext = ctx;
|
|
638
647
|
const action = args.trim().toLowerCase();
|
|
639
648
|
if (!action) {
|
|
@@ -685,7 +685,13 @@ export default function toolWishlist(pi: ExtensionAPI) {
|
|
|
685
685
|
reportPath,
|
|
686
686
|
truncated: display.truncated,
|
|
687
687
|
});
|
|
688
|
-
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (
|
|
691
|
+
(ctx.mode === "rpc" || !supportsReportEntries) &&
|
|
692
|
+
(ctx.mode === "rpc" || ctx.mode === "tui" || ctx.mode === undefined) &&
|
|
693
|
+
typeof ctx.ui.editor === "function"
|
|
694
|
+
) {
|
|
689
695
|
await ctx.ui.editor(
|
|
690
696
|
"SpecPi Wishlist (view only; changes are ignored)",
|
|
691
697
|
`${content}\n\n---\nReport: ${displayPath}`,
|
|
@@ -1113,7 +1113,7 @@ export default function workflowControls(pi: ExtensionAPI) {
|
|
|
1113
1113
|
createdAt: new Date().toISOString(),
|
|
1114
1114
|
};
|
|
1115
1115
|
pi.appendEntry(TASK_HANDOFF_ENTRY, data);
|
|
1116
|
-
if (!supportsEntryRenderer && typeof ctx.ui.editor === "function") {
|
|
1116
|
+
if ((ctx.mode === "rpc" || !supportsEntryRenderer) && typeof ctx.ui.editor === "function") {
|
|
1117
1117
|
await ctx.ui.editor("Task handoff (view only)", markdown);
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
@@ -1738,7 +1738,7 @@ export default function workflowControls(pi: ExtensionAPI) {
|
|
|
1738
1738
|
|
|
1739
1739
|
if (!latestChallenge) {
|
|
1740
1740
|
ctx.ui.notify("No completed challenge exists on this session branch.", "info");
|
|
1741
|
-
} else if (supportsEntryRenderer) {
|
|
1741
|
+
} else if (supportsEntryRenderer && ctx.mode !== "rpc") {
|
|
1742
1742
|
pi.appendEntry(CHALLENGE_ENTRY, { ...latestChallenge, kind: "display" });
|
|
1743
1743
|
} else if (typeof ctx.ui.editor === "function") {
|
|
1744
1744
|
await ctx.ui.editor("Completion challenge (view only)", latestChallenge.markdown ?? "Unavailable");
|
package/package.json
CHANGED
|
@@ -367,6 +367,28 @@ function assertInstalledLifecycle(packageRoot, binPath, temporaryRoot, baseEnv)
|
|
|
367
367
|
runCli(["install", ...lifecycleFlags]);
|
|
368
368
|
runCli(["doctor"], { timeout: 300_000 });
|
|
369
369
|
|
|
370
|
+
const privateEvidence = new Map([
|
|
371
|
+
[path.join(agentDir, "specpi", "delegation", "settings.json"), '{"schema":1,"timeoutMinutes":15}\n'],
|
|
372
|
+
[
|
|
373
|
+
path.join(agentDir, "specpi", "delegation", "settings.json.bak"),
|
|
374
|
+
`${JSON.stringify({
|
|
375
|
+
content: '{"schema":1,"timeoutMinutes":10}\n',
|
|
376
|
+
sha256: createHash("sha256").update('{"schema":1,"timeoutMinutes":10}\n').digest("hex"),
|
|
377
|
+
})}\n`,
|
|
378
|
+
],
|
|
379
|
+
[path.join(agentDir, "specpi", "wishlist", "observations.jsonl"), '{"private":true}\n'],
|
|
380
|
+
[
|
|
381
|
+
path.join(agentDir, "specpi", "wishlist", "decisions.jsonl"),
|
|
382
|
+
'{"action":"retire","journal":{"schema":1,"evidence":["private proof"],"gates":["npm run check"],"version":"0.10.0"}}\n',
|
|
383
|
+
],
|
|
384
|
+
[path.join(agentDir, "specpi", "experiments", "registry.json"), '{"private":true}\n'],
|
|
385
|
+
[path.join(agentDir, "specpi", "experiments", "patches", "package-smoke.patch"), "private patch\n"],
|
|
386
|
+
]);
|
|
387
|
+
for (const [file, content] of privateEvidence) {
|
|
388
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
389
|
+
fs.writeFileSync(file, content, { mode: 0o600 });
|
|
390
|
+
}
|
|
391
|
+
|
|
370
392
|
const guardDirectory = path.join(agentDir, "extensions", "command-guard");
|
|
371
393
|
const driftedGuardPath = path.join(guardDirectory, "index.ts");
|
|
372
394
|
fs.appendFileSync(driftedGuardPath, "\n// package-check rollback drift\n");
|
|
@@ -386,20 +408,6 @@ function assertInstalledLifecycle(packageRoot, binPath, temporaryRoot, baseEnv)
|
|
|
386
408
|
"failed update did not restore the complete managed tree",
|
|
387
409
|
);
|
|
388
410
|
|
|
389
|
-
const privateEvidence = new Map([
|
|
390
|
-
[path.join(agentDir, "specpi", "wishlist", "observations.jsonl"), '{"private":true}\n'],
|
|
391
|
-
[
|
|
392
|
-
path.join(agentDir, "specpi", "wishlist", "decisions.jsonl"),
|
|
393
|
-
'{"action":"retire","journal":{"schema":1,"evidence":["private proof"],"gates":["npm run check"],"version":"0.10.0"}}\n',
|
|
394
|
-
],
|
|
395
|
-
[path.join(agentDir, "specpi", "experiments", "registry.json"), '{"private":true}\n'],
|
|
396
|
-
[path.join(agentDir, "specpi", "experiments", "patches", "package-smoke.patch"), "private patch\n"],
|
|
397
|
-
]);
|
|
398
|
-
for (const [file, content] of privateEvidence) {
|
|
399
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
400
|
-
fs.writeFileSync(file, content, { mode: 0o600 });
|
|
401
|
-
}
|
|
402
|
-
|
|
403
411
|
runCli(["update", ...lifecycleFlags, "--force"]);
|
|
404
412
|
runCli(["doctor"], { timeout: 300_000 });
|
|
405
413
|
runCli(["uninstall", "--yes"]);
|
|
@@ -409,11 +417,14 @@ function assertInstalledLifecycle(packageRoot, binPath, temporaryRoot, baseEnv)
|
|
|
409
417
|
false,
|
|
410
418
|
"uninstall left a managed extension behind",
|
|
411
419
|
);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
420
|
+
for (const file of DELEGATION_MANAGED_FILES) {
|
|
421
|
+
assert.equal(
|
|
422
|
+
fs.existsSync(path.join(agentDir, "extensions", "delegation", file)),
|
|
423
|
+
false,
|
|
424
|
+
`uninstall left a managed delegation file: ${file}`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
417
428
|
for (const [file, expected] of privateEvidence) {
|
|
418
429
|
assert.equal(fs.existsSync(file), true, `uninstall removed private SpecPi evidence: ${file}`);
|
|
419
430
|
assert.equal(fs.readFileSync(file, "utf8"), expected, `uninstall changed private SpecPi evidence: ${file}`);
|
|
@@ -460,7 +471,12 @@ try {
|
|
|
460
471
|
assert.ok(fs.existsSync(tarball), "the reported npm tarball does not exist");
|
|
461
472
|
assertArtifactMatchesManifest(tarball, packResult);
|
|
462
473
|
|
|
463
|
-
const installEnv = {
|
|
474
|
+
const installEnv = {
|
|
475
|
+
...process.env,
|
|
476
|
+
PI_CODING_AGENT_DIR: path.join(temporaryRoot, "install-agent"),
|
|
477
|
+
npm_config_audit: "false",
|
|
478
|
+
npm_config_fund: "false",
|
|
479
|
+
};
|
|
464
480
|
runNpm(
|
|
465
481
|
[
|
|
466
482
|
"install",
|
package/scripts/check-syntax.mjs
CHANGED
|
@@ -26,6 +26,11 @@ export function syntaxFiles(root) {
|
|
|
26
26
|
visit(path.join(root, directory));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// The separately distributed editor extension is absent from the npm artifact.
|
|
30
|
+
if (fs.existsSync(path.join(root, "vscode"))) {
|
|
31
|
+
visit(path.join(root, "vscode"));
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
return files.sort();
|
|
30
35
|
}
|
|
31
36
|
|