specpi 0.36.4 → 0.37.1
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 +11 -0
- package/NPM_RELEASE.md +1 -1
- package/README.md +10 -9
- package/SECURITY_MODEL.md +3 -1
- 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,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.1 - 2026-10-03
|
|
4
|
+
|
|
5
|
+
- Pin `specpi-lancet-guard` 0.6.1, which fixes `/lancet-guard setup` failing inside the prebuilt Pi binary with "ONNX Runtime could not be loaded on this platform: Cannot find package 'onnxruntime-node'". Pi installed from its release download, for example through mise, is a compiled Bun binary that could not resolve the guard's installed `onnxruntime-node`; Pi installed through npm runs on Node and was unaffected. The model is unchanged, LANCET Nano v0.4.3, so updating needs no new `/lancet-guard setup`. SpecPi Chat stays at 0.19.0.
|
|
6
|
+
|
|
7
|
+
## 0.37.0 - 2026-10-01
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
|
|
3
14
|
## 0.36.4 - 2026-10-01
|
|
4
15
|
|
|
5
16
|
- 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`.
|
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.1**, paired with the Chat **0.19.0** VSIX. Browser QA **0.3.0** and LANCET guard **0.6.1** 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.1 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
|
|
|
@@ -47,14 +47,13 @@ Everything it touches is written down, versioned, and easy to undo.
|
|
|
47
47
|
|
|
48
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 |
|
|
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
|
@@ -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.6.
|
|
14
|
+
Reviewed on 2026-09-16 against published npm metadata and integrity-verified source archives, and `specpi-lancet-guard` 0.6.1, 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.6.
|
|
27
|
+
| specpi-lancet-guard | 0.6.1 | [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.1",
|
|
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.6.
|
|
18
|
+
export const GUARD_PIN = "npm:specpi-lancet-guard@0.6.1";
|
|
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.6.
|
|
10
|
+
"npm:specpi-lancet-guard@0.6.1"
|
|
11
11
|
]
|
|
12
12
|
}
|