specpi 0.12.0 → 0.13.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 +8 -0
- package/README.md +13 -11
- package/SECURITY_MODEL.md +6 -0
- package/THIRD_PARTY.md +4 -2
- package/docs/browser-testing.md +76 -0
- package/docs/delegation/README.md +1 -1
- package/extensions/browser/core.d.mts +64 -0
- package/extensions/browser/diagnostics.ts +275 -0
- package/extensions/browser/index.ts +349 -57
- package/extensions/browser/interactions.ts +118 -0
- package/extensions/browser/lifecycle.ts +28 -0
- package/extensions/tool-wishlist/verification.mjs +1 -0
- package/package.json +15 -4
- package/scripts/check-package.mjs +11 -3
- package/scripts/check-pi-package.mjs +1 -0
- package/scripts/run-browser-tests.mjs +61 -0
- package/scripts/setup-browser-tests.mjs +38 -0
- package/scripts/site-browser.mjs +272 -0
- package/scripts/specpi.mjs +18 -0
- package/templates/AGENTS.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 - 2026-09-05
|
|
4
|
+
|
|
5
|
+
- Add bounded, best-effort sanitized browser exceptions, console errors, failed requests, and HTTP error diagnostics with explicit cursor/loss/clear semantics and ephemeral retention.
|
|
6
|
+
- Add keyboard/chord input, native dropdown selection, and deadline-bounded page-condition waits; invalidate snapshot refs on application-driven navigation and preserve isolated cancellation cleanup.
|
|
7
|
+
- Strictly type-check the browser extension against pinned development Pi, TypeBox, Node, and Playwright declarations without eagerly loading the browser runtime or changing production optional peers.
|
|
8
|
+
- Add real registered-tool Chromium fixtures, repeatable responsive-site checks with fault-injection tests, and a shared CI browser gate required before Pages deployment. Preserve the pinned-Pi no-skips coverage gate.
|
|
9
|
+
- Document privacy/testing contracts and an evidence-backed decision to retain project-native TypeScript semantic navigation rather than add an LSP tool now.
|
|
10
|
+
|
|
3
11
|
## 0.12.0 - 2026-09-05
|
|
4
12
|
|
|
5
13
|
- Add experimental, opt-in delegation for independent reviews and selected-source analysis through native Pi sessions. The parent remains the sole writer; workers have no shell, edits, live web, nested delegation or ambient extensions.
|
package/README.md
CHANGED
|
@@ -24,17 +24,15 @@ SpecPi adds task contracts, workflow controls, and a local improvement loop to P
|
|
|
24
24
|
|
|
25
25
|
Collection is disabled until explicitly enabled. Reports are sanitized, bounded, deduplicated by task, and never uploaded. Later evidence can reopen an item for review, but never restarts implementation automatically.
|
|
26
26
|
|
|
27
|
-
Version `0.
|
|
27
|
+
Version `0.13.0` adds browser diagnostics, keyboard and native dropdown interactions, bounded condition waits, scoped TypeScript checking, and repeatable rendered-site tests. Experimental delegation, task cards, verification receipts and human outcome assessments remain part of the single-agent workflow. See the [release notes](CHANGELOG.md) for the change list.
|
|
28
28
|
|
|
29
29
|
## Optional delegation
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
One agent owns edits and verifies results. Experimental delegation adds up to two read-only Pi workers: `review` checks a frozen artifact; `scout` answers a focused question using selected sources. Workers cannot write, run shell commands, browse the web, or delegate further.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Delegation is **off by default**. In Pi, use `/delegate on` to enable it, `/delegate status` to inspect work, and `/delegate off` to revoke it. Research informed the design; SpecPi quality, speed, and cost gains remain unmeasured.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
See the [delegation guide](docs/delegation/README.md), [tool protocol](docs/delegation/protocol.md), [research](docs/delegation/research.md), and [evaluation plan](docs/delegation/evaluation.md) for setup, examples, boundaries and evidence.
|
|
35
|
+
See [setup and limits](docs/delegation/README.md) or [how the research shaped the architecture](https://tannermidd.github.io/SpecPi/single-agent/).
|
|
38
36
|
|
|
39
37
|
## Install
|
|
40
38
|
|
|
@@ -57,14 +55,14 @@ specpi doctor
|
|
|
57
55
|
Pin the reusable CLI when installing a reviewed release, or inspect its plan without retaining a global CLI installation:
|
|
58
56
|
|
|
59
57
|
```bash
|
|
60
|
-
npm install --global specpi@0.
|
|
61
|
-
npx --package specpi@0.
|
|
58
|
+
npm install --global specpi@0.13.0
|
|
59
|
+
npx --package specpi@0.13.0 specpi plan
|
|
62
60
|
```
|
|
63
61
|
|
|
64
62
|
For a source-audited installation, clone the exact release:
|
|
65
63
|
|
|
66
64
|
```bash
|
|
67
|
-
git clone --branch v0.
|
|
65
|
+
git clone --branch v0.13.0 --depth 1 https://github.com/TannerMidd/SpecPi.git
|
|
68
66
|
cd SpecPi
|
|
69
67
|
./specpi plan
|
|
70
68
|
./specpi install
|
|
@@ -122,7 +120,7 @@ Direct `pi install npm:specpi` loads extensions, skills, and themes only. It doe
|
|
|
122
120
|
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
121
|
| `/spec` | Replace normal chrome with a technical run panel, seal live reasoning, hold streaming prose until complete, and keep tools collapsed. |
|
|
124
122
|
| `/guard` | Deny confirmed host-wide destructive calls and request approval for bounded risk classes. |
|
|
125
|
-
| Browser tools |
|
|
123
|
+
| Browser tools | Inspect isolated Chromium, diagnose errors, exercise keyboard/select/wait flows, and capture screenshots. |
|
|
126
124
|
| `specpi-spec` theme | Bring blueprint blue, technical greys, layered surfaces, and restrained semantic states into Pi. |
|
|
127
125
|
| `specpi` CLI | Plan, install, update, verify, and uninstall managed state with backups and rollback. |
|
|
128
126
|
|
|
@@ -228,6 +226,10 @@ npm run format
|
|
|
228
226
|
npm run check
|
|
229
227
|
```
|
|
230
228
|
|
|
231
|
-
JavaScript and TypeScript use four-space indentation, explicit braced control flow, and one statement per line. The repository check enforces formatting, validates syntax, runs the Node test suite, executes registry-linked validators, and installs the exact npm tarball through an isolated lifecycle. Maintainers should follow [NPM_RELEASE.md](NPM_RELEASE.md) for release preparation and protected publication.
|
|
229
|
+
JavaScript and TypeScript use four-space indentation, explicit braced control flow, and one statement per line. The repository check enforces formatting, validates syntax, strictly type-checks the browser extension, runs the Node test suite, executes registry-linked validators, and installs the exact npm tarball through an isolated lifecycle. Maintainers should follow [NPM_RELEASE.md](NPM_RELEASE.md) for release preparation and protected publication.
|
|
230
|
+
|
|
231
|
+
For application testing, use `browser_diagnostics` alongside rendered inspection, and `browser_press`, `browser_select_option`, and `browser_wait_for` for keyboard, native dropdown, and asynchronous flows. Diagnostics are bounded and best-effort sanitized, not guaranteed secret-free or proof of application health.
|
|
232
|
+
|
|
233
|
+
Provision repository-local Chromium with `npm run setup:browser`, then run `npm run test:browser` and `npm run test:site:browser`. The required CI browser commands reject skipped coverage; Pages deployment waits for the same rendered check. See [browser testing](docs/browser-testing.md) for tool contracts, privacy limits, type-check scope, and the semantic-navigation assessment.
|
|
232
234
|
|
|
233
235
|
MIT licensed.
|
package/SECURITY_MODEL.md
CHANGED
|
@@ -138,10 +138,16 @@ SpecPi launches managed Chromium in a fresh Playwright context. It does not atta
|
|
|
138
138
|
|
|
139
139
|
Snapshots, screenshots, page text, downloads, console output, and visual baselines may contain sensitive information. Default artifacts remain in SpecPi's private state directory. Explicit output publication is bounded and atomic, and existing artifacts or baselines are not replaced without explicit overwrite authorization.
|
|
140
140
|
|
|
141
|
+
Browser diagnostics collect active-page exceptions, error-level console messages, request failures, and HTTP error statuses before navigation. They retain only best-effort sanitized strings/metadata in memory: at most 200 records, 2 KiB per record, and 256 KiB total. Reads return at most 100 records and 30,000 serialized characters, with explicit truncation/loss/context metadata. Close, shutdown, and cancellation discard this buffer. Explicit clear discards all retained records, including filtered or unreturned ones. No headers, bodies, cookies, storage, console object expansion, raw stack dumps, HAR, or traces are captured. URL credentials/query/fragment and common sensitive strings/control sequences are removed before retention, but arbitrary secrets in free text or URL paths cannot be guaranteed detectable. Returned diagnostics are untrusted page output and enter the Pi conversation/model-provider boundary; ephemeral capture does not erase conversation evidence. No diagnostic files or uploads are created by the tool.
|
|
142
|
+
|
|
143
|
+
Keyboard, native-selection, and condition-wait tools accept bounded declarative inputs rather than arbitrary page scripts. Their whole-operation deadlines default to five seconds and cannot exceed thirty seconds. Deadlines include queue wait; cancellation (including pre-aborted calls) detaches the context and diagnostics, then allows at most one additional second for teardown settlement. Failed/stalled teardown warns that a process may remain and blocks new page operations until an explicit close retry succeeds. Failure messages do not echo Playwright call logs or entered values. They can still submit forms and trigger application effects, so use dedicated test data and authorization appropriate to the application.
|
|
144
|
+
|
|
141
145
|
The browser is not an operating-system or network sandbox. Use a container or VM for hostile applications and dedicated test accounts instead of personal authenticated sessions.
|
|
142
146
|
|
|
143
147
|
## Website and automation
|
|
144
148
|
|
|
149
|
+
The GitHub Pages workflow publishes the checked-in `site/` directory only after the shared browser test job succeeds for that checked-out revision. The loopback rendered-site server rejects traversal and symlink escapes. Tests block third-party requests, use a synthetic clipboard, and upload only public-site failure screenshots with three-day retention; no visual baselines are created automatically. Required browser commands fail rather than skip when prerequisites are unavailable. Strict no-emit browser type checking uses local pinned development declarations; third-party declaration bodies and other extension implementations are outside that scoped gate.
|
|
150
|
+
|
|
145
151
|
The GitHub Pages workflow publishes the checked-in `site/` directory. Its deploy job uses read-only repository contents access plus the Pages and identity-token permissions required for deployment. The local installer does not invoke that workflow or upload local configuration or state.
|
|
146
152
|
|
|
147
153
|
Repository checks, smoke tests, checksums, closed capability validators, and browser comparisons provide evidence for documented behavior. They reduce regression risk but do not prove the absence of vulnerabilities or establish cryptographic provenance for dependencies and releases.
|
package/THIRD_PARTY.md
CHANGED
|
@@ -33,7 +33,7 @@ The published `specpi` npm package declares the Pi host runtime modules its exte
|
|
|
33
33
|
- `@earendil-works/pi-tui` — terminal component, key, and width primitives
|
|
34
34
|
- `typebox` — the unscoped TypeBox package Pi bundles, used for tool input schemas
|
|
35
35
|
|
|
36
|
-
They retain their own copyright and license terms. SpecPi never vendors, bundles, or installs them; the Pi host supplies them at extension load time through loader aliases. Pi disables peer resolution for managed package installs, so a peer range would not enforce the host version there. Marking the peers optional also keeps an ordinary npm CLI installation from adding a second copy beside or inside SpecPi. The full managed installation enforces its supported Pi floor through the installer's `MIN_PI_VERSION` compatibility check, and the limited direct Pi mode documents the same host prerequisite.
|
|
36
|
+
They retain their own copyright and license terms. SpecPi never vendors, bundles, or installs them for end users; the Pi host supplies them at extension load time through loader aliases. Repository development additionally pins local copies for type checking and isolated test launches, as listed below. Pi disables peer resolution for managed package installs, so a peer range would not enforce the host version there. Marking the peers optional also keeps an ordinary npm CLI installation from adding a second copy beside or inside SpecPi. The full managed installation enforces its supported Pi floor through the installer's `MIN_PI_VERSION` compatibility check, and the limited direct Pi mode documents the same host prerequisite.
|
|
37
37
|
|
|
38
38
|
SpecPi also installs these exact browser-runtime packages from the reviewed `browser-runtime/package-lock.json`:
|
|
39
39
|
|
|
@@ -52,7 +52,9 @@ Repository development uses these exact, project-local formatting and linting pa
|
|
|
52
52
|
- `@typescript-eslint/parser@8.68.0` — MIT
|
|
53
53
|
- `typescript@6.0.3` — Apache-2.0
|
|
54
54
|
|
|
55
|
-
They are development-only dependencies, are not shipped by the SpecPi installer, and enforce the repository's JavaScript and TypeScript readability rules.
|
|
55
|
+
They are development-only dependencies, are not shipped by the SpecPi installer, and enforce the repository's JavaScript and TypeScript readability rules. TypeScript also runs strict no-emit checking for the browser extension and a bounded semantic-navigation fixture.
|
|
56
|
+
|
|
57
|
+
Browser type checking and registered-tool tests additionally use exact project-local development dependencies: `@earendil-works/pi-coding-agent@0.84.4`, `@earendil-works/pi-ai@0.84.4`, `@earendil-works/pi-tui@0.84.4` (MIT), `typebox@1.3.7` (MIT), `@types/node@22.20.1` (MIT), and `playwright@1.62.1` (Apache-2.0). These reuse the reviewed Pi/runtime versions, do not alter the optional production-peer contract, and are not bundled or installed by SpecPi. Direct development dependencies are pinned; this is not a claim that the development transitive graph is locked. The browser executable test runtime still uses the separately reviewed lockfile. `setup:browser` provisions only `.specpi-test/browser-runtime/`; its explicit `--with-deps` option invokes Playwright OS dependency setup on disposable Linux CI runners. No language-server executable or additional automation framework was added.
|
|
56
58
|
|
|
57
59
|
The GitHub Pages site vendors the Latin subsets of IBM Plex Sans and IBM Plex Mono. Copyright © 2017 IBM Corp. with Reserved Font Name "Plex". The font files are distributed under the SIL Open Font License 1.1; the required license text is included at `site/fonts/LICENSE.txt`.
|
|
58
60
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Browser application testing
|
|
2
|
+
|
|
3
|
+
## Agent workflow
|
|
4
|
+
|
|
5
|
+
1. `browser_open` opens an HTTP(S) page in isolated Chromium; `browser_snapshot` exposes bounded rendered text and namespaced control references.
|
|
6
|
+
2. Use click/fill and the tools below to exercise the application. Refresh snapshots after mutations or navigation (including application-initiated navigation). Targets use CSS, exact `text=`, or current snapshot refs; the first matching element is used.
|
|
7
|
+
3. Inspect `browser_diagnostics` after navigation and interactions, wait for explicit expected states, and capture screenshots at relevant viewports. No errors alone does not establish correct behavior.
|
|
8
|
+
4. `browser_close` discards the context and diagnostic buffer. No personal browser profile is attached.
|
|
9
|
+
|
|
10
|
+
| Tool | Examples and limits |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `browser_press` | `{ "key": "Tab" }`, `{ "target": "#search", "key": "Enter" }`, `{ "key": "Shift+Tab" }`. A single key/chord on the target or current focus, not a script or macro. |
|
|
13
|
+
| `browser_select_option` | `{ "target": "#region", "options": [{ "label": "Europe" }] }`. Each of up to 50 options specifies exactly one value, label, or index. Multiple options require a native multiple select. Custom dropdowns use click/keyboard tools. |
|
|
14
|
+
| `browser_wait_for` | `{ "condition": "text", "target": "#status", "text": "Saved" }`, `{ "condition": "hidden", "target": "#spinner" }`, or `{ "condition": "url", "url": "http://localhost:3000/done" }`. Element conditions: attached, detached, visible, hidden, or exact text. Text is treated literally, not as a regular expression. URL matching is exact after HTTP(S) normalization, not glob matching. |
|
|
15
|
+
| `browser_diagnostics` | `{ "maxEntries": 50, "maxChars": 12000 }`. Optional category: pageerror, console, requestfailed, or http; cursor for incremental reads; explicit clear. Does not launch a browser just to read an empty buffer. |
|
|
16
|
+
|
|
17
|
+
Press, selection, and waits have an operation `timeoutMs` of 5,000 by default, bounded to 1–30,000, starting at admission (including queued time). Timeouts throw instead of returning success. Cancellation, including already-aborted calls, discards diagnostics and initiates browser close; teardown settlement has an additional one-second bound. A rejected/stalled close explicitly warns that a process may remain and blocks new page operations until an explicit `browser_close` retry succeeds. After normal cleanup the next browser operation can create a fresh context. New interaction failure messages omit Playwright call logs because those logs may echo entered values or selectors. There is no arbitrary page evaluation, automatic submission retry, or sleep-only tool. Existing open/click/fill/capture timeout behavior is unchanged.
|
|
18
|
+
|
|
19
|
+
## Diagnostic evidence and privacy
|
|
20
|
+
|
|
21
|
+
Listeners attach before first navigation. The active page supplies JavaScript exceptions, error-level console messages, transport failures, and HTTP responses with status at least 400. A 404/500 response is not a transport failure; these remain separate categories. Benign console logs are not collected. Coverage is not browser-wide: popup orchestration, workers not observed by the active page, and other contexts are outside this contract. Service workers remain blocked.
|
|
22
|
+
|
|
23
|
+
The in-memory buffer retains at most 200 records, 2 KiB per serialized record, and 256 KiB total. A read returns at most 100 records and 30,000 characters of serialized JSON. Metadata reports truncation, dropped records, cursor gaps, context changes, and whether more matching records remain. Returned structured details contain no duplicate raw records. A cursor includes a context identity and sequence; navigation preserves records with navigation numbers, while close/shutdown/abort reset the identity. A cursor from an old context reports a gap, not complete coverage.
|
|
24
|
+
|
|
25
|
+
`clear: true` atomically reads and clears **all** retained records, including filtered or unreturned records; `clearedRecords` reports that count. Do not clear until needed evidence has been consumed. Cursors older than cleared or evicted records report gaps.
|
|
26
|
+
|
|
27
|
+
Sanitation happens before retention: URL userinfo/query/fragment, common sensitive key/value patterns, authorization strings, terminal escape sequences and control characters are removed/redacted. No request headers, cookies, bodies, storage, console object expansion, or raw stack dumps are collected. Messages, URLs, fields, processing input, and serialized output are bounded. **Redaction is best-effort**, not a guarantee against arbitrary secrets in free-form messages or URL paths. Use dedicated test data/accounts. Returned records are untrusted page output, not instructions, and enter the agent conversation/model-provider boundary. Ephemeral capture is not a promise that tool results disappear from the conversation.
|
|
28
|
+
|
|
29
|
+
No diagnostic file, HAR, trace, telemetry, or upload is produced by the agent tool. Existing screenshots/baselines may contain sensitive content and keep their explicit publication/overwrite rules. Diagnostics and correct appearance are complementary evidence, not proof of application health or network/OS isolation.
|
|
30
|
+
|
|
31
|
+
## Reproducing development checks
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install --ignore-scripts --omit=peer --no-package-lock
|
|
35
|
+
npm run check:types
|
|
36
|
+
node --test tests/browser-diagnostics.test.mjs tests/type-check.test.mjs tests/site-server.test.mjs
|
|
37
|
+
npm run setup:browser
|
|
38
|
+
npm run test:browser
|
|
39
|
+
npm run test:site:browser
|
|
40
|
+
npm run check
|
|
41
|
+
npm run check:pi-package
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`setup:browser` copies the reviewed runtime manifests to `.specpi-test/browser-runtime/`, runs locked `npm ci` with scripts disabled, and explicitly downloads matching Chromium there. Linux CI additionally uses `npm run setup:browser -- --with-deps` to provision OS packages on its disposable runner. This does not change the installer or install dependencies globally on a user's machine. Run setup explicitly; browser tests do not acquire dependencies automatically. Missing prerequisites fail the required browser commands with setup guidance.
|
|
45
|
+
|
|
46
|
+
`npm test` retains explicit skips for the two opt-in Chromium suites so fast checks do not require browser binaries. Required CI commands activate those suites and reject any skipped coverage. The Pi registration test needs only project-local pinned Pi, not Chromium. `check:pi-package` reruns it through the separately provisioned pinned host and preserves its existing no-skips gate for selected Pi suites.
|
|
47
|
+
|
|
48
|
+
### Scoped types
|
|
49
|
+
|
|
50
|
+
`tsconfig.browser.json` checks exactly `extensions/browser/index.ts`, `diagnostics.ts`, `interactions.ts`, `lifecycle.ts`, and the `core.d.mts` boundary, with strict checking and no emit. It uses real pinned Pi, TypeBox, Playwright and Node declaration packages from development dependencies. Browser runtime imports remain lazy; Playwright imports in the extension are type-only. The `.mjs` image/runtime helper has narrow declarations backed by existing helper/runtime tests and an export-inventory regression; its JavaScript implementation is **not** fully type-checked. `skipLibCheck` skips third-party declarations, not first-party browser implementation errors. Other extensions remain syntax-checked, not advertised as type-checked. A negative fixture proves invalid key and Playwright API argument types fail without generated JavaScript.
|
|
51
|
+
|
|
52
|
+
### Rendered site
|
|
53
|
+
|
|
54
|
+
The committed loopback server serves only `site/` under `/SpecPi/`, on an ephemeral port, rejects traversal/symlink escapes, and is closed with the browser after tests. To inspect manually, run `node scripts/site-browser.mjs` and use the printed local URL; Ctrl+C closes the server.
|
|
55
|
+
|
|
56
|
+
The rendered matrix covers the home, wiki, and architecture pages at 1440×900, 834×1112, and 390×844. It tests local navigation, keyboard skip links, guard/cycle tabs and their ARIA states, disclosures, copy success/failure with a synthetic clipboard, loaded images/fonts, horizontal overflow, and unexpected runtime/network errors. Remote requests are rejected. Controlled page-local fault injection demonstrates that runtime exceptions, broken interactions, and overflow fail the same assertions without committing broken site content.
|
|
57
|
+
|
|
58
|
+
The shared browser workflow runs for CI and is a prerequisite of Pages deployment for the same revision. It uploads only public-site failure screenshots, retained for three days, from `.specpi-test/browser-artifacts/`. Tests never automatically create or replace visual baselines. A green DOM/interaction check is not a pixel-regression proof; screenshots still require visual review.
|
|
59
|
+
|
|
60
|
+
### Capability-registry boundary
|
|
61
|
+
|
|
62
|
+
The existing `local-browser-automation` registry entry and `browser-runtime-smoke` prove their historical rendering/image-comparison contract only. They are not expanded into claims that diagnostics or keyboard behavior have passed that closed validator. Browser diagnostics and interactions in SpecPi 0.13.0 are evidenced by dedicated registered-tool/Chromium tests and CI; no wishlist item is automatically selected or retired and no invented shipped version is entered in the registry.
|
|
63
|
+
|
|
64
|
+
## Semantic navigation assessment (R6)
|
|
65
|
+
|
|
66
|
+
**Decision: no new agent-facing semantic tool in this delivery.** Keep project-native compiler/tooling as the default and reassess after concrete larger-refactor friction.
|
|
67
|
+
|
|
68
|
+
Run `node --test tests/semantic-navigation.test.mjs`. The existing pinned TypeScript language service resolves an aliased cross-file definition, finds four related symbol references while excluding a shadowed name, and reports diagnostic 2345 after an on-disk argument-type mutation. The fixture creates temporary source files, supplies an explicit language-service host, and disposes it afterward. No language server, plugin, project configuration script, or new executable dependency is loaded. Windows path normalization was necessary when comparing compiler-returned reference paths; the test retains that check.
|
|
69
|
+
|
|
70
|
+
| Approach | Assessment |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| Project-native compiler and shell tools | Adequate baseline for this TypeScript fixture and the browser refactor. Text search is easy but cannot distinguish aliases/shadowing; compiler APIs can, at the cost of writing a small explicit host. No measured productivity improvement is claimed. |
|
|
73
|
+
| Narrow TypeScript adapter | Could expose bounded path/line/column results and reuse this compiler if repeated refactor work justifies a maintained tool contract. Not justified by this small fixture alone. |
|
|
74
|
+
| Broad LSP integration | Adds server acquisition/trust, process cleanup, language-specific configuration, and protocol complexity without evidence of a current need. Deferred, not implemented. |
|
|
75
|
+
|
|
76
|
+
Any later adapter proposal must define supported languages/projects, on-disk versus unsaved buffers, canonical project-root and symlink boundaries, out-of-root declaration references, generated/vendor exclusions, result/time limits, cancellation/subprocess cleanup, and a no-auto-edit/no-auto-install policy. Definitions in dependencies may need an explicit read-only opt-in. Repository plugins/config scripts are executable trust boundaries, not automatically safe navigation inputs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Bounded delegation
|
|
2
2
|
|
|
3
|
-
Status: experimental in SpecPi 0.
|
|
3
|
+
Status: experimental in SpecPi 0.13.0. Disabled by default.
|
|
4
4
|
The package remains `specpi`; no separate npm package or background service is required.
|
|
5
5
|
|
|
6
6
|
SpecPi keeps one agent responsible for changes and acceptance. This extension adds
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type * as Playwright from "playwright";
|
|
2
|
+
|
|
3
|
+
export type Viewport = { width: number; height: number };
|
|
4
|
+
export type ViewportInput = { preset?: "desktop" | "tablet" | "mobile"; width?: number; height?: number };
|
|
5
|
+
export type PngImage = Viewport & { data: Buffer };
|
|
6
|
+
export type BrowserRuntime = {
|
|
7
|
+
playwright: typeof Playwright;
|
|
8
|
+
PNG: {
|
|
9
|
+
new (size: Viewport): PngImage;
|
|
10
|
+
sync: { read(data: Buffer): PngImage; write(image: PngImage): Buffer };
|
|
11
|
+
};
|
|
12
|
+
pixelmatch: (
|
|
13
|
+
a: Buffer,
|
|
14
|
+
b: Buffer,
|
|
15
|
+
output: Buffer,
|
|
16
|
+
width: number,
|
|
17
|
+
height: number,
|
|
18
|
+
options: { threshold: number },
|
|
19
|
+
) => number;
|
|
20
|
+
};
|
|
21
|
+
export declare const MAX_CAPTURE_DIMENSION: number;
|
|
22
|
+
export declare const MAX_CAPTURE_PIXELS: number;
|
|
23
|
+
export declare const MAX_INLINE_IMAGE_BYTES: number;
|
|
24
|
+
export declare const MAX_PNG_BYTES: number;
|
|
25
|
+
export declare const VIEWPORT_PRESETS: Readonly<Record<"desktop" | "tablet" | "mobile", Viewport>>;
|
|
26
|
+
export declare const DEFAULT_DIFF_THRESHOLD: number;
|
|
27
|
+
export declare const DEFAULT_MAX_DIFF_PIXEL_RATIO: number;
|
|
28
|
+
export declare const MAX_VIEWPORT_PIXELS: number;
|
|
29
|
+
export declare function assertDistinctPaths(entries: Array<[string, string]>): void;
|
|
30
|
+
export declare function assertPngResourceBounds(data: Buffer, label?: string): Viewport;
|
|
31
|
+
export declare function readPngDimensions(data: Buffer, label?: string): Viewport;
|
|
32
|
+
export declare function comparePngBuffers(
|
|
33
|
+
a: Buffer,
|
|
34
|
+
b: Buffer,
|
|
35
|
+
runtime: Pick<BrowserRuntime, "PNG" | "pixelmatch">,
|
|
36
|
+
options?: { threshold?: number; maxDiffPixelRatio?: number },
|
|
37
|
+
): {
|
|
38
|
+
pass: boolean;
|
|
39
|
+
dimensionsMatch: boolean;
|
|
40
|
+
baseline: Viewport;
|
|
41
|
+
current: Viewport;
|
|
42
|
+
diffPixels: number;
|
|
43
|
+
diffPixelRatio: number;
|
|
44
|
+
diffBuffer: Buffer;
|
|
45
|
+
threshold: number;
|
|
46
|
+
maxDiffPixelRatio: number;
|
|
47
|
+
};
|
|
48
|
+
export declare function getAgentDir(extensionUrl: string): string;
|
|
49
|
+
export declare function loadBrowserRuntime(runtimeDir: string): Promise<BrowserRuntime>;
|
|
50
|
+
export declare function makeArtifactPath(
|
|
51
|
+
agentDir: string,
|
|
52
|
+
sessionId: string | undefined,
|
|
53
|
+
kind: string,
|
|
54
|
+
extension?: string,
|
|
55
|
+
): string;
|
|
56
|
+
export declare function sanitizeArtifactSegment(value: string): string;
|
|
57
|
+
export declare function normalizeBrowserUrl(value: string): string;
|
|
58
|
+
export declare function publishBuffer(
|
|
59
|
+
file: string,
|
|
60
|
+
data: Buffer,
|
|
61
|
+
options?: { overwrite?: boolean; signal?: AbortSignal },
|
|
62
|
+
): Promise<void>;
|
|
63
|
+
export declare function resolveUserPath(cwd: string, value: string, label?: string): string;
|
|
64
|
+
export declare function resolveViewport(input?: ViewportInput): Viewport;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { ConsoleMessage, Page, Request, Response } from "playwright";
|
|
3
|
+
|
|
4
|
+
export const DIAGNOSTIC_CATEGORIES = ["pageerror", "console", "requestfailed", "http"] as const;
|
|
5
|
+
export type DiagnosticCategory = (typeof DIAGNOSTIC_CATEGORIES)[number];
|
|
6
|
+
export const MAX_DIAGNOSTIC_RECORDS = 200;
|
|
7
|
+
export const MAX_DIAGNOSTIC_BYTES = 256 * 1024;
|
|
8
|
+
export const MAX_RECORD_BYTES = 2048;
|
|
9
|
+
|
|
10
|
+
export function boundedInteger(value: number | undefined, fallback: number, maximum: number, minimum = 1): number {
|
|
11
|
+
const result = value ?? fallback;
|
|
12
|
+
if (!Number.isInteger(result) || result < minimum || result > maximum) {
|
|
13
|
+
throw new Error(`Expected an integer from ${minimum} to ${maximum}.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function prefix(value: string, limit: number): string {
|
|
20
|
+
return value.slice(0, limit).toWellFormed();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function diagnosticUrl(value: string) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(value.slice(0, 16384));
|
|
26
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
27
|
+
return { text: "[non-http location]", truncated: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
url.username = "";
|
|
31
|
+
url.password = "";
|
|
32
|
+
url.search = "";
|
|
33
|
+
url.hash = "";
|
|
34
|
+
|
|
35
|
+
return { text: prefix(url.href, 300), truncated: value.length > 16384 || url.href.length > 300 };
|
|
36
|
+
} catch {
|
|
37
|
+
return { text: "[location omitted]", truncated: value.length > 16384 };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sanitizeUrl(value: string): string {
|
|
42
|
+
return diagnosticUrl(value).text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Best effort only. Application messages and URL paths may contain arbitrary secrets. */
|
|
46
|
+
function diagnosticText(value: string, limit = 700) {
|
|
47
|
+
// Bound processing as well as retention. Redact before taking the final display prefix.
|
|
48
|
+
let locationTruncated = false;
|
|
49
|
+
const sanitized = value
|
|
50
|
+
.slice(0, 16384)
|
|
51
|
+
.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\|$)/gu, " ")
|
|
52
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, " ")
|
|
53
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/gu, " ")
|
|
54
|
+
.replace(/https?:\/\/[^\s<>"']+/giu, (url) => {
|
|
55
|
+
const location = diagnosticUrl(url);
|
|
56
|
+
locationTruncated ||= location.truncated;
|
|
57
|
+
|
|
58
|
+
return location.text;
|
|
59
|
+
})
|
|
60
|
+
.replace(/\b(?:bearer|basic)\s+[^\s,;]+/giu, "[authorization redacted]")
|
|
61
|
+
.replace(
|
|
62
|
+
/(["']?(?:password|passwd|secret|token|api[_-]?key|authorization|cookie|credential|access[_-]?key)["']?\s*[:=]\s*)(?:"[^"]*(?:"|$)|'[^']*(?:'|$)|[^\s,;]+)/giu,
|
|
63
|
+
"$1[redacted]",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
text: prefix(sanitized, limit),
|
|
68
|
+
truncated: value.length > 16384 || sanitized.length > limit || locationTruncated,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function sanitizeDiagnostic(value: string, limit = 700): string {
|
|
73
|
+
return diagnosticText(value, limit).text;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type DiagnosticRecord = {
|
|
77
|
+
sequence: number;
|
|
78
|
+
navigation: number;
|
|
79
|
+
category: DiagnosticCategory;
|
|
80
|
+
message: string;
|
|
81
|
+
url: string;
|
|
82
|
+
method?: string;
|
|
83
|
+
status?: number;
|
|
84
|
+
resourceType?: string;
|
|
85
|
+
truncated: boolean;
|
|
86
|
+
};
|
|
87
|
+
export type DiagnosticQuery = {
|
|
88
|
+
maxEntries?: number;
|
|
89
|
+
maxChars?: number;
|
|
90
|
+
category?: DiagnosticCategory;
|
|
91
|
+
cursor?: string;
|
|
92
|
+
clear?: boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export class BrowserDiagnostics {
|
|
96
|
+
private context = crypto.randomUUID();
|
|
97
|
+
private sequence = 0;
|
|
98
|
+
private navigation = 0;
|
|
99
|
+
private floor = 0;
|
|
100
|
+
private dropped = 0;
|
|
101
|
+
private bytes = 0;
|
|
102
|
+
private records: DiagnosticRecord[] = [];
|
|
103
|
+
|
|
104
|
+
reset(): void {
|
|
105
|
+
this.context = crypto.randomUUID();
|
|
106
|
+
this.sequence = 0;
|
|
107
|
+
this.navigation = 0;
|
|
108
|
+
this.floor = 0;
|
|
109
|
+
this.dropped = 0;
|
|
110
|
+
this.bytes = 0;
|
|
111
|
+
this.records = [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
navigated(): void {
|
|
115
|
+
this.navigation += 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
record(
|
|
119
|
+
category: DiagnosticCategory,
|
|
120
|
+
message: string,
|
|
121
|
+
url = "",
|
|
122
|
+
extra: { method?: string; status?: number; resourceType?: string } = {},
|
|
123
|
+
): void {
|
|
124
|
+
const clean = diagnosticText(message);
|
|
125
|
+
const location = url ? diagnosticUrl(url) : undefined;
|
|
126
|
+
const method = extra.method ? diagnosticText(extra.method, 20) : undefined;
|
|
127
|
+
const resourceType = extra.resourceType ? diagnosticText(extra.resourceType, 30) : undefined;
|
|
128
|
+
const entry: DiagnosticRecord = {
|
|
129
|
+
sequence: ++this.sequence,
|
|
130
|
+
navigation: this.navigation,
|
|
131
|
+
category,
|
|
132
|
+
message: clean.text,
|
|
133
|
+
url: location?.text ?? "",
|
|
134
|
+
method: method?.text,
|
|
135
|
+
status: extra.status,
|
|
136
|
+
resourceType: resourceType?.text,
|
|
137
|
+
truncated:
|
|
138
|
+
clean.truncated ||
|
|
139
|
+
location?.truncated === true ||
|
|
140
|
+
method?.truncated === true ||
|
|
141
|
+
resourceType?.truncated === true,
|
|
142
|
+
};
|
|
143
|
+
while (Buffer.byteLength(JSON.stringify(entry)) > MAX_RECORD_BYTES) {
|
|
144
|
+
entry.truncated = true;
|
|
145
|
+
if (entry.message.length) {
|
|
146
|
+
entry.message = prefix(entry.message, Math.floor(entry.message.length / 2));
|
|
147
|
+
} else {
|
|
148
|
+
entry.url = prefix(entry.url, Math.floor(entry.url.length / 2));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.records.push(entry);
|
|
153
|
+
this.bytes += Buffer.byteLength(JSON.stringify(entry));
|
|
154
|
+
while (this.records.length > MAX_DIAGNOSTIC_RECORDS || this.bytes > MAX_DIAGNOSTIC_BYTES) {
|
|
155
|
+
const removed = this.records.shift()!;
|
|
156
|
+
this.bytes -= Buffer.byteLength(JSON.stringify(removed));
|
|
157
|
+
this.floor = removed.sequence;
|
|
158
|
+
this.dropped += 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
read(query: DiagnosticQuery = {}) {
|
|
163
|
+
const maxEntries = boundedInteger(query.maxEntries, 50, 100);
|
|
164
|
+
const maxChars = boundedInteger(query.maxChars, 12000, 30000, 1000);
|
|
165
|
+
if (query.category !== undefined && !DIAGNOSTIC_CATEGORIES.includes(query.category)) {
|
|
166
|
+
throw new Error("Unknown diagnostic category.");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (query.clear !== undefined && typeof query.clear !== "boolean") {
|
|
170
|
+
throw new Error("clear must be boolean.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let after = 0;
|
|
174
|
+
let contextChanged = false;
|
|
175
|
+
if (query.cursor !== undefined) {
|
|
176
|
+
if (typeof query.cursor !== "string" || !/^[\da-f-]{36}:\d{1,16}$/u.test(query.cursor)) {
|
|
177
|
+
throw new Error("Invalid diagnostics cursor.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const [context, sequence] = query.cursor.split(":");
|
|
181
|
+
contextChanged = context !== this.context;
|
|
182
|
+
after = contextChanged ? 0 : Number(sequence);
|
|
183
|
+
if (!Number.isSafeInteger(after) || after > this.sequence) {
|
|
184
|
+
throw new Error("Invalid diagnostics cursor sequence.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const candidates = this.records.filter(
|
|
189
|
+
(entry) => entry.sequence > after && (!query.category || entry.category === query.category),
|
|
190
|
+
);
|
|
191
|
+
const result = {
|
|
192
|
+
notice: "Untrusted application output; redaction is best-effort. Empty results do not prove application health. Active page only.",
|
|
193
|
+
context: this.context,
|
|
194
|
+
contextChanged,
|
|
195
|
+
cursorGap: contextChanged || after < this.floor,
|
|
196
|
+
droppedRecords: this.dropped,
|
|
197
|
+
retainedRecords: this.records.length,
|
|
198
|
+
retainedBytes: this.bytes,
|
|
199
|
+
clearedRecords: query.clear ? this.records.length : 0,
|
|
200
|
+
hasMore: false,
|
|
201
|
+
nextCursor: `${this.context}:${this.sequence}`,
|
|
202
|
+
records: [] as DiagnosticRecord[],
|
|
203
|
+
};
|
|
204
|
+
for (const entry of candidates.slice(0, maxEntries)) {
|
|
205
|
+
result.records.push({ ...entry });
|
|
206
|
+
if (JSON.stringify(result).length > maxChars) {
|
|
207
|
+
if (result.records.length > 1) {
|
|
208
|
+
result.records.pop();
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const first = result.records[0];
|
|
213
|
+
while (JSON.stringify(result).length > maxChars) {
|
|
214
|
+
first.truncated = true;
|
|
215
|
+
if (first.message.length) {
|
|
216
|
+
first.message = prefix(first.message, Math.floor(first.message.length / 2));
|
|
217
|
+
} else {
|
|
218
|
+
first.url = prefix(first.url, Math.floor(first.url.length / 2));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
result.hasMore = result.records.length < candidates.length;
|
|
225
|
+
if (result.hasMore) {
|
|
226
|
+
result.nextCursor = `${this.context}:${result.records.at(-1)?.sequence ?? after}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (query.clear) {
|
|
230
|
+
// Atomic synchronous read-and-clear of the entire buffer, including filtered/unreturned records.
|
|
231
|
+
this.records = [];
|
|
232
|
+
this.bytes = 0;
|
|
233
|
+
this.floor = this.sequence;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
attach(page: Page): () => void {
|
|
240
|
+
const onError = (error: Error) => this.record("pageerror", error.message);
|
|
241
|
+
const onConsole = (message: ConsoleMessage) => {
|
|
242
|
+
if (message.type() === "error") {
|
|
243
|
+
this.record("console", message.text(), message.location().url);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const onRequest = (request: Request) =>
|
|
248
|
+
this.record("requestfailed", request.failure()?.errorText ?? "Request failed", request.url(), {
|
|
249
|
+
method: request.method(),
|
|
250
|
+
resourceType: request.resourceType(),
|
|
251
|
+
});
|
|
252
|
+
const onResponse = (response: Response) => {
|
|
253
|
+
if (response.status() >= 400) {
|
|
254
|
+
const request = response.request();
|
|
255
|
+
this.record("http", `HTTP ${response.status()}`, response.url(), {
|
|
256
|
+
status: response.status(),
|
|
257
|
+
method: request.method(),
|
|
258
|
+
resourceType: request.resourceType(),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
page.on("pageerror", onError);
|
|
264
|
+
page.on("console", onConsole);
|
|
265
|
+
page.on("requestfailed", onRequest);
|
|
266
|
+
page.on("response", onResponse);
|
|
267
|
+
|
|
268
|
+
return () => {
|
|
269
|
+
page.off("pageerror", onError);
|
|
270
|
+
page.off("console", onConsole);
|
|
271
|
+
page.off("requestfailed", onRequest);
|
|
272
|
+
page.off("response", onResponse);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|