specpi 0.36.3 → 0.37.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 +12 -0
- package/NPM_RELEASE.md +1 -1
- package/README.md +11 -10
- package/SECURITY_MODEL.md +4 -2
- package/THIRD_PARTY.md +5 -5
- package/package.json +4 -4
- package/scripts/lancet-guard.mjs +1 -1
- package/scripts/lib.mjs +58 -0
- package/scripts/packages.mjs +64 -31
- package/scripts/specpi.mjs +63 -7
- package/templates/settings.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0 - 2026-10-01
|
|
4
|
+
|
|
5
|
+
- Pair with SpecPi Chat 0.19.0, which shows a codemode run as one tool card: the script, the tool calls it made and its output.
|
|
6
|
+
- Move the reviewed Pi base to 1.0.0. Development fixtures, package checks and the docs pin `@earendil-works/pi-coding-agent`, `pi-ai` and `pi-tui` 1.0.0 in place of 0.84.4.
|
|
7
|
+
- Pin `pi-goal-x` 0.32.0, which widens its declared Pi support from `<0.85.0` to `<2.0.0`; 0.31.2 refused Pi 1.0.
|
|
8
|
+
- Turn on Pi's built-in codemode tool by default. Install and update add `+codemode` to `defaultTools` in Pi's settings, after `pi --version` confirms Pi 0.99 or later. Older Pi would read the entry as a plain tool list and lose its default tools, so the installer warns and adds nothing there. An existing choice is left alone: `defaultTools` that already names codemode (including `-codemode`), is empty or is not an array, or `-builtin:codemode` in `extensions`. Uninstall removes only the entry SpecPi added. Nested tool calls from codemode scripts pass through the same `tool_call` handlers as direct calls, so scope, the permission system and the LANCET guard still apply.
|
|
9
|
+
|
|
10
|
+
## 0.36.4 - 2026-10-01
|
|
11
|
+
|
|
12
|
+
- Pin `specpi-lancet-guard` 0.6.0, which adds `/lancet-guard mode <ask|block>`. Choosing what a `risky` verdict does used to mean editing `~/.pi/lancet-guard.json` by hand; the command saves `risky` there and keeps the rest of the file. Ask stays the default. Block mode blocks a `risky` verdict and stops the agent, as the hard-deny rules do; `review` asks in both modes, and the local rules and a missing model block in both. Status shows the mode, and setting one warns when a trusted project's `.pi/lancet-guard.json` overrides it there. The model is unchanged, LANCET Nano v0.4.3, so updating needs no new `/lancet-guard setup`.
|
|
13
|
+
- The README's command guard section gives LANCET Nano v0.4.3's Triage Score across its three release benchmarks, 68.3 with 9.0% of safe commands stopped, as the LANCET page does, in place of its lancet-bench-2-next comparison with Jev. The README, the LANCET and Jev pages and the security model point to `/lancet-guard mode block` instead of the raw setting, and the LANCET page lists the command. SpecPi Chat stays at 0.18.0.
|
|
14
|
+
|
|
3
15
|
## 0.36.3 - 2026-09-30
|
|
4
16
|
|
|
5
17
|
- Pin `specpi-lancet-guard` 0.5.0, which scores with LANCET Nano v0.4.3, a newly trained model in a new format. The guard's runtime is ported to v0.4.3's `classify.py`: the tokenizer loads `vocab.json` and `merges.txt`, a command longer than one 512-token window is read as overlapping windows, and bands come from the risk logit. PowerShell and cmd are now scored rather than always asking, so a PowerShell command LANCET does not flag runs; commands over 8,192 bytes still ask. A command longer than one window is scored but never cleared: review found that harmless lines put in front of a risky command pull it to `not_flagged` once it spans a second window, so a `not_flagged` verdict there asks, as every command that long did before. On lancet-bench-2-next, LANCET's new 3,204-command release benchmark, it catches 78.0% of risky commands, as v0.4.2 did, while stopping 12.4% of safe ones, against 20.0%; its Triage Score across three benchmarks is 68.3, against 54.4. On ShellRisk-Bench's test split it catches 64.8%, against 60.1%, at 3.0% stopped; on the neutral set it catches 85.8%, against 91.0%. Long commands cost about 0.3 s per window, 5 to 8 s at the byte limit; within one window, padding still moves some risky commands to `not_flagged`. The parity fixture is re-recorded with v0.4.3's runtime and gains PowerShell, cmd, window-boundary and byte-limit cases; the port matches it within 6.7e-16 in score and 1.8e-14 in risk logit, and CI decisions match on Linux, Windows and macOS. Users who update must run `/lancet-guard setup` again; a guard saved as on fails closed until they do. The README, security model, third-party notes and website now report lancet-bench-2-next, because lancet-bench-1 is retired and part of v0.4.3's training data. SpecPi Chat stays at 0.18.0.
|
package/NPM_RELEASE.md
CHANGED
|
@@ -6,7 +6,7 @@ Publishing, tags, deprecation, ownership changes and GitHub Releases require exp
|
|
|
6
6
|
|
|
7
7
|
## Prepare
|
|
8
8
|
|
|
9
|
-
1. This candidate is SpecPi **0.
|
|
9
|
+
1. This candidate is SpecPi **0.37.0**, paired with the Chat **0.19.0** VSIX. Browser QA **0.3.0** and LANCET guard **0.6.0** are already independently published; do not republish or edit their source as part of this release. Choose an unused version; update `package.json`, the dated `CHANGELOG.md` entry, README, and the website. Check Chat's version and VSIX download links. Preserve historical changelog entries.
|
|
10
10
|
2. Validate:
|
|
11
11
|
|
|
12
12
|
```sh
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
SpecPi 0.
|
|
31
|
+
SpecPi 0.37.0 is a small starting point for the [Pi coding agent](https://pi.dev/). It is one opinionated setup for how the agent should work, not a marketplace of plugins.
|
|
32
32
|
|
|
33
33
|
At the center are two built-in extensions. **Scope control** keeps each task to the files it said it would touch. The **improvement loop** turns repeated friction into small, tested changes to the setup, instead of letting prompts and workarounds pile up. Around those are eight hand-picked packages, each locked to an exact version and checked before anything installs, plus **SpecPi Chat**, a VS Code panel for working alongside the agent.
|
|
34
34
|
|
|
@@ -45,16 +45,15 @@ Everything it touches is written down, versioned, and easy to undo.
|
|
|
45
45
|
|
|
46
46
|
## A local command guard
|
|
47
47
|
|
|
48
|
-
Before the agent runs a shell command, [LANCET Nano](https://github.com/TannerMidd/LANCET-model), a 111M-parameter classifier, can check it on your own CPU. It reads Bash, PowerShell and cmd, takes about 10 ms for a typical command, needs no API key, sends nothing over the network, and costs nothing per check. Fixed rules settle the obvious cases first, blocking `rm -rf /` and passing `git status`, and the model scores the rest. A risky or uncertain verdict asks you before anything runs.
|
|
48
|
+
Before the agent runs a shell command, [LANCET Nano](https://github.com/TannerMidd/LANCET-model), a 111M-parameter classifier, can check it on your own CPU. It reads Bash, PowerShell and cmd, takes about 10 ms for a typical command, needs no API key, sends nothing over the network, and costs nothing per check. Fixed rules settle the obvious cases first, blocking `rm -rf /` and passing `git status`, and the model scores the rest. A risky or uncertain verdict asks you before anything runs; `/lancet-guard mode block` blocks risky ones outright instead.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
| Network | none | every check |
|
|
50
|
+
<p align="center">
|
|
51
|
+
<a href="https://tannermidd.github.io/SpecPi/lancet/">
|
|
52
|
+
<img src="https://tannermidd.github.io/SpecPi/media/lancet-comparison.webp" width="880" alt="Bar chart of Triage Score (higher is better) for 14 command guards on the same 7,911 commands. LANCET Nano v0.4.3 scores 68.3, catching 77.0% of risky commands while stopping 9.0% of safe ones. Next are Jev 38.9, verdict-shell-safety 32.4, Kestrel 30.7, ModernBERT bash 23.2, dcg 19.8, AutoShell-0.8B 19.2, Gyra 17.9, bash-classify 14.3, Laya 13.3, bev-decider 13.3, laya-cli-gate 12.5, sh-guard 10.9 and Shieldstral-1.0-3B 6.1.">
|
|
53
|
+
</a>
|
|
54
|
+
</p>
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
Every guard in the chart was scored once, at its own shipped setting, on the same three benchmarks, 7,911 commands in total, weighted by size: lancet-bench-2-next (1,602 risky commands and their safe look-alikes), the ShellRisk-Bench test split, and a 513-command neutral set. The [Triage Score](https://github.com/TannerMidd/LANCET-model) gives a guard a point for each risky command it asks about or blocks, and shrinks in proportion once it stops more than 10% of safe commands. The guard ships off. Run `/lancet-guard setup` in Pi once to download the model (about 109 MB, verified by SHA-256) and switch it on. [How it works, what it costs, and its limits](https://tannermidd.github.io/SpecPi/lancet/).
|
|
58
57
|
|
|
59
58
|
## Measured context
|
|
60
59
|
|
|
@@ -129,7 +128,7 @@ published figures.
|
|
|
129
128
|
|
|
130
129
|
## Install
|
|
131
130
|
|
|
132
|
-
Requires Node.js 22.19+, Git, npm, and an existing Pi installation on PATH.
|
|
131
|
+
Requires Node.js 22.19+, Git, npm, and an existing Pi installation on PATH. The base is tested with Pi 1.0.0.
|
|
133
132
|
|
|
134
133
|
```sh
|
|
135
134
|
npm install --global specpi@latest
|
|
@@ -140,6 +139,8 @@ specpi doctor
|
|
|
140
139
|
|
|
141
140
|
`plan` shows what will change without modifying anything. Restart Pi after install.
|
|
142
141
|
|
|
142
|
+
Install also turns on Pi's built-in [codemode](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/codemode.md) tool, which lets the model run a short sandboxed script that calls several tools at once. It does this by adding `+codemode` to `defaultTools` in Pi's `settings.json`, only on Pi 0.99 or later. To keep it off, put `-codemode` there instead; SpecPi leaves an existing choice alone, and uninstall removes only the entry it added.
|
|
143
|
+
|
|
143
144
|
Full setup options, package details, and requirements: [website](https://tannermidd.github.io/SpecPi/#install).
|
|
144
145
|
|
|
145
146
|
## Where things live
|
package/SECURITY_MODEL.md
CHANGED
|
@@ -62,7 +62,7 @@ Settings live in `<agent-dir>/specpi/jev/settings.json`, written atomically with
|
|
|
62
62
|
|
|
63
63
|
`specpi-lancet-guard` is pinned in the base set and ships **off**. SpecPi writes nothing into its configuration: the package's own default is off, and only its own `/lancet-guard on` (this session) or `/lancet-guard on --global` (saved in `<home>/.pi/lancet-guard.json`) turns it on. An installer run never arms or disarms it, so a saved choice survives every update. `@gotgenes/pi-permission-system` stays pinned and decides every tool call whether the guard is on or off; the guard is a second, independent check: either one can block a call, so the guard never allows what the permission system would refuse.
|
|
64
64
|
|
|
65
|
-
When on, local rules copied from `specpi-jev-guard` decide first -- hard-deny patterns block, provably read-only commands and the user's allow globs pass -- and LANCET Nano v0.4.3, a 111M-parameter CodeT5+ 220M classifier, scores the rest of `bash`, `powershell` and `background` commands in-process on the CPU through ONNX Runtime, reading Bash and PowerShell alike, so a PowerShell command it does not flag runs. `risky` asks (or blocks,
|
|
65
|
+
When on, local rules copied from `specpi-jev-guard` decide first -- hard-deny patterns block, provably read-only commands and the user's allow globs pass -- and LANCET Nano v0.4.3, a 111M-parameter CodeT5+ 220M classifier, scores the rest of `bash`, `powershell` and `background` commands in-process on the CPU through ONNX Runtime, reading Bash and PowerShell alike, so a PowerShell command it does not flag runs. `risky` asks (or blocks, after `/lancet-guard mode block` saves `"risky": "block"`), `review` -- the band Nano is unsure about -- asks, `not_flagged` runs, and what LANCET cannot read (input over 8,192 bytes) asks. Longer commands are read in full, as overlapping 512-token windows, but never cleared: a `not_flagged` verdict on one asks, because harmless lines in front of a risky command pull it under the threshold once it spans a second window. Writes and edits to protected or out-of-workspace paths ask without a model call. With no UI, an ask becomes a block unless `uncertain` is `allow`. A missing, damaged or unloadable model blocks what the rules leave open: switching the guard on accepts that, and `/lancet-guard on` is refused until the model is installed. `not_flagged` is not a safety claim; on lancet-bench-2-next, LANCET's 3,204-command release benchmark, the classifier alone caught 78.0% of risky commands and stopped 12.4% of safe ones, against 95.1% and 21.8% for Jev, and credentials are among its weakest areas. Updating the guard to a release that pins a new model leaves the old model unused: a guard saved as on fails closed until `/lancet-guard setup` fetches the new one.
|
|
66
66
|
|
|
67
67
|
Commands never leave the machine and are never executed by the guard. The only network access is `/lancet-guard setup`, run by a human: it downloads one ZIP from the pinned `TannerMidd/LANCET-model` GitHub release into a private staging directory, capped at its pinned size, and refuses it unless its SHA-256 matches the digest compiled into the package. Only then does the package's own small ZIP reader extract the six model files, each inflated to its pinned size and refused unless its own SHA-256 matches. GitHub is the only host involved, so it works where Hugging Face is blocked. The digests are checked again on every load. The package's one production dependency, `onnxruntime-node` 1.30.0, is pinned exactly; its install script would fetch CUDA libraries on Linux x64, so the installer sets `ONNXRUNTIME_NODE_INSTALL=skip`.
|
|
68
68
|
|
|
@@ -82,10 +82,12 @@ Wishlist records remain local under `<agent-dir>/specpi/`. Sanitization and salt
|
|
|
82
82
|
|
|
83
83
|
Package acquisition requests exact npm dependency saves through the child process environment and verifies the installed top-level versions before completing the transaction. A mismatch fails the operation and triggers managed-state rollback. Upstream transitive dependency ranges remain outside this pinning guarantee.
|
|
84
84
|
|
|
85
|
-
`plan` is read-only. Installation, updates, and removal require confirmation or `--yes`. SpecPi manages its two first-party extension families, improvement skill, manifest, AGENTS marker block, and the eight package entries listed in `templates/settings.json`. Normal install/update runs `pi install npm:<name>@<pin>` for every default package. Only package entries are merged; existing resource filters and unrelated settings are retained. `--skip-package-install` allows a core-only install or preserves an already configured base during update, skipping Chromium setup too. Confirmed install/update otherwise invokes the installed Browser QA Node bin with bounded subprocesses to download Chromium and check readiness. `--skip-browser-install` skips that setup only; doctor still checks readiness. SpecPi never passes `--with-deps`, auto-installs OS libraries, or installs/removes Bun. No new shell profile integration is installed.
|
|
85
|
+
`plan` is read-only. Installation, updates, and removal require confirmation or `--yes`. SpecPi manages its two first-party extension families, improvement skill, manifest, AGENTS marker block, and the eight package entries listed in `templates/settings.json`. Normal install/update runs `pi install npm:<name>@<pin>` for every default package. Only package entries and one `defaultTools` entry are merged; existing resource filters and unrelated settings are retained. `--skip-package-install` allows a core-only install or preserves an already configured base during update, skipping Chromium setup too. Confirmed install/update otherwise invokes the installed Browser QA Node bin with bounded subprocesses to download Chromium and check readiness. `--skip-browser-install` skips that setup only; doctor still checks readiness. SpecPi never passes `--with-deps`, auto-installs OS libraries, or installs/removes Bun. No new shell profile integration is installed.
|
|
86
86
|
|
|
87
87
|
Managed configuration and files are locked and backed up before mutation; first-party writes are atomic and checksum-tracked. Failure restores the saved configuration and first-party files. Package acquisition runs upstream package-manager scripts and may leave downloads, dependency changes, or external script effects even after configuration rollback. Those effects, including downloaded browser-cache bytes, are outside SpecPi's transaction and survive uninstall. Updates require `--force` before replacing modified retained resources. Retired resources are backed up before deactivation; pre-install files are restored where ownership records identify them. Old runtime directories are moved into backups without inspecting their contents. Backups and private evidence remain after uninstall and can contain sensitive local material.
|
|
88
88
|
|
|
89
|
+
Install and update, including core-only runs, add `+codemode` to `defaultTools` so Pi's built-in codemode tool is on. The installer first reads `pi --version` and adds nothing below Pi 0.99, which would read the entry as a plain tool list and drop its default tools. It also adds nothing when `defaultTools` already names codemode in any form (including `-codemode`), is empty (no built-in tools), is not an array, or when `extensions` contains `-builtin:codemode`. The manifest records only whether `defaultTools` existed before; uninstall removes `+codemode` and deletes the key only if SpecPi created it. Codemode runs model-written JavaScript in Pi's QuickJS sandbox, whose only capability is calling the session's tools. Each nested call goes through the same `tool_call` and `tool_result` handlers as a direct call, so `/scope`, the permission system and the LANCET guard see and can block it; an isolated Pi 1.0.0 session with a faux provider confirmed a nested `bash` call reaches `tool_call` with a `<parent>/<n>` id and that a block rejects it inside the script. Codemode is itself one more tool to these gates; under the permission system it falls to the `"*"` fallback unless a rule names it.
|
|
90
|
+
|
|
89
91
|
Legacy migration restores only recorded settings ownership, preserves differing user values, and removes only the SpecPi shell marker block before applying the new base. The installer does not enumerate or modify authentication, provider credential stores, trust, sessions, missions, history, or unrelated private evidence. Normal updates deliberately reapply the default package pins, retaining the original entry for removal. Uninstall restores only package entries that still match the last installed value; user edits are preserved. Downloaded packages and tools are not deleted. Resources that SpecPi never owned require separate human management.
|
|
90
92
|
|
|
91
93
|
## Browser QA package
|
package/THIRD_PARTY.md
CHANGED
|
@@ -5,13 +5,13 @@ SpecPi's npm artifact contains first-party source and no bundled runtime depende
|
|
|
5
5
|
- `@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai`, and `@earendil-works/pi-tui`: MIT, from [Pi](https://github.com/earendil-works/pi).
|
|
6
6
|
- `typebox`: MIT, from [TypeBox](https://github.com/sinclairzx81/typebox).
|
|
7
7
|
|
|
8
|
-
Development fixtures pin Pi packages to **0.
|
|
8
|
+
Development fixtures pin Pi packages to **1.0.0** and TypeBox to **1.3.7**. Host installations retain their upstream licenses and manage provider connections themselves. SpecPi does not install or configure providers.
|
|
9
9
|
|
|
10
10
|
Development formatting uses Prettier **3.9.6** (MIT), ESLint **10.9.1** (MIT), `@stylistic/eslint-plugin` **5.10.0** (MIT), `@typescript-eslint/parser` **8.68.0** (BSD-2-Clause), and TypeScript **6.0.3** (Apache-2.0). Exact versions are recorded in `package.json`; installed packages retain upstream notices. Git and Node.js are external prerequisites.
|
|
11
11
|
|
|
12
12
|
## Default packages
|
|
13
13
|
|
|
14
|
-
Reviewed on 2026-09-16 against published npm metadata and integrity-verified source archives, and `specpi-lancet-guard` 0.
|
|
14
|
+
Reviewed on 2026-09-16 against published npm metadata and integrity-verified source archives, and `specpi-lancet-guard` 0.6.0, which replaces `specpi-jev-guard` 0.4.0, from source in this repository. `pi-goal-x` 0.32.0 was reviewed on 2026-10-01 against its 0.31.2 archive: it widens Pi support to `<2.0.0` and adds goal scheduling and prompt-cache retention, with no new network, process or install-script use. All eight top-level packages declare the MIT license. Pins are authoritative in `templates/settings.json`.
|
|
15
15
|
|
|
16
16
|
SpecPi requests exact npm dependency saves for these pins and checks installed top-level versions before completing installation. The override applies to package acquisition without changing the user's global npm configuration.
|
|
17
17
|
|
|
@@ -21,16 +21,16 @@ SpecPi requests exact npm dependency saves for these pins and checks installed t
|
|
|
21
21
|
| specpi-browser-qa | 0.3.0 | [TannerMidd/SpecPi](https://github.com/TannerMidd/SpecPi/tree/browser-qa-v0.3.0/packages/browser-qa) |
|
|
22
22
|
| specpi-delegation | 0.2.0 | [TannerMidd/SpecPi](https://github.com/TannerMidd/SpecPi/tree/main/packages/delegation) |
|
|
23
23
|
| specpi-experiments | 0.1.0 | [TannerMidd/SpecPi](https://github.com/TannerMidd/SpecPi/tree/main/packages/experiments) |
|
|
24
|
-
| pi-goal-x | 0.
|
|
24
|
+
| pi-goal-x | 0.32.0 | [tmonk/pi-goal-x](https://github.com/tmonk/pi-goal-x) |
|
|
25
25
|
| @sreetej510/pi-usage | 0.10.0 | [Sreetej510/pi-extensions](https://github.com/Sreetej510/pi-extensions) |
|
|
26
26
|
| @gotgenes/pi-permission-system | 32.0.2 | [gotgenes/pi-packages](https://github.com/gotgenes/pi-packages) |
|
|
27
|
-
| specpi-lancet-guard | 0.
|
|
27
|
+
| specpi-lancet-guard | 0.6.0 | [TannerMidd/SpecPi](https://github.com/TannerMidd/SpecPi/tree/main/packages/lancet-guard) |
|
|
28
28
|
|
|
29
29
|
Transitive dependencies and their notices remain in Pi's npm installation tree. Top-level pins do not freeze upstream dependency ranges or constitute a full transitive security audit. Pi invokes npm with its upstream package-management semantics, including dependency lifecycle scripts. Browser QA has no install hook: confirmed SpecPi install/update explicitly invokes its installed Node setup bin, unless acquisition or browser setup is skipped. It downloads package-pinned Chromium without installing OS libraries. BetterWright is optional/manual and retains its own Bun-based setup; SpecPi neither removes Bun nor deletes user-owned tools. Usage reporting and web packages make their own provider/service connections. Consult upstream licenses and security policies before redistributing their components.
|
|
30
30
|
|
|
31
31
|
Pi Lens is no longer a default package. Normal updates retire only unchanged entries added by SpecPi; independent or modified entries and downloaded bytes remain, with their upstream notices. Restart Pi to unload Lens.
|
|
32
32
|
|
|
33
|
-
The combined base is tested with Pi 0.
|
|
33
|
+
The combined base is tested with Pi 1.0.0. Pi Goal X declares Pi `>=0.83.0 <2.0.0`; compatibility with newer hosts is not assumed. SpecPi's former custom browser, structural-search, background-task, and native command-guard implementations, DonSeTch, and Pi themes have been removed; command gating is the pinned `specpi-lancet-guard` package's. Its delegation and experiment implementations were not discarded: they now ship as the independent `specpi-delegation` and `specpi-experiments` packages described below. Removal restores owned package settings but does not delete downloaded upstream packages or tools. Retired private runtimes remain in local backups with their notices.
|
|
34
34
|
|
|
35
35
|
## Network services
|
|
36
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Scope control and a human-selected harness improvement loop for Pi",
|
|
5
5
|
"author": "Tanner Middleton",
|
|
6
6
|
"repository": {
|
|
@@ -94,9 +94,9 @@
|
|
|
94
94
|
"check": "npm run format:check && npm run check:syntax && npm test && npm run check:package && npm run check:vscode"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
98
|
-
"@earendil-works/pi-ai": "0.
|
|
99
|
-
"@earendil-works/pi-tui": "0.
|
|
97
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
98
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
99
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
100
100
|
"typebox": "1.3.7",
|
|
101
101
|
"playwright": "1.62.1",
|
|
102
102
|
"@stylistic/eslint-plugin": "5.10.0",
|
package/scripts/lancet-guard.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import os from "node:os";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
|
|
17
17
|
/** Must match the pin in templates/settings.json. */
|
|
18
|
-
export const GUARD_PIN = "npm:specpi-lancet-guard@0.
|
|
18
|
+
export const GUARD_PIN = "npm:specpi-lancet-guard@0.6.0";
|
|
19
19
|
|
|
20
20
|
export const RETIRED_GUARD_PIN = "npm:specpi-jev-guard@0.4.0";
|
|
21
21
|
|
package/scripts/lib.mjs
CHANGED
|
@@ -75,6 +75,64 @@ export function restorePackageChanges(current, changes, warnings = []) {
|
|
|
75
75
|
return result;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export const CODEMODE_ENTRY = "+codemode";
|
|
79
|
+
// Pi 0.99.0 added codemode and `+name` entries in defaultTools. Older Pi reads "+codemode" as a
|
|
80
|
+
// plain tool name that replaces its defaults, which would leave the session with no built-in tools.
|
|
81
|
+
export const CODEMODE_MIN_PI = [0, 99, 0];
|
|
82
|
+
|
|
83
|
+
export function supportsCodemode(version) {
|
|
84
|
+
if (!Array.isArray(version)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let index = 0; index < CODEMODE_MIN_PI.length; index += 1) {
|
|
89
|
+
if (version[index] !== CODEMODE_MIN_PI[index]) {
|
|
90
|
+
return version[index] > CODEMODE_MIN_PI[index];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Adds "+codemode" to defaultTools unless the user already decided about codemode or built-in tools.
|
|
98
|
+
// Returns what was there before so uninstall can take back only SpecPi's entry.
|
|
99
|
+
export function enableCodemode(settings) {
|
|
100
|
+
if (Array.isArray(settings.extensions) && settings.extensions.includes("-builtin:codemode")) {
|
|
101
|
+
return { skipped: "extension-disabled" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const current = settings.defaultTools;
|
|
105
|
+
if (current !== undefined && !Array.isArray(current)) {
|
|
106
|
+
return { skipped: "invalid" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// An empty list turns every built-in tool off; adding a +name entry would turn the defaults back on.
|
|
110
|
+
if (Array.isArray(current) && current.length === 0) {
|
|
111
|
+
return { skipped: "no-builtin-tools" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if ((current || []).some((entry) => ["codemode", "+codemode", "-codemode"].includes(entry))) {
|
|
115
|
+
return { skipped: "configured" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
settings.defaultTools = [...(current || []), CODEMODE_ENTRY];
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
change: { beforeExists: current !== undefined, ...(current === undefined ? {} : { before: current }) },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The settings-change record restoreSettingChanges understands. The path and entry are fixed here, never
|
|
126
|
+
// read from the manifest, so a stale or edited manifest cannot point a restore at another setting.
|
|
127
|
+
export function codemodeSettingChange(record) {
|
|
128
|
+
return {
|
|
129
|
+
path: ["defaultTools"],
|
|
130
|
+
beforeExists: record?.beforeExists === true,
|
|
131
|
+
...(record?.beforeExists === true && Array.isArray(record.before) ? { before: record.before } : {}),
|
|
132
|
+
managedArrayEntries: [CODEMODE_ENTRY],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
78
136
|
function markerRange(text, start, end) {
|
|
79
137
|
const startIndex = text.indexOf(start);
|
|
80
138
|
const endIndex = text.indexOf(end);
|
package/scripts/packages.mjs
CHANGED
|
@@ -66,7 +66,7 @@ export function packageChanges(before, after) {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
function resolvePiCommand() {
|
|
70
70
|
const requested = process.env.SPECPI_PI || "pi";
|
|
71
71
|
const hasPath = path.isAbsolute(requested) || /[/\\]/.test(requested);
|
|
72
72
|
const suffixes =
|
|
@@ -78,15 +78,74 @@ export function installBasePackages(agentDir) {
|
|
|
78
78
|
: (process.env.PATH || process.env.Path || "")
|
|
79
79
|
.split(path.delimiter)
|
|
80
80
|
.flatMap((directory) => suffixes.map((suffix) => path.resolve(directory, `${requested}${suffix}`)));
|
|
81
|
-
|
|
81
|
+
|
|
82
|
+
return candidates.find((file) => fs.existsSync(file) && fs.statSync(file).isFile());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runPi(command, args, options) {
|
|
86
|
+
if (/\.[cm]?js$/i.test(command)) {
|
|
87
|
+
return spawnSync(process.execPath, [command, ...args], options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
|
|
91
|
+
// Environment expansion keeps paths with spaces and shell metacharacters one quoted argument.
|
|
92
|
+
const env = { ...options.env };
|
|
93
|
+
const line = [command, ...args]
|
|
94
|
+
.map((value, index) => {
|
|
95
|
+
if (/["\r\n]/.test(value)) {
|
|
96
|
+
throw new Error("Unsupported quote or newline in Pi command path");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const key = `SPECPI_PACKAGE_ARG_${index}`;
|
|
100
|
+
env[key] = value;
|
|
101
|
+
|
|
102
|
+
return `"%${key}%"`;
|
|
103
|
+
})
|
|
104
|
+
.join(" ");
|
|
105
|
+
|
|
106
|
+
return spawnSync(line, [], {
|
|
107
|
+
...options,
|
|
108
|
+
env,
|
|
109
|
+
shell: process.env.ComSpec || process.env.COMSPEC || "cmd.exe",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return spawnSync(command, args, options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The version of the Pi CLI SpecPi would install with, or undefined when it cannot be read.
|
|
117
|
+
export function piVersion(agentDir) {
|
|
118
|
+
const command = resolvePiCommand();
|
|
119
|
+
if (!command) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = runPi(command, ["--version"], {
|
|
125
|
+
cwd: agentDir,
|
|
126
|
+
env: { ...process.env, PI_CODING_AGENT_DIR: agentDir },
|
|
127
|
+
encoding: "utf8",
|
|
128
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
129
|
+
timeout: 30_000,
|
|
130
|
+
windowsHide: true,
|
|
131
|
+
});
|
|
132
|
+
const match = result.status === 0 ? /^\s*v?(\d+)\.(\d+)\.(\d+)\b/u.exec(result.stdout || "") : null;
|
|
133
|
+
|
|
134
|
+
return match ? match.slice(1, 4).map(Number) : undefined;
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function installBasePackages(agentDir) {
|
|
141
|
+
const command = resolvePiCommand();
|
|
82
142
|
if (!command) {
|
|
83
143
|
throw new Error("Pi was not found. Install Pi, put pi on PATH, or set SPECPI_PI to its CLI path.");
|
|
84
144
|
}
|
|
85
145
|
|
|
86
146
|
for (const source of basePackages) {
|
|
87
147
|
console.log(`Installing ${source}`);
|
|
88
|
-
const
|
|
89
|
-
const options = {
|
|
148
|
+
const result = runPi(command, ["install", source], {
|
|
90
149
|
cwd: agentDir,
|
|
91
150
|
// Later installs must not re-resolve earlier pins through npm's default caret ranges.
|
|
92
151
|
env: {
|
|
@@ -101,33 +160,7 @@ export function installBasePackages(agentDir) {
|
|
|
101
160
|
},
|
|
102
161
|
stdio: "inherit",
|
|
103
162
|
windowsHide: true,
|
|
104
|
-
};
|
|
105
|
-
let result;
|
|
106
|
-
if (/\.[cm]?js$/i.test(command)) {
|
|
107
|
-
result = spawnSync(process.execPath, [command, ...args], options);
|
|
108
|
-
} else if (process.platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
|
|
109
|
-
// Environment expansion keeps paths with spaces and shell metacharacters one quoted argument.
|
|
110
|
-
const values = [command, ...args];
|
|
111
|
-
const line = values
|
|
112
|
-
.map((value, index) => {
|
|
113
|
-
if (/["\r\n]/.test(value)) {
|
|
114
|
-
throw new Error("Unsupported quote or newline in Pi command path");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const key = `SPECPI_PACKAGE_ARG_${index}`;
|
|
118
|
-
options.env[key] = value;
|
|
119
|
-
|
|
120
|
-
return `"%${key}%"`;
|
|
121
|
-
})
|
|
122
|
-
.join(" ");
|
|
123
|
-
result = spawnSync(line, [], {
|
|
124
|
-
...options,
|
|
125
|
-
shell: process.env.ComSpec || process.env.COMSPEC || "cmd.exe",
|
|
126
|
-
});
|
|
127
|
-
} else {
|
|
128
|
-
result = spawnSync(command, args, options);
|
|
129
|
-
}
|
|
130
|
-
|
|
163
|
+
});
|
|
131
164
|
if (result.error || result.status !== 0) {
|
|
132
165
|
throw new Error(
|
|
133
166
|
`Pi installation failed for ${source}: ${result.error?.message || `exit ${result.status}`}`,
|
package/scripts/specpi.mjs
CHANGED
|
@@ -10,19 +10,29 @@ import {
|
|
|
10
10
|
AGENTS_START,
|
|
11
11
|
SHELL_END,
|
|
12
12
|
SHELL_START,
|
|
13
|
+
codemodeSettingChange,
|
|
13
14
|
deepEqual,
|
|
14
15
|
deletePath,
|
|
16
|
+
enableCodemode,
|
|
15
17
|
readPath,
|
|
16
18
|
removeManagedBlock,
|
|
17
19
|
restorePackageChanges,
|
|
18
20
|
setPath,
|
|
19
21
|
sha256,
|
|
22
|
+
supportsCodemode,
|
|
20
23
|
upsertManagedBlock,
|
|
21
24
|
} from "./lib.mjs";
|
|
22
25
|
import { validateCapabilityRegistry } from "../extensions/tool-wishlist/registry.mjs";
|
|
23
26
|
import { runValidator } from "../extensions/tool-wishlist/validators.mjs";
|
|
24
27
|
import { acquireSpecPiLock } from "./lock.mjs";
|
|
25
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
basePackages,
|
|
30
|
+
checkBasePackages,
|
|
31
|
+
installBasePackages,
|
|
32
|
+
packageChanges,
|
|
33
|
+
piVersion,
|
|
34
|
+
runBrowserQA,
|
|
35
|
+
} from "./packages.mjs";
|
|
26
36
|
import { RETIRED_GUARD_NOTICE, RETIRED_GUARD_PIN, retiredGuardWasOn } from "./lancet-guard.mjs";
|
|
27
37
|
import {
|
|
28
38
|
applyShellToolMapping,
|
|
@@ -84,8 +94,9 @@ Usage:
|
|
|
84
94
|
specpi doctor
|
|
85
95
|
specpi uninstall [--yes]
|
|
86
96
|
|
|
87
|
-
Installs /scope, the harness improvement loop, and eight pinned packages
|
|
88
|
-
The base is tested with Pi 0.
|
|
97
|
+
Installs /scope, the harness improvement loop, and eight pinned packages,
|
|
98
|
+
and turns on Pi's codemode tool. The base is tested with Pi 1.0.0.
|
|
99
|
+
Run specpi plan to see package versions.
|
|
89
100
|
--skip-package-install installs only the core, or preserves an existing base on update.
|
|
90
101
|
--skip-browser-install skips Chromium setup, not package acquisition or doctor checks.
|
|
91
102
|
--force replaces modified retained resources after backing them up.
|
|
@@ -205,6 +216,9 @@ function printPlan(options = {}) {
|
|
|
205
216
|
console.log(
|
|
206
217
|
` ${permissionConfigFile(agentDir)} (one shellTools entry, only when the permission system is installed, so bash rules apply to background jobs)`,
|
|
207
218
|
);
|
|
219
|
+
console.log(
|
|
220
|
+
` ${settingsPath} (adds "+codemode" to defaultTools on Pi 0.99 or later, unless defaultTools already mentions codemode, is empty, or codemode is disabled; uninstall removes only that entry)`,
|
|
221
|
+
);
|
|
208
222
|
const wanted = new Set(managedFiles().map(([, target]) => target));
|
|
209
223
|
for (const target of Object.keys(manifest?.files || {})) {
|
|
210
224
|
if (!wanted.has(target)) {
|
|
@@ -235,7 +249,7 @@ function printPlan(options = {}) {
|
|
|
235
249
|
: "After package acquisition, run the installed Browser QA Node bin to download Chromium and verify offline rendering, pixel comparison, and accessibility. No Bun or OS library installation.",
|
|
236
250
|
);
|
|
237
251
|
console.log(
|
|
238
|
-
"Only package settings are merged. No theme or shell integration. Wishlist collection starts off. Backups precede mutation; downloaded packages, browser-cache bytes and upstream script effects cannot be rolled back or removed by uninstall.",
|
|
252
|
+
"Only package settings and that defaultTools entry are merged. No theme or shell integration. Wishlist collection starts off. Backups precede mutation; downloaded packages, browser-cache bytes and upstream script effects cannot be rolled back or removed by uninstall.",
|
|
239
253
|
);
|
|
240
254
|
}
|
|
241
255
|
|
|
@@ -260,6 +274,44 @@ function restoreLegacySettings(manifest, warnings) {
|
|
|
260
274
|
writeJson(settingsPath, settings, existingMode(settingsPath, 0o600));
|
|
261
275
|
}
|
|
262
276
|
|
|
277
|
+
// Takes back the "+codemode" entry a previous install added, leaving the rest of defaultTools alone.
|
|
278
|
+
function restoreCodemodeSetting(manifest, warnings) {
|
|
279
|
+
if (!manifest?.codemodeSetting) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const settings = readJson(settingsPath, {});
|
|
284
|
+
restoreSettingChanges(settings, [codemodeSettingChange(manifest.codemodeSetting)], warnings);
|
|
285
|
+
writeJson(settingsPath, settings, existingMode(settingsPath, 0o600));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Turns on Pi's codemode tool through defaultTools. Returns the manifest record, or undefined when
|
|
289
|
+
// nothing was added because the user already chose, or Pi is too old to read the entry safely.
|
|
290
|
+
function applyCodemodeSetting(warnings) {
|
|
291
|
+
const version = piVersion(agentDir);
|
|
292
|
+
if (!supportsCodemode(version)) {
|
|
293
|
+
warnings.push(
|
|
294
|
+
`Codemode left off: it needs Pi 0.99 or later, and ${version ? `Pi ${version.join(".")} was found` : "the Pi version could not be read"}. Update Pi, then run specpi update.`,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const settings = readJson(settingsPath, {});
|
|
301
|
+
const result = enableCodemode(settings);
|
|
302
|
+
if (result.skipped === "invalid") {
|
|
303
|
+
warnings.push("Codemode left off: defaultTools in settings.json is not an array; preserved it as it is.");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!result.change) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
writeJson(settingsPath, settings, existingMode(settingsPath, 0o600));
|
|
311
|
+
|
|
312
|
+
return result.change;
|
|
313
|
+
}
|
|
314
|
+
|
|
263
315
|
function removeLegacyShell(manifest) {
|
|
264
316
|
if (manifest?.shellRc && fs.existsSync(manifest.shellRc)) {
|
|
265
317
|
const result = removeManagedBlock(fs.readFileSync(manifest.shellRc, "utf8"), SHELL_START, SHELL_END);
|
|
@@ -317,9 +369,9 @@ async function mutate(options, operation) {
|
|
|
317
369
|
...files.map(([, target]) => target),
|
|
318
370
|
...Object.keys(previous?.files || {}),
|
|
319
371
|
];
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
372
|
+
// Always watched: codemode's defaultTools entry is added on every install and update, and
|
|
373
|
+
// removed on uninstall, whether or not packages are installed.
|
|
374
|
+
watched.push(settingsPath);
|
|
323
375
|
|
|
324
376
|
if (previous?.shellRc) {
|
|
325
377
|
watched.push(previous.shellRc);
|
|
@@ -338,6 +390,7 @@ async function mutate(options, operation) {
|
|
|
338
390
|
const warnings = [];
|
|
339
391
|
const preserveBase = operation !== "uninstall" && options.skipPackages && previous?.basePackages?.length;
|
|
340
392
|
restoreLegacySettings(preserveBase ? { ...previous, packageChanges: [] } : previous, warnings);
|
|
393
|
+
restoreCodemodeSetting(previous, warnings);
|
|
341
394
|
removeLegacyShell(previous);
|
|
342
395
|
let packageState = preserveBase
|
|
343
396
|
? {
|
|
@@ -402,6 +455,8 @@ async function mutate(options, operation) {
|
|
|
402
455
|
removeShellToolMapping(agentDir);
|
|
403
456
|
}
|
|
404
457
|
|
|
458
|
+
const codemodeSetting = operation === "uninstall" ? undefined : applyCodemodeSetting(warnings);
|
|
459
|
+
|
|
405
460
|
injectTestFailure("after-settings");
|
|
406
461
|
const records = {};
|
|
407
462
|
const wanted = new Set(files.map(([, target]) => target));
|
|
@@ -481,6 +536,7 @@ async function mutate(options, operation) {
|
|
|
481
536
|
blockFiles: { agents: previous?.blockFiles?.agents || { existed: transaction.get(agentsPath).exists } },
|
|
482
537
|
files: records,
|
|
483
538
|
...packageState,
|
|
539
|
+
...(codemodeSetting ? { codemodeSetting } : {}),
|
|
484
540
|
backups: [...(previous?.backups || []), path.relative(stateDir, backupDir)],
|
|
485
541
|
});
|
|
486
542
|
}
|
package/templates/settings.json
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"npm:specpi-browser-qa@0.3.0",
|
|
5
5
|
"npm:specpi-delegation@0.2.0",
|
|
6
6
|
"npm:specpi-experiments@0.1.0",
|
|
7
|
-
"npm:pi-goal-x@0.
|
|
7
|
+
"npm:pi-goal-x@0.32.0",
|
|
8
8
|
"npm:@sreetej510/pi-usage@0.10.0",
|
|
9
9
|
"npm:@gotgenes/pi-permission-system@32.0.2",
|
|
10
|
-
"npm:specpi-lancet-guard@0.
|
|
10
|
+
"npm:specpi-lancet-guard@0.6.0"
|
|
11
11
|
]
|
|
12
12
|
}
|