specpi 0.36.4 → 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 CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 0.36.4 - 2026-10-01
4
11
 
5
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`.
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.36.4**, paired with the Chat **0.18.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.
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.36.4 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.
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
 
@@ -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
- | | LANCET Nano v0.4.3 |
51
- | --- | ---: |
52
- | Triage Score | 68.3 |
53
- | Safe commands wrongly stopped | 9.0% |
54
- | Cost per check | $0 |
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
- Both figures are taken across 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/).
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.84.4** and TypeBox to **1.3.7**. Host installations retain their upstream licenses and manage provider connections themselves. SpecPi does not install or configure providers.
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.0, which replaces `specpi-jev-guard` 0.4.0, from source in this repository. All eight top-level packages declare the MIT license. Pins are authoritative in `templates/settings.json`.
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,7 +21,7 @@ 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.31.2 | [tmonk/pi-goal-x](https://github.com/tmonk/pi-goal-x) |
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
27
  | specpi-lancet-guard | 0.6.0 | [TannerMidd/SpecPi](https://github.com/TannerMidd/SpecPi/tree/main/packages/lancet-guard) |
@@ -30,7 +30,7 @@ Transitive dependencies and their notices remain in Pi's npm installation tree.
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.84.4. Pi Goal X declares Pi `>=0.83.0 <0.85.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.
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.36.4",
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.84.4",
98
- "@earendil-works/pi-ai": "0.84.4",
99
- "@earendil-works/pi-tui": "0.84.4",
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/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);
@@ -66,7 +66,7 @@ export function packageChanges(before, after) {
66
66
  });
67
67
  }
68
68
 
69
- export function installBasePackages(agentDir) {
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
- const command = candidates.find((file) => fs.existsSync(file) && fs.statSync(file).isFile());
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 args = ["install", source];
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}`}`,
@@ -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 { basePackages, checkBasePackages, installBasePackages, packageChanges, runBrowserQA } from "./packages.mjs";
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.84.4. Run specpi plan to see package versions.
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
- if (!options.skipPackages || previous?.settingsChanges?.length || previous?.packageChanges?.length) {
321
- watched.push(settingsPath);
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
  }
@@ -4,7 +4,7 @@
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.31.2",
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
10
  "npm:specpi-lancet-guard@0.6.0"