@lincoln504/pi-research 1.1.2 → 1.3.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/README.md +3 -3
- package/dist/cli.mjs +2599 -1268
- package/dist/thread-worker.mjs +224 -147
- package/docs/AGENT-SKILL.md +11 -4
- package/docs/ARCHITECTURE.md +20 -10
- package/docs/CHANGELOG.md +195 -17
- package/docs/CONFIGURATION.md +9 -7
- package/docs/KNOWLEDGE-STORE.md +4 -3
- package/docs/PI-EXTENSION.md +1 -1
- package/docs/SDK.md +55 -0
- package/docs/THIRD-PARTY-NOTICES.md +1 -1
- package/package.json +5 -4
- package/scripts/cleanup.cjs +114 -12
- package/scripts/prepare.cjs +15 -5
- package/scripts/setup.cjs +24 -4
- package/skills/pi-research/README.md +9 -2
- package/skills/pi-research/SKILL.md +6 -3
- package/skills/pi-research/references/configuration.md +6 -2
- package/skills/pi-research/scripts/run.mjs +24 -2
- package/src/cleanup/research-cleanup.ts +12 -4
- package/src/cli.ts +372 -86
- package/src/config.ts +134 -17
- package/src/constants.ts +31 -0
- package/src/core/interfaces/health-check-interfaces.ts +3 -39
- package/src/core/interfaces/knowledge-interfaces.ts +6 -1
- package/src/core/interfaces/planning-interfaces.ts +8 -0
- package/src/core/interfaces/service-names.ts +0 -1
- package/src/core/interfaces/state-manager-interfaces.ts +11 -3
- package/src/core/llm/agentic-repair.ts +14 -0
- package/src/core/pi-version.ts +136 -0
- package/src/core/planning-service.ts +43 -1
- package/src/core/service-initialization.ts +6 -14
- package/src/core/service-interfaces.ts +1 -1
- package/src/core/service-registry.ts +117 -8
- package/src/healthcheck/index.ts +7 -2
- package/src/index.ts +11 -18
- package/src/infrastructure/browser/browser-client.ts +80 -13
- package/src/infrastructure/browser/browser-error-utils.ts +6 -1
- package/src/infrastructure/browser/browser-server.ts +41 -12
- package/src/infrastructure/browser/browser-task-scheduler.ts +159 -35
- package/src/infrastructure/browser/cleanup-utils.ts +95 -9
- package/src/infrastructure/browser/client-agent.ts +6 -5
- package/src/infrastructure/browser/config.ts +14 -3
- package/src/infrastructure/browser/priority-task-queue.ts +10 -3
- package/src/infrastructure/browser/scheduler-factory.ts +44 -4
- package/src/infrastructure/browser/task-execution-service.ts +92 -28
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +50 -13
- package/src/infrastructure/browser/thread-worker-messaging.ts +54 -2
- package/src/infrastructure/browser/thread-worker-types.ts +3 -1
- package/src/infrastructure/browser/thread-worker.mjs +224 -147
- package/src/infrastructure/browser/worker-pool-manager.ts +18 -5
- package/src/infrastructure/embedding/embedding-client.ts +22 -8
- package/src/infrastructure/embedding/embedding-factory.ts +61 -15
- package/src/infrastructure/embedding/embedding-server.ts +105 -11
- package/src/infrastructure/file-lock-service.ts +295 -28
- package/src/infrastructure/knowledge-store-service.ts +48 -3
- package/src/infrastructure/process-lifecycle-service.ts +134 -34
- package/src/infrastructure/research-run-semaphore.ts +37 -3
- package/src/infrastructure/state/state-backup-manager.ts +5 -7
- package/src/infrastructure/state/state-browser-api.ts +5 -2
- package/src/infrastructure/state/state-browser-manager.ts +15 -1
- package/src/infrastructure/state/state-manager-service.ts +5 -5
- package/src/infrastructure/state/state-manager.ts +34 -23
- package/src/infrastructure/types/state-types.ts +9 -0
- package/src/knowledge/chunker.ts +88 -1
- package/src/knowledge/embedder-init.ts +1 -1
- package/src/knowledge/embedder.ts +188 -46
- package/src/knowledge/index.ts +1 -1
- package/src/knowledge/store.ts +101 -21
- package/src/knowledge/webgpu-viability.ts +9 -4
- package/src/knowledge/writer-queue.ts +88 -46
- package/src/logger.ts +5 -1
- package/src/observers/research-observer-impl.ts +31 -0
- package/src/orchestration/abort-utils.ts +60 -0
- package/src/orchestration/deep-research-orchestrator.ts +25 -4
- package/src/orchestration/headless-observer.ts +8 -0
- package/src/orchestration/quick-research-orchestrator.ts +2 -2
- package/src/orchestration/research-orchestration-service.ts +65 -15
- package/src/orchestration/research-session-service.ts +37 -11
- package/src/orchestration/research-synthesis-service.ts +33 -11
- package/src/orchestration/researcher-executor.ts +100 -20
- package/src/orchestration/researcher.ts +39 -20
- package/src/orchestration/session-state.ts +11 -0
- package/src/sdk.ts +125 -19
- package/src/security/cisa-kev.ts +10 -3
- package/src/security/github-advisories.ts +18 -7
- package/src/security/index.ts +51 -30
- package/src/security/nvd-client.ts +24 -8
- package/src/security/osv-client.ts +14 -4
- package/src/skill-install/skill-installer.ts +153 -11
- package/src/stackexchange/index.ts +20 -5
- package/src/stackexchange/output/table.ts +7 -2
- package/src/stackexchange/rest-client.ts +41 -3
- package/src/tools/research-knowledge-search.ts +15 -0
- package/src/tools/research-tool-definition.ts +8 -2
- package/src/tools/scrape.ts +48 -25
- package/src/tools/search.ts +16 -2
- package/src/tools/security.ts +68 -7
- package/src/tui/research-health.ts +18 -2
- package/src/utils/atomic-replace.ts +101 -0
- package/src/utils/circuit-breaker.ts +9 -2
- package/src/utils/citation-utils.ts +3 -1
- package/src/utils/disk-space-checker.ts +11 -6
- package/src/utils/http-body.ts +176 -0
- package/src/utils/json-utils.ts +116 -76
- package/src/utils/llm-usage.ts +17 -1
- package/src/utils/log-rotation.ts +42 -10
- package/src/utils/log-utils.ts +40 -0
- package/src/utils/metrics-summary.ts +27 -9
- package/src/utils/stdio-capture.ts +63 -22
- package/src/utils/text-utils.ts +8 -1
- package/src/utils/tool-usage-tracker.ts +9 -2
- package/src/web-research/browser-search.ts +70 -5
- package/src/web-research/scraper-utils.ts +13 -0
- package/src/web-research/search.ts +25 -6
- package/src/web-research/types.ts +19 -4
- package/src/web-research/web-scraper.ts +72 -54
- package/src/youtube/transcript-client.ts +5 -1
- package/src/core/health-check-service.ts +0 -265
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,132 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-08-06
|
|
9
|
+
|
|
10
|
+
Bundles three unreleased fix rounds that accumulated on top of 1.2.0 (each
|
|
11
|
+
already pushed to main but never cut), plus a final fresh-eyes review pass
|
|
12
|
+
done specifically to gate this release: an independent reviewer read every
|
|
13
|
+
line of the combined diff against 1.2.0 with no knowledge of why any of it
|
|
14
|
+
was written, and found several real defects the rounds that produced the
|
|
15
|
+
code had missed — two of them the most severe in this release. Every item
|
|
16
|
+
below was re-verified against the current code (not the original bug
|
|
17
|
+
report) before being counted as fixed; the two highest-severity findings
|
|
18
|
+
were personally reproduced (one as an actual OOM crash) before dispatching
|
|
19
|
+
a fix.
|
|
20
|
+
|
|
21
|
+
A minor (not patch) version for the same reason 1.2.0 was: the range
|
|
22
|
+
changes the cancellation exit-code contract again, in the same category
|
|
23
|
+
the last release documented as minor-worthy. A cancelled research run that
|
|
24
|
+
had already collected at least one report used to report plain success
|
|
25
|
+
(`ok: true`, exit `0`) because the orchestrator's fallback-synthesis path
|
|
26
|
+
resolves normally on cancellation rather than throwing — `cmdResearch`'s
|
|
27
|
+
success branch never consulted the cancellation latch, only its error
|
|
28
|
+
branch did. It now exits `128 + signal` (or `130` for a programmatic
|
|
29
|
+
abort) and sets `cancelled: true` in `--json`, matching every other
|
|
30
|
+
cancellation-reporting path fixed in 1.2.0.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
**Found in this release's own fresh-eyes pass (new, on top of the three prior rounds):**
|
|
35
|
+
- **The chunker's own safety invariant was false — `overlap < targetSize` does not, in fact, prevent infinite loops.** Reproduced directly: `new Chunker({targetSize: 5, overlap: 4}).chunk(...)` on text containing a boundary-straddling emoji crashed the worker process with an out-of-memory error rather than merely running slowly. The UTF-16 surrogate-pair backoff (added earlier in this range) can land on the exact fixed point the forward-progress guard already enforces for `end`, so neither `start` nor `end` advances between iterations and the same chunk is pushed forever. A matching, milder gap let a lone high surrogate ship uncorrected into stored/embedded text whenever backing off would have violated that same guard. Both closed: the surrogate fix now grows past the low surrogate instead of silently doing nothing when shrinking would re-collide, and a new forward-progress invariant on `start` — mirroring the existing one on `end` — guarantees real per-iteration progress independent of any heuristic interaction, not just the ones already traced by hand. A brute-force sweep across small targetSize/overlap/emoji-position combinations backs the fix, not just the one reproducing triple.
|
|
36
|
+
- **Uninstall's "is this directory exclusively ours" check was a bare name match, not an ownership check.** `isPiResearchNamespaced()` treated any path segment literally equal to `pi-research` as sufficient proof a `PI_RESEARCH_STATE_DIR` override was safe to delete wholesale — but naming a directory after the tool pointed at it is a completely ordinary thing to do (e.g. organizing several tools' state under `~/agents-state/<tool-name>/`), and nothing stops a user from keeping unrelated content in that same folder. The one regression test guarding this path only ever planted pi-research-owned entries, never foreign content, so it could never have caught this. Now only the exact default `<dirName>/research` layout — something the runtime actually constructs itself and can therefore prove is exclusively its own — authorizes wholesale removal; every other override, however named, only ever loses the specific entries the runtime is known to create there.
|
|
37
|
+
- **A researcher could still run a full billed session after its run was told to stop.** The three fast-stop checkpoints added earlier in this range (top-of-loop, initial-links loop, post-backoff sleep) left one window uncovered: between `createResearcherSession()` resolving and the session being registered for `abortAllSessions()`'s visibility, a few lines later behind an awaited call. A stop tripping in exactly that gap was invisible to the abort scan, and the caller's bounded ~10s wait for self-stopping researchers elapsed before the session's own LLM call and scrape burst finished — the run returned and released its slot while the researcher kept going in the background. A fourth checkpoint immediately after registration closes it.
|
|
38
|
+
- **A "FIX" comment described a circuit-breaker key format that has never existed in production.** `research-cleanup.ts` cleared session breakers assuming they're keyed `${piSessionId}-<8hex>`, but every real caller keys them by the plain `researchId` (generated independently of the pi session) — the function that would produce the assumed format has no production caller at all. The clear was consequently a no-op, masked on the normal run-completion path by a second, correctly-keyed clear elsewhere; the gap only mattered when this was the sole cleanup path (a throw before the orchestrator ever runs). Fixed to clear by the real key; the now-unused prefix-clearing utility this bug motivated, along with its own comment repeating the same wrong premise, is removed rather than left as bait for the same mistake again.
|
|
39
|
+
- **A follower's healthcheck request couldn't be cancelled the way search/scrape already can.** The abort-signal plumbing added earlier in this range for search/scrape was never extended to healthcheck: the interface type carries no `signal` parameter and the one production caller always calls without one — currently harmless, but silently ready to reintroduce "follower cancellation burns a leader pool slot to timeout" the moment any caller starts passing a real signal, with no compiler error to flag it. Wired through, mirroring search/scrape exactly.
|
|
40
|
+
- **Routine follower-cancellation was logged and counted as a server error.** The same abort-signal work introduced rejections shaped like `"Task search aborted while running"`, which the pool-shutdown error classifier doesn't recognize — so every cancelled in-flight browser task (a normal, frequent event whenever a research run is cancelled) logged at ERROR and inflated `browser_search_errors_total`/`browser_scrape_errors_total` before the layer that correctly demotes it ever saw the error. Each of the three call sites now checks the actual signal the rejection is a consequence of, rather than pattern-matching text — deliberately not widened inside the shared classifier, which a different caller also uses to detect a genuine leader handover.
|
|
41
|
+
- **Severity filtering for a security-database search normalized only one of three backends.** `getSeverityParam()` was applied on the NVD leg only; GitHub and OSV read the raw, unvalidated value directly — reproducing, for two of three backends, the exact "unrecognized value silently filters everything to zero" bug an earlier round in this range fixed for NVD. Masked today because the sole production caller does its own separate, stricter validation first. Now normalized once, ahead of all three legs.
|
|
42
|
+
- A flaky CI failure surfaced during this release's own gate run traced to a test relying on a fixed 50ms sleep to assume a lock had been fully acquired before tearing it down — on a loaded runner, teardown could occasionally win the race and land in a different, already-correctly-handled code path than the one the test meant to exercise. Replaced with a real "critical section has started" signal; no production code was at fault.
|
|
43
|
+
|
|
44
|
+
**From the first post-1.2.0 audit round** (orchestration, browser, embedding/state, knowledge, CLI/config, utils, tools, SDK/planning — ~40 defects): aborted researchers restarting as untracked zombies (an `=== 'Aborted'` string match missed the actual sentinel shape); PDF content zeroed over cluster IPC and the resulting extraction error cached as a successful scrape; the stale-profile sweep touching arbitrary shared-tmp entries outside its own namespace; cluster workers leaking the research query into `ps` output; `clearEmbeddingServer` gaining compare-and-delete semantics (a mere follower's clean shutdown could deregister a live leader); `getEmbedder` refusing to join an in-flight init for a different model/device; a code-fence-straddling chunk boundary emitting one overlap-sliver per character; an explicit `--config` file's keys previously applying nowhere when shadowed by the ambient overlay; CLI flags silently swallowing their own values (`--model --json` running against the literal model `"--json"`); nested embedder-init stdio capture losing LIFO ordering on a mid-capture cancel; the scrape context gate billing pre-slice batch counts; StackExchange rate-limit/quota state not surviving across tool calls; and NVD severity filtering not normalizing case or the MODERATE→MEDIUM synonym.
|
|
45
|
+
|
|
46
|
+
**From the second post-1.2.0 audit round** (browser/web-research, orchestration/SDK, knowledge/embedding, CLI/config/install — ~38 defects, including 2 regressions in the first round's own diff caught by that round's fresh-eyes pass): browser scrapes never checking HTTP status, so 4xx/5xx error pages were cached as citable content; the 100MB PDF / 25MB HTML size caps enforced only after the worker had already buffered and IPC'd the full body; search failures always hitting the global circuit breaker instead of the run's own; a follower's cancelled run burning its leader's pool slot to full timeout instead of aborting on socket close; fast-stop not reaching a researcher still acquiring links, sleeping between attempts, or rebuilding its session; the inter-attempt backoff sleep being non-abortable; a poisoned embedding leader staying eligible to re-elect onto GPU in the same process; NaN embedding vectors laundering to 0 across the HTTP hop; the full-text-search skip-guard's version bookkeeping missing rows committed mid-rebuild; a missing/unreadable explicit `--config` file being silently ignored instead of erroring; duplicate SIGINT delivery (process-group plus launcher-forwarded) hitting a consumed one-shot handler and force-exiting mid-cleanup; and the uninstall script's original, pre-this-release ownership gap for `PI_RESEARCH_STATE_DIR` (the one further hardened above).
|
|
47
|
+
|
|
48
|
+
**From the backlog-clearing round** (re-auditing every item deferred since v1.0.7 plus both rounds above): the cancellation-reporting gap described above; `knowledge --json` success payloads not carrying `ok: true` despite claiming parity with the error shape, and `knowledge-config` having no `--json` error handling at all; the SSRF IPv6 gate missing `fec0::/10` and the NAT64 well-known prefix `64:ff9b::/96` (a real bypass on hosts whose egress traverses a NAT64 gateway, reachable via `64:ff9b::169.254.169.254`); steering messages already consumed by a finished run leaking into the next unrelated run's system-prompt injection; `clearBrowserServer` gaining the same compare-and-delete semantics its embedding-leader sibling already had; a second, independent chunker surrogate-pair gap (raw UTF-16 boundaries with no pair awareness); and steering-extended round budgets being invisible to the round-phase prompt logic (`PlanningService` recomputed `maxRounds` from the base complexity table only). `HealthCheckService` — zero runtime consumers, fully superseded by the live `src/healthcheck/registry.ts` system — was deleted.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- **A cancelled-but-partially-successful research run now reports its true status.** See above — this is the same exit-code contract 1.2.0 established, extended to a success-path case that fix didn't reach.
|
|
52
|
+
|
|
53
|
+
## [1.2.0] - 2026-08-05
|
|
54
|
+
|
|
55
|
+
Clears the medium/low findings deferred out of the 1.1.2 audit, then a second
|
|
56
|
+
full-tree audit of everything since. Every item below was confirmed against the
|
|
57
|
+
code — and, where the claim was about the operating system, a package manager, or
|
|
58
|
+
a library's failure behaviour, against a live experiment on this machine rather
|
|
59
|
+
than from memory.
|
|
60
|
+
|
|
61
|
+
A minor (not patch) version because the range carries deliberate contract
|
|
62
|
+
changes alongside the fixes: cancelled runs now exit `128 + signal` instead of
|
|
63
|
+
`70` (and the agent-skill launcher forwards cancelling signals to the engine),
|
|
64
|
+
and the host-version self-check gained the tested-ceiling warning.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- **A worker dying mid-task permanently shrank the browser queue — enough deaths wedged the leader.** Measured on poolifier 5.3.2: when a cluster worker is OOM-killed or SIGKILLed, the in-flight `pool.execute()` promise is *never* settled — not on the death, not when a replacement respawns, not even on `pool.destroy()`. The worker enforces its task timeout on itself, but only while alive — so the dispatched closure pended forever and its PriorityTaskQueue concurrency slot was never released. Each such death shrank the queue by one; the caller's own timeout error is excluded from both retry and the circuit breaker, incoming requests kept resetting the idle timer, and leadership was retained — a machine-wide singleton serving nothing but timeouts. Compounding it, an unexpected `SIGKILL` exits with the same `null` code as an intentional `pool.destroy()`, so these deaths never counted toward auto-recovery either. Every `execute()` now carries its own dead-worker backstop timer created *inside* the dispatched closure (sharing no lifetime with the caller's outer race — the shared-timer variant is exactly how a queued task once lost its guard), intentional destroys bump the pool epoch first so a same-epoch `null` exit now counts as the abnormal death it is, and a task whose caller has already been answered releases its queue claim instead of keeping the slot plus a fresh worker budget. Poolifier's post-destroy wording ("not started pool") is also classified as the transient pool-swap it is, and the orphan-exit path bounds its browser cleanup so a wedged browser cannot keep the orphan alive forever.
|
|
68
|
+
- **The integration suite deleted the real `~/.cache/pi-research` on any machine with `XDG_CACHE_HOME` exported.** The preuninstall parity test spawns the real `scripts/cleanup.cjs` with a throwaway HOME — but cleanup.cjs resolves the cache from `XDG_CACHE_HOME` *before* falling back to the homedir, and the test passed the parent environment through. On a configured Linux desktop (where `XDG_CACHE_HOME` is commonly an absolute path) every `npm run test:integration` silently removed the real cache tree — browser profiles, the WebGPU probe verdict, downloaded embedding models. Verified by timestamps on this machine: the cache's birth time sat in the middle of a test run. The spawn now pins every path the script honours inside the throwaway HOME. In the same class: `cleanup.cjs` now applies to `PI_RESEARCH_STATE_DIR` / `PI_RESEARCH_CONFIG_DIR_NAME` the identical strictness it already applied to a relative `XDG_CACHE_HOME` (a relative state dir would have rm'd a tree relative to wherever the script was invoked), and `prepare.cjs` no longer builds via a shell string that leaves `$(…)` live inside double quotes — the same injection shape c1e363e7 removed from setup/cleanup.
|
|
69
|
+
- **A failed copy-refresh destroyed the installed skill permanently.** The stale-copy refresh removed the install first and copied second, so a copy failure (ENOSPC, unreadable source, a locked file on Windows) left *nothing* — and the retry guard required the path to exist, so the vanished install was never retried. Worse, the copy helper was `fs.cpSync`, whose native implementation aborts the entire host process on an unreadable directory (observed: `terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error'`) — and this runs at every startup. Refresh is now build-then-swap with rollback (the old tree is not touched until the replacement is fully built; on any failure the old install is byte-for-byte intact and retried next startup; staging names are dot-prefixed so a crash mid-swap cannot leave a directory a skill scanner would list as a duplicate skill), and the copy walks the tree in JS so failures throw catchably instead of killing the process. `installSkill`'s stale-symlink re-point now honours `dryRun` like every other mutation.
|
|
70
|
+
- **A signal aimed at the skill launcher orphaned a live engine run.** The launcher installed no signal handlers and never forwarded anything to its child — so a harness that enforces its command timeout by SIGTERM-ing the *launcher* pid (not the process group) killed the messenger and left the engine researching for minutes, holding one of the 3 machine-wide run slots plus the browser pool and embedding server. The cancelling signals are now forwarded to the child and the child's exit drives the launcher's own `128 + N`, verified end-to-end against a stub engine. A duplicate delivery (a terminal Ctrl-C already signals the whole group) is harmless — the engine swallows its second signal.
|
|
71
|
+
- **A caller mid-lock-acquisition slipped past teardown and ran its critical section on a disposed service.** `cleanup()`'s drain watches only the *held* handle and the retirement check runs before the retry loop is entered — so a caller contending for a peer's lock (holding nothing, turn already granted) was invisible to teardown, survived `cleanup()`, won the lock when the peer released, and ran its read-modify-write against torn-down services. The retry loop now re-checks retirement each attempt and again between winning the file and committing the acquisition. Also in the lock's reclaim dance: a transient read failure *after* the rename-to-trash used to silently destroy a lock that may have just changed hands to a live peer (the verify never ran) — it is now restored, on the same "a restored lock is always better than a destroyed one" rule f6c7604e established; the no-hardlink restore fallback re-creates the path exclusively instead of `rename` (which would silently replace a lock a peer created in the probe window); and trash copies leaked by a crash mid-dance are swept at startup (nothing collected them before).
|
|
72
|
+
- **A document arriving during the embedder's idle teardown could still be resurrected by a shutdown.** The 1.1.2-era fix let an `initialize()` that raced an *idle* dispose wait it out and revive — but a waiter already parked on that dispose re-checked only the *state* afterwards, not the *reason*. A terminal dispose landing as a downgrade while it waited left state at `'idle'`, so the parked waiter fell through and ran a full model load (30s cached, up to 300s uncached) during process teardown — the exact resurrection the downgrade exists to prevent. The waiter now re-reads the downgraded reason and refuses. (A later, deliberate `embed()` after a completed dispose still revives — that re-use is pinned behaviour.)
|
|
73
|
+
- **The per-query failure attribution never reached the researcher it was written for.** The infrastructure-vs-query distinction added for search failures was produced and then dropped: the search tool — the only surface the agent actually reads — rendered `*No results found.*` for every zero-result query regardless of cause, so a timeout or dead worker still read as query feedback and the agent still rewrote perfectly good queries. The tool now renders the attributed message. Two residual paths in the same class: a worker-side task cancellation (pool drain, leader handover) with no caller abort fell through to the "query may be too narrow" default and is now attributed as the infrastructure interruption it is; and a user's Esc/Ctrl-C was counted into the search *error* metrics before the abort rethrow, painting a failing backend out of a habit of cancelling runs. Duplicate query strings in one burst are also deduplicated now — both the result map and the failures map are keyed by the string, so two racing instances of the same query could clobber delivered results with a later timeout.
|
|
74
|
+
- **`--json` could report a cancellation with the wrong number — or as retryable.** The cancellation latch recorded *that* a signal fired but not *which*, so an abort unwinding through the error reporter returned a flat 130 racing the signal path's own `128 + N` — SIGTERM nondeterministically exited 130 or 143, and the `exitCode` field could contradict the actual status. The latch now carries the signal name, so both racers agree. A capacity error surfacing after Ctrl-C could also emit `retryable: true` alongside `cancelled: true` — a consumer honouring the former would re-run the research the user just stopped; `cancelled` now wins. And a bare `AbortError` *name* with no latch and none of the exact cancellation signatures is no longer classified as a cancellation: internal timeout aborts throw that name too, and an escaped one would have reported "cancelled — do not re-run" for a run nobody stopped.
|
|
75
|
+
- **Project-scoped keys in an explicit `--config` file were silently dropped.** The env bridge skips per-directory keys so the *ambient* config.env cannot out-rank a per-cwd override — but `getConfig` never reads the `--config` path at all, so a depth or knowledge-store mode set in `--config ci.env` simply did not apply, with no warning, while the bridge's comment claimed otherwise. Keys from an explicitly named config file are now bridged: a file the user names on this exact invocation outranking the per-directory registry is the intent.
|
|
76
|
+
- **A failed prune's duplicates could outlive every subsequent refresh — and be the copy served.** The add-then-prune failure mode (accepted in 1.1.2's ingest fix) leaves two generations of a URL; three follow-on holes made those duplicates sticky. The scrape-cache lookup took `limit(1)` with no ordering, and LanceDB's fragment-scan order typically returns the *oldest* row — so the superseded generation was served, and its older timestamp fed the serve-age gate. The dedup check compared the incoming hash against whichever row the store returned first, so with stable content the refresh matched the old generation, skipped, and never pruned — the one code path that ever looks at that URL again preserved the duplicates forever. And a prune skipped because the store was closing (a post-run background ingest racing Ctrl-C — the *common* window) resolved as success, so the failed-prune metric never saw its most frequent cause. Newest generation now wins everywhere, the unchanged-content skip sweeps older generations, and the closing-skip is surfaced. The strict `timestamp < olderThan` boundary is now pinned against the real engine (a row stamped *at* the boundary is the replacement itself and must survive).
|
|
77
|
+
- **Service-container lifecycle gaps from the 0cba8c58 race class.** `clearService`/`replace` disposed the early-published instance while its `initialize()` was still running, handing the awaiting caller a service disposed under it (observed shape: store-closed errors when `/research-config` changed the knowledge mode during a concurrent first search) — both now settle the in-flight initialization first. A ctx-bearing re-initialization was invisible to `disposeAll`'s settle-wait (and to concurrent callers) because it was never recorded as in-flight; it is now. `getServiceLifecycle` reported a mid-initialization service as `INITIALIZED` (instance is published before its initialize completes — that early publish is what lets dependency cycles resolve); the in-flight promise is now checked first. And the scheduler's own `shutdown()` aborted permanently — drain, state-clear, server-stop and pool teardown all skipped, with `isShuttingDown` already latched — if container disposal began between its guard and its `getService` call.
|
|
78
|
+
- **The restart cooldown trusted the wrong fact.** d4f300d7 made the herd-guard cooldown yield when the registered leader is dead — but the cooldown protects the *cached handle*, and "the registered leader is alive" is not "our handle points at it". If our leader died and another process elected a successor inside the window, the probe found the (new) leader alive, the cooldown held, and every retry kept failing against the corpse's port. The check now also requires the cached client to target the registered leader's port.
|
|
79
|
+
- **A pre-release of the floor version passed the host check.** `0.80.8-rc.1` orders *below* the `0.80.8` floor under semver and may predate the very APIs the floor guards (ModelRuntime landed in 0.80.8), but the parser discarded the tag and passed it as ok. The floor comparison now accounts for it; pre-releases above the floor are unaffected.
|
|
80
|
+
- **Small IPC asymmetries from the 4a4e65c6 class.** The browser server's 403/503/405 refusal paths now drain the request before answering (the embedding server's mirrored path already did — an undrained body can surface to the client as ECONNRESET instead of the status), and the WebGPU probe no longer decodes its child's pipe chunks individually — the last surviving `body += chunk` variant in the tree (log-mojibake only: the sentinel is ASCII).
|
|
81
|
+
- **Multi-byte UTF-8 was silently corrupted at chunk boundaries on every local IPC path.** Six HTTP paths (browser client/server, embedding client×2/server) accumulated bodies with `body += chunk`, which stringifies each Buffer on its own — a character split across a chunk boundary decoded as two fragments and both halves became U+FFFD. Measured: 15 mangled characters in a 560KB CJK payload. Nothing threw (`JSON.parse` still succeeded), so the damage reached the report and the knowledge store unannounced — and only ever for non-ASCII content, which is why it survived. The browser client carries scraped page text and the embedding server the text being embedded, so this corrupted both the output and the vectors. Bodies are now buffered and decoded once (`Utf8Body`), which also makes the payload caps count real bytes instead of UTF-16 units of an already-broken string.
|
|
82
|
+
- **Every shutdown path yanked a held cross-process lock and could wedge the lock queue forever.** `FileLockService.cleanup()` ran with no check for an in-flight critical section: it unlinked the lock file mid read-modify-write (a peer could acquire and lose the update), and because `releaseLock()` returns early when the handle is already null — *before* the `finally` that hands the FIFO turn on — every caller parked on the queue blocked forever. That wait has no timeout of its own (the lock timeout only starts once the turn is granted), so it was a permanent wedge, not a slow path. Teardown now drains the held lock first (bounded), retires the instance so late callers fail fast instead of hanging, and always hands the turn back.
|
|
83
|
+
- **The lock-reclaim restore path destroyed the very lock it meant to put back.** Both trash-and-verify branches restored a not-ours lock by linking it back inside a swallowing catch and then unlinking the trash copy unconditionally — correct only when the link failed because a peer had already re-created the lock. Any other failure (EPERM, a filesystem without hardlinks, a Windows link limit) silently destroyed a live peer's lock and let this process win it. The two cases are now distinguished: an occupied path means the peer's lock is authoritative and ours is dropped; otherwise ours is moved back. `withLock`'s second parameter — accepted and then ignored entirely — is gone rather than left to mislead.
|
|
84
|
+
- **Windows deleted live browser profiles out from under running research.** The profile-reclaim freshness heuristic read `parent.lock`'s mtime, which is stamped once at browser startup and never refreshed — so every browser alive longer than the 2-minute window looked dead, the normal case for a research run. The "the OS won't delete a held-open file" assumption was no safety net either: a recursive delete removes prefs and cookies well before it reaches the one locked file. Replaced with a positive probe of whether the lock is still held open.
|
|
85
|
+
- **`PI_RESEARCH_DISABLED_TOOLS` was not strictly subtract-only.** Disabling any unrelated tool silently *enabled* `grep` (the merged list replaced the default exclusions instead of stacking on them), and the same "no preference" input meant opposite things on the CLI and in the extension because an empty array tests truthy. Disabling now only ever removes capabilities, identically everywhere.
|
|
86
|
+
- **A vanished embedding client leaked its server request forever.** The embedding server's request promise resolved only from `'end'` or `'error'`, but a client that goes away emits `'aborted'`/`'close'` and neither of those — the handler then dangled indefinitely holding the request, the response and the buffered body. Not exotic: the client destroys the socket on its own timeout. `'close'` now settles it.
|
|
87
|
+
- **Three security clients labelled metrics with the raw user search term.** The registry keys counters by label set and is cleared only explicitly, so every distinct query added a permanent entry for the life of a pi host session — unbounded growth driven straight by user input. The term labels are gone.
|
|
88
|
+
- **`cleanup.cjs` honoured a RELATIVE `XDG_CACHE_HOME` that the runtime deliberately rejects.** Since that script exists to delete things, it would have `rmSync`'d a cwd-relative `pi-research` tree — whatever happened to sit there — while leaving the real cache in place. It now mirrors the runtime's resolution, which its comment already claimed it did.
|
|
89
|
+
- **A queued browser task could be dispatched with its timeout already disarmed.** All three scheduler entry points built ONE timeout promise and raced it twice — once against the enqueue, again inside the enqueued closure against the pool call. The two races have different lifetimes: when the outer one settled (a caller abort, a queue-capacity rejection), the method's `finally` cleared the shared timer, and a task still *queued* at that moment was dispatched afterwards racing a promise that could never settle — a guard that silently stopped guarding while the task held a queue concurrency slot. The closure's bound is now its own timer with its own lifetime (see the dead-worker entry above, which this replacement also had to cover).
|
|
90
|
+
- **Orphaned browser workers never noticed their parent had died — on Linux the guard was completely inert.** The worker polls `process.kill(process.ppid, 0)` to detect an orphaned state, and treats `EPERM` as "parent alive but owned by another user". But POSIX re-parents an orphan the instant its parent dies — to PID 1, or to the nearest subreaper — and `process.ppid` is re-read from the OS on every access, so the probe targeted PID 1, which always exists and raises `EPERM` for an unprivileged process. The guard therefore reported "alive" forever and orphaned workers kept their camoufox browsers running. Measured directly: a child's `ppid` flips from the real parent to `1` on the first tick after the parent exits, and `process.kill(1, 0)` throws `EPERM`. The parent PID is now captured once at setup, re-parenting is checked explicitly (the authoritative POSIX signal), and the liveness probe targets the original PID rather than whatever inherited the process. Windows does not re-parent, so the probe remains the mechanism there.
|
|
91
|
+
- **The browser-leader PID-reuse guard was dead code.** `StateManager.isPidAlive` passes `state.browserServer.startTime` as `expectedStartTime` so a recycled PID cannot be mistaken for a live leader, and `ProcessLifecycleService` implements the comparison — but the leader election in `scheduler-factory.ts` wrote `state.browserServer` as an inline literal carrying only the PID, bypassing the `setBrowserServer` API that records the start time. `expectedStartTime` was therefore always `undefined` and the check silently degraded to a bare signal-0 probe, which a reused PID passes: every follower would adopt an unrelated process as the browser leader and connect to its port. The election now records its own start time (matching the embedding leader, which already did).
|
|
92
|
+
- **A recently-triggered restart could leave the scheduler handle pointed at a dead leader.** A module-global 10 s cooldown suppresses cascading restarts, but it applied unconditionally — including when the cached client pointed at a process that no longer exists. A *hard* leader kill (SIGKILL, OOM) surfaces as `ECONNREFUSED`, which is a transient socket error but **not** a pool-shutdown error, so it takes the cooldown-guarded path rather than the leader-handover path added in 1.1.0. Inside the cooldown window the handle was kept, the retry re-resolved to the same dead port, and the task failed identically — the same stale-handle failure 1.1.0 fixed for graceful shutdown, reachable through the ungraceful door. The cooldown now yields whenever the registered leader cannot be confirmed alive; the herd guard is unchanged for the live-leader case it exists for.
|
|
93
|
+
- **A failed knowledge-store refresh destroyed the entry it was refreshing.** Re-ingesting a changed URL deleted the existing chunks and *then* inserted the replacements. Anything that failed in between — an unreachable embedder, `ENOSPC`, a store closing mid-write — left the URL with no entry at all, having thrown away a perfectly good older copy to make room for one that never arrived. The replacement is now written first and the superseded rows pruned afterwards, bounded by the new rows' timestamp so the prune cannot take what was just inserted. A failed prune leaves duplicates (cleaned up by the next refresh) instead of discarding a write that already succeeded.
|
|
94
|
+
- **A document arriving during the embedder's idle teardown was dropped, not retried.** The idle timer releases the embedding pipeline purely to reclaim memory, and the next embed is meant to bring it straight back — but `dispose()` flips the state synchronously and then waits (up to ~5 s for in-flight embeds plus the pipeline's own teardown), and for that entire window `initialize()` threw `Cannot initialize while disposing`. No classifier matches that string, so the knowledge writer queue took its generic branch and dropped the document with no retry. An *idle* dispose is now awaited and the embedder revived; a *terminal* dispose (shutdown, config change) still refuses, so teardown cannot resurrect the pipeline it is releasing.
|
|
95
|
+
- **Cancelling a run was reported as an infrastructure outage.** On cancellation every query returns empty, which is indistinguishable from a dead browser pool by result count alone — so the run failed with "Browser workers may be unavailable, DuckDuckGo is unreachable, or the system is under extreme load". The user had pressed Ctrl-C. Cancellation is now detected before that heuristic and propagated as a cancellation, including out of `search()`, which previously flattened it into per-query `unknown` errors and let the run carry on to synthesize from nothing.
|
|
96
|
+
- **Search failures were reported to the researcher as "the query may be too narrow".** Every zero-result query got the same `empty_results` message regardless of cause, so a timeout or a dead worker read as feedback about the query — prompting the agent to rewrite a perfectly good query when the infrastructure was what broke. (Observed when a pinned circuit breaker fast-failed 19 of a 20-query burst.) Timeouts and worker errors now carry their own type and message, stating explicitly that the failure says nothing about the query.
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
- **Cancelled runs exit in the POSIX `128 + signal` range instead of `70`.** *This changes the CLI's exit-code contract.* A deliberate stop is not a runtime fault, but it shared `SOFTWARE` (70) with genuine crashes — and 70 is documented in `SKILL.md` as "runtime error — suggest one retry for transient failures", so an agent relaying the result would offer to re-run the very research the user had just interrupted. Cancellation now reports `130` for Ctrl-C/SIGINT, `143` for SIGTERM, `129` for SIGHUP, `131` for SIGQUIT, and `130` for a programmatic abort where no signal was involved; it says "cancelled" rather than "failed" and sets `cancelled: true` (never `retryable`) in `--json`. Classification keys off the signal handler having fired, not off pattern-matching whichever subsystem noticed the abort first.
|
|
100
|
+
|
|
101
|
+
The code is derived **per signal** rather than fixed, because the CLI installs handlers for these signals and so is never actually killed by them — it catches them and exits deliberately. A fixed code would make the status a wrapper observes depend on whether our handler beat a force-kill (handler wins → the fixed code, handler loses → the shell's `128 + N`); deriving `128 + N` makes the handler transparent, so the same user action reports the same number either way. Every value lands in 129–143, safely inside the 0–255 a POSIX wait status preserves — nothing can be truncated into a false success. Windows implements no `128 + N` convention at all, so there these are simply our documented contract values. `SKILL.md` now tells agents to treat **any** exit ≥ 128 as a cancellation.
|
|
102
|
+
- **The evaluator is no longer called on the final round.** At the round cap the loop breaks to the forced final synthesis regardless of what the evaluator decides, and that path re-runs it with `mustSynthesize` — so on every capped run whose evaluator would have said `delegate` (the common case, since it is being cut off mid-plan) the in-loop call was a second full-context coordinator request whose result was discarded. Exactly one coordinator call now happens either way, with the same outcome.
|
|
103
|
+
|
|
104
|
+
- **The agent-skill launcher reported a cancelled run as a runtime error.** `run.mjs` maps a signal-killed engine to exit `70` — the code `SKILL.md` documents as "runtime error, suggest one retry" — so a harness that SIGTERMs a run on timeout prompted the agent to re-run research that had deliberately been stopped, one layer above the engine's own (now fixed) classification. `SIGINT`/`SIGTERM`/`SIGHUP`/`SIGQUIT` now report "cancelled" and exit with the same `128 + N` the shell and the engine use; `SIGKILL` and the rest still exit `70`, since an unexplained hard kill is more likely an OOM than a user's decision. Verified end-to-end against a stub engine: SIGTERM → `143`, SIGINT → `130`, SIGHUP → `129`, SIGQUIT → `131`, SIGKILL → `70`.
|
|
105
|
+
- **A leader-liveness probe could fan out across a failing query burst.** The cooldown check added above runs on the failure path, where a 20-query search losing its leader reaches it up to 19 times within milliseconds. The probe is a `/proc` read on Linux, but on Windows the PID-reuse guard resolves the process start time by spawning `powershell -NoProfile` — so an uncoalesced burst meant ~19 concurrent PowerShell processes on the slowest supported platform. Concurrent probes now share one result for a 1 s window, far shorter than the restart cooldown itself: it collapses a burst rather than caching a verdict.
|
|
106
|
+
|
|
107
|
+
- **The service container handed half-initialized services to concurrent callers.** `_initializeService` publishes the instance *before* awaiting the service's own `initialize()` — deliberately, since that is what lets a dependency cycle resolve instead of deadlocking — but any unrelated caller arriving in that window received a service whose setup had not run. `StateManagerService` only assigns its inner manager after nine awaited resolutions, so every method on it throws until then, and the embedding factory resolves it and immediately calls `updateState()`. A caller passing `ctx` was worse: it re-ran `initialize()` on an instance still executing it, constructing a second `StateManager` and discarding the first. `get()` now waits for an in-flight initialization — except when called from *within* another service's initialization, which is exactly the cycle the early publish exists to break. `tryGet()` deliberately still exposes non-settled instances: it doubles as the lifecycle inspector the health check uses to report `initializing`/`disposed` without forcing initialization.
|
|
108
|
+
- **Teardown could report completion while a service was still starting up.** `disposeAll()` snapshotted "instances that are non-null right now", so a `get()` whose factory had not yet resolved was invisible to it and nothing awaited it — the service then installed itself into the registry *after* shutdown finished and was never disposed. For `BrowserTaskScheduler` that is a listening HTTP server plus a cluster pool and Camoufox children outliving shutdown; for `KnowledgeStoreService`, an ONNX session and LanceDB handles. In-flight initializations are now settled first, under a bounded wait so a service blocked on something slow cannot hold shutdown open.
|
|
109
|
+
- **The PID-reuse guard was inert on macOS.** The non-Linux start-time lookup used `ps -o etimes=`, a procps-ng/FreeBSD extension that Apple's `ps` does not implement (POSIX mandates only `etime`). It fails quietly — procps-ng writes to stderr and still exits 0 with empty stdout, BSD `ps` exits non-zero — so the lookup returned `null` for every PID and the file lock, run-slot semaphore, GPU lease and both leader elections all silently degraded to a bare signal-0 check, while their comments claimed PID-reuse safety. Now falls back to parsing POSIX `etime`.
|
|
110
|
+
- **A process that could not be signalled was reported as dead.** `isProcessAlive` caught every `process.kill(pid, 0)` failure as "gone", but `EPERM` (POSIX) and `EACCES` (what libuv surfaces on Windows for `ERROR_ACCESS_DENIED`) mean the process *exists* and simply is not ours to signal — another user on a shared host, a CI side-car, a different Windows session. Treating those as dead lets a caller reclaim a live lock, run slot or leader from its owner. Existence and identity are now separate steps, so a permission error no longer short-circuits the start-time comparison either: a PID-reuse impostor at a non-signalable PID is still detected. This also made the existing impostor test load-bearing — it previously passed because `kill(1, 0)` raises `EPERM` and `EPERM` was read as dead, so the start-time comparison never ran and the test would have passed even with start-time lookup completely broken.
|
|
111
|
+
- **Install/uninstall ran a shell string built from a filesystem path.** `setup.cjs` and `cleanup.cjs` invoked camoufox via `execSync` with the resolved binary path interpolated into a command string. `execSync` goes through `/bin/sh -c`, where `$(…)`, backticks and quotes remain live *inside* double quotes — so a package installed under a directory whose name contains shell metacharacters would execute them at install time. Both now use `spawnSync` with an argv array, matching the runtime path in `ensure-browser.ts`.
|
|
112
|
+
|
|
113
|
+
- **The embedding server accepted work from any local process.** The browser server has required a shared secret on every request since 1.0.x; the embedding server, which exposes the same kind of loopback HTTP endpoint, checked nothing. Binding `127.0.0.1` is not an authorization boundary — any local process could enqueue embedding work, and because the serial queue steps the leader *down* when it poisons, sustained abuse denies the knowledge store for every process in the cluster rather than merely burning GPU time. It now requires the same class of secret, published to followers through the state file that already carries the browser one, checked in constant time before the body is read (so an unauthenticated caller cannot make the server buffer 50 MB) and before routing (so it cannot probe leadership state either). A leader that predates the secret publishes none and checks none, so a mixed-version cluster keeps working.
|
|
114
|
+
- **Copy-installed skills were never upgraded.** A symlink re-reads the package on every use, so it is always current; a copy is a point-in-time snapshot, and reconcile skipped non-symlinks entirely while the manifest recorded no version — so drift was undetectable by construction. Copies are not an edge case: Windows without symlink privilege, cross-device installs, `--copy`, and the documented OpenClaw route all produce them, and those users kept the `SKILL.md` their engine shipped with indefinitely, *including its exit-code contract*, while the engine underneath them changed. Entries are now version-stamped and an owned copy is refreshed when the package moves on. Ownership is re-checked first, so a foreign directory at the tracked path is still never touched, and entries written before stamping are refreshed once.
|
|
115
|
+
- **A cancelled knowledge search reported an empty store.** The catch-all returned the "nothing found" miss string for *every* error including an abort, so the calling agent read a definitive "the store has nothing on this" and proceeded to full live research — exactly the work the user had just interrupted. Cancellation now propagates. (Same misclassification as the search layer's outage message on Ctrl-C.)
|
|
116
|
+
- **Read-only accessors on the synthesis service could evict a live run's reports.** `getReport`, `getAllReports`, `getReportsForRound`, `getReportCount`, `hasReports`, `buildFallbackSynthesis`, `ensureCitedLinks` and `extractAllCitations` all went through a create-on-read helper, so a query for an unknown session id inserted an empty map — and at the 200-session cap that insert evicts the head. The LRU touch protects sessions that are read or written, but an in-flight run whose researchers are all still executing is precisely the stalest key, so a stray read could drop a live run's reports and silently shrink its final synthesis. Reads now use a non-mutating view.
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
- **Host (pi) version compatibility is now a floor *and* a tested ceiling.** The runtime self-check enforced only a minimum, so a host newer than anything this release was built against passed silently and failed deeper — and the declared range cannot help: as a pi extension the host supplies `@earendil-works/*` (our range never constrains it), and standalone, published tarballs carry no lockfile so every fresh install resolves the newest 0.x at that instant. pi is pre-1.0, where a minor bump may break anything under semver — and 0.83.0 already did extend the `ResourceLoader` contract. Below the floor is still fatal; above the last verified line it now warns once and continues, because refusing would strand users on every pi release while silence hides the likeliest cause of a strange failure. The policy moved into a tested module (`src/core/pi-version.ts`) with a test asserting the floor still matches the range declared in `package.json`, and another asserting the installed pi is within the tested window so the constant cannot go quietly stale.
|
|
120
|
+
- **CI tests more than one Node version.** `engines` declares `>=22.19.0` but every job pinned `.nvmrc` (22.19.0), so the only version ever exercised was the floor of the supported range. A current-LTS leg now runs alongside it (with the Node version in the cache key, so the two legs cannot share a `node_modules` tree and decide each other's result).
|
|
121
|
+
- **Nothing executed the CLI from an installed package layout.** The install checks verified files exist; the only place `dist/cli.mjs` actually ran was from the repo root, where the full dev tree is present. Because the bundle is built with esbuild `packages: 'external'`, every bare import resolves at runtime from the *consumer's* tree — so a dependency moved to `devDependencies`, a dropped `files` entry, or an exports subpath present only in the dev tree would pass lint, type-check, unit tests and manifest verification, and ship broken. CI and the release job now execute the installed CLI and the skill launcher. (Checked by hand: all 25 external specifiers currently resolve, so the hole was latent, not live.) The job is a three-OS matrix — the npm-generated Windows `.cmd`/`.ps1` bin shim, the thing `npx pi-research` actually invokes, had never once been generated or run in CI — and it runs the shim directly, on Windows the `.cmd` specifically.
|
|
122
|
+
- **Pinned the dependency-graph generator.** `deps:generate` ran `npx --yes madge`, resolving the newest release from the registry at CI time — unpinned third-party code executing in a job that holds `contents: write` and pushes back to the repo, and a hard network dependency that made CI non-reproducible. Now pinned to `madge@8.0.0`.
|
|
123
|
+
|
|
124
|
+
### Documentation
|
|
125
|
+
- **The README overstated the researcher's boundary.** It said the research agent "can read and search files in the working directory". The `cwd` passed to `read`/`grep` is a resolution base, not a jail — an absolute path resolves to itself — so the real boundary is *no mutation*, not *this directory only*. Reworded to "read-only by design … it can read and search files", and `ARCHITECTURE.md` now states plainly that `read`/`grep` are registered unconditionally and are not covered by the researcher exclusion list.
|
|
126
|
+
- **The SDK's cancellation contract was entirely undocumented, and is not what you would guess.** `runDeepResearch`, `runQuickResearch`, `verifyUrl` and `scrapeUrl` all accept an `AbortSignal` as a trailing positional argument — absent from `SDK.md`. More importantly, an abort does **not** simply reject: if any researcher report was already collected, the run *resolves* with a partial synthesis and fires `onComplete`, and only a run with nothing gathered rejects. A caller treating "resolved" as "ran to completion" would silently ship a truncated report as a full one. Now documented as a table, together with the matching CLI behaviour.
|
|
127
|
+
- **`--help` did not list exit code `130`.** The same omission `1.1.1` fixed for `75`.
|
|
128
|
+
- **`.env.example` implied `PI_RESEARCH_PURGE_BROWSERS` takes effect on `npm uninstall`.** It is read by `scripts/cleanup.cjs`, which npm ≥7 never invokes — and that script reads the process environment, not this file. The entry now shows the command that actually works.
|
|
129
|
+
- **`npm uninstall` cleans up nothing, and the docs said otherwise.** npm removed the `preuninstall` lifecycle for the package being uninstalled in npm 7; verified against npm 11 here, where `postinstall` fires on install and `preuninstall` never fires on uninstall. The bundled `scripts/cleanup.cjs` — which removes the agent-skill links, the state tree and the cache tree including downloaded embedding models — therefore never ran, while `AGENT-SKILL.md` claimed stale links were "garbage-collected … on `npm uninstall`". Documented accurately, with `pi-research skill uninstall` named as the supported path, and the script itself exposed as `npm run cleanup`.
|
|
130
|
+
|
|
131
|
+
### Tests
|
|
132
|
+
- **The future-state-version test asserted only the harmless half of the quarantine contract.** It checked that a `version: 2` file written by a newer build degrades to an in-memory default, but not that the newer file is left untouched — the part that actually protects the other build's live sessions, browser lease and `authSecret`. Its comment also described the mechanism as "treat it as corruption and recover", which is the opposite of what the code does. It now asserts the on-disk file is byte-intact after a write attempt and that a forensic copy was quarantined. (The full contract was covered elsewhere in the same file; this test simply read as permitting the destructive behaviour.)
|
|
133
|
+
|
|
8
134
|
## [1.1.2] - 2026-08-04
|
|
9
135
|
|
|
10
136
|
> Supersedes `v1.1.1`, which was tagged but never published: its release run was
|
|
@@ -175,21 +301,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
175
301
|
|
|
176
302
|
---
|
|
177
303
|
|
|
178
|
-
## Deferred Findings
|
|
179
|
-
|
|
180
|
-
The
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
304
|
+
## Deferred Findings — cleared 2026-08-06
|
|
305
|
+
|
|
306
|
+
The v1.0.7-era list below was re-audited against the current tree, three weeks
|
|
307
|
+
and many fix rounds after it was written. Every item resolved one of two ways:
|
|
308
|
+
already fixed as a side effect of unrelated work since, or fixed directly in
|
|
309
|
+
this pass with a regression test. None remain open.
|
|
310
|
+
|
|
311
|
+
- **KnowledgeStore.close() timeout behavior** — already safe: writes register
|
|
312
|
+
into the active-write set before the close-in-progress check can be seen,
|
|
313
|
+
and LanceDB table handles keep working after the owning connection closes,
|
|
314
|
+
so a timed-out close no longer strands or corrupts an in-flight write.
|
|
315
|
+
- **Cross-session teardown via shared singletons** — not reachable: pi fully
|
|
316
|
+
serializes a session's teardown before the next one is created, so shared
|
|
317
|
+
singletons are never disposed out from under a still-live session.
|
|
318
|
+
- **SIGINT-adjacent cancellation race** — real gap, fixed: a cancelled run
|
|
319
|
+
that had already produced a report returned through the orchestrator's
|
|
320
|
+
normal (non-throwing) partial-synthesis path, so the CLI's success branch
|
|
321
|
+
never saw the cancellation latch and reported `ok:true`/exit 0 for a run
|
|
322
|
+
the user had stopped. The success path now checks the latch and reports
|
|
323
|
+
the same `cancelled: true` / `128+signal` contract the error path already
|
|
324
|
+
used.
|
|
325
|
+
- **Knowledge-store migration vs. concurrent access** — already safe: a
|
|
326
|
+
cross-process file lock serializes migrations, and every read/write
|
|
327
|
+
re-resolves its table handle, so a migration racing another process's
|
|
328
|
+
handle fails that operation cleanly instead of serving stale data.
|
|
329
|
+
- **Global activity gate reused for per-session decisions** — real gap,
|
|
330
|
+
fixed: steering messages already consumed by a finished research run
|
|
331
|
+
survived in session state and leaked into the next, unrelated run's
|
|
332
|
+
system-prompt injection and final-report attribution. Only genuinely
|
|
333
|
+
still-queued messages now survive a run boundary.
|
|
334
|
+
- **Zombie queued browser tasks after timeout** — already fixed (in the
|
|
335
|
+
first post-1.2.0 audit round): a queued task's client-side timeout now
|
|
336
|
+
actually removes it from the scheduler's queue instead of leaving it to
|
|
337
|
+
be dispatched later.
|
|
338
|
+
- **Non-atomic `clearBrowserServer`** — real gap, fixed: the file write
|
|
339
|
+
itself was already atomic, but the read-decide-clear sequence around it
|
|
340
|
+
was not — a dead-leader restart path could wipe a legitimately new
|
|
341
|
+
leader's registration elected in the same window. `clearBrowserServer`
|
|
342
|
+
now takes an optional expected-owner argument and compares-and-deletes
|
|
343
|
+
inside the lock, mirroring the embedding leader's existing CAS.
|
|
344
|
+
- **Inconsistent `knowledge --json` payload shapes** — real gap, fixed: the
|
|
345
|
+
success payload never carried `ok:true` despite a comment claiming parity
|
|
346
|
+
with the error shape, and `knowledge-config`'s failure path had no
|
|
347
|
+
try/catch at all and fell through to a plain-text-only handler even under
|
|
348
|
+
`--json`. Both now emit a consistent, documented shape.
|
|
349
|
+
- **Backward-clock-step tolerance across multiple rate limiters** — already
|
|
350
|
+
safe: every limiter's timing arithmetic leans toward waiting longer on a
|
|
351
|
+
negative clock delta, never toward a burst or a permanent wedge.
|
|
352
|
+
|
|
353
|
+
Two further items surfaced and closed in the same pass, found via later
|
|
354
|
+
audit rounds' own deferred lists rather than the v1.0.7 one:
|
|
355
|
+
- **SSRF: `isPrivateIpv6` missed `fec0::/10`** (deprecated site-local) and
|
|
356
|
+
**`64:ff9b::/96`** (the NAT64 well-known prefix — actively deployed today,
|
|
357
|
+
and a real bypass vector on a host whose egress traverses a NAT64
|
|
358
|
+
gateway). Both ranges are now blocked.
|
|
359
|
+
- **Chunk boundaries could split a UTF-16 surrogate pair**, corrupting one
|
|
360
|
+
character (renders as U+FFFD) in stored/embedded text at an unlucky
|
|
361
|
+
boundary — the same class of bug already handled in three other places
|
|
362
|
+
in the codebase, now handled here too, on both the truncation and the
|
|
363
|
+
overlap-derived start boundary.
|
|
364
|
+
- **Steering-extended round budgets weren't visible to the round-phase
|
|
365
|
+
prompt.** When mid-run steering extended the round cap beyond the base
|
|
366
|
+
per-complexity value, the planning service still computed guidance
|
|
367
|
+
against the base value, producing an impossible "Round 4 of 3" and,
|
|
368
|
+
more consequentially, prematurely selecting LATE-phase framing during
|
|
369
|
+
rounds steering had explicitly unlocked.
|
|
370
|
+
|
|
371
|
+
One dead-code cleanup: `HealthCheckService` (a superseded, zero-consumer
|
|
372
|
+
predecessor of the live `src/healthcheck/registry.ts` system) was removed.
|
|
194
373
|
|
|
195
|
-
See `DEFERRED-FINDINGS-2026-07-14.md` for full details.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -6,7 +6,7 @@ model. This document covers the
|
|
|
6
6
|
settings exposed in the `/research-config` TUI first, then the complete
|
|
7
7
|
environment-variable reference, and finally how the configuration layers resolve.
|
|
8
8
|
|
|
9
|
-

|
|
9
|
+

|
|
10
10
|
|
|
11
11
|
### Settings in the TUI
|
|
12
12
|
|
|
@@ -79,7 +79,7 @@ Research
|
|
|
79
79
|
| `PI_RESEARCH_MAX_CONCURRENT_RUNS` | `3` | ≥1 | Machine-wide cap on research runs executing at the same time, across **every** process (CLI, agent skill, pi extension, SDK). Runs beyond the cap queue rather than fail. All concurrent runs share one leader-elected browser/embedding pool, so oversubscribing it degrades every run at once. |
|
|
80
80
|
| `PI_RESEARCH_RUN_ACQUIRE_TIMEOUT_MS` | `600000` | ≥0 | How long a run queues for a free slot before failing with "maximum concurrent research runs reached" (CLI exit `75`). `0` = fail immediately instead of queueing. |
|
|
81
81
|
| `PI_RESEARCH_MODEL` | _(pi: session model; CLI/skill: required)_ | — | The model research runs on. **Required for the standalone CLI / agent skill** — they use only this configured model (never the model selected inside the pi extension) and refuse to start without one (the CLI's per-run `--model` flag also satisfies this). On the SDK it selects the session model when no `model` option is given. In the pi extension it overrides researcher sub-agents and knowledge synthesis, while the coordinator and evaluator keep using the session model. Accepts `provider/id` or a bare model id. |
|
|
82
|
-
| `PI_RESEARCH_DISABLED_TOOLS` | _(none)_ | — | Comma-separated research tools to disable for a run (`search`, `scrape`, `security_search`, `stackexchange`, `youtube_transcript`, `grep`). Removed from every researcher's toolset and named in the coordinator/evaluator prompt. |
|
|
82
|
+
| `PI_RESEARCH_DISABLED_TOOLS` | _(none)_ | — | Comma-separated research tools to disable for a run (`search`, `scrape`, `security_search`, `stackexchange`, `youtube_transcript`, `grep`). Removed from every researcher's toolset and named in the coordinator/evaluator prompt. Strictly additive — it can only remove capabilities, never grant one, and it stacks on top of the default exclusions rather than replacing them. |
|
|
83
83
|
| `PI_RESEARCH_REPORT_EXPORT_ENABLED` (TUI) | `false` | — | Front-ends write a Markdown report to disk and surface its path. |
|
|
84
84
|
| `PI_RESEARCH_REPORT_EXPORT_DIR` | _(smart cwd)_ | — | Pin exported reports to a fixed directory, bypassing the cwd-relative resolution. Useful for the agent skill, which runs from the host agent's arbitrary directory. |
|
|
85
85
|
| `PI_RESEARCH_MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING` | `0.15` | 0.05–1.0 | Max fraction of the context window used for initial scrape context. |
|
|
@@ -151,11 +151,13 @@ Diagnostics & platform
|
|
|
151
151
|
| `PI_RESEARCH_TMP_DIR` | `~/.cache/pi-research/profiles` | Transient per-worker browser-profile directory. Disk-backed by default (kept off a RAM-backed `/tmp` so per-worker profiles don't add memory pressure). Point under the system temp dir to opt into tmpfs/RAM. |
|
|
152
152
|
| `PI_RESEARCH_STATE_DIR` | `~/.pi/research/state` | Override the state directory (active sessions, browser status, project registry). |
|
|
153
153
|
| `PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS` | `100` | TUI refresh debounce (0–1000 ms). |
|
|
154
|
-
| `PI_RESEARCH_SKIP_HEALTHCHECK` | _(unset)_ | Set `1`/`true` to skip the pre-flight browser/embedding health check and rely on per-task timeouts. |
|
|
154
|
+
| `PI_RESEARCH_SKIP_HEALTHCHECK` | _(unset)_ | Set `1`/`true` to skip the pre-flight browser/embedding health check and rely on per-task timeouts. **Depth 0 (quick) only** — depth 1-3 runs have no such pre-flight check, so this has no effect on them. |
|
|
155
155
|
| `PI_RESEARCH_USE_XVFB` | _(unset)_ | Linux only. Bare-TTY runs are true-headless and need no X server; set `true` to opt into a virtual framebuffer (`sudo apt install xvfb`). |
|
|
156
156
|
| `PI_RESEARCH_SKILL_DIR` | _(auto)_ | Override the bundled research-skill source directory used by the skill installer. |
|
|
157
|
-
| `PI_RESEARCH_PURGE_BROWSERS` | _(unset)_ |
|
|
157
|
+
| `PI_RESEARCH_PURGE_BROWSERS` | _(unset)_ | Read by the bundled `scripts/cleanup.cjs`: set `1` to also delete the shared camoufox browser cache (kept by default because other installs may use it). Note npm ≥7 does not run `preuninstall`, so that script does not fire on `npm uninstall` — see [AGENT-SKILL.md](AGENT-SKILL.md). |
|
|
158
158
|
| `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` | _(unset)_ | Set `1` during `npm install` to skip the camoufox browser download (fetched lazily on first use instead; standard Playwright convention). |
|
|
159
|
+
| `PLAYWRIGHT_BROWSERS_PATH` | _(user cache)_ | Standard Playwright variable, honoured here too: overrides where the camoufox binary is stored and looked up, and is passed through to browser workers. |
|
|
160
|
+
| `XDG_CACHE_HOME` | `~/.cache` | Standard XDG variable. When set, every `~/.cache/pi-research/...` path below is rooted at `$XDG_CACHE_HOME/pi-research/...` instead. |
|
|
159
161
|
| `PLAYWRIGHT_INSTALL_DEPS` | _(unset)_ | Linux only. Set `true` during `npm install` to also install system libraries via `npx playwright install-deps` (same as `npm run install:system-deps`). |
|
|
160
162
|
| `PI_RESEARCH_CONFIG_DIR_NAME` | `.pi` | Override the host config-directory name under your home dir (advanced; e.g. set to share another harness's config root). |
|
|
161
163
|
|
|
@@ -165,7 +167,7 @@ Testing only — never enable in production
|
|
|
165
167
|
|----------|-------------|
|
|
166
168
|
| `PI_RESEARCH_MOCK_SEARCH` | Return fabricated search results instead of real web data. |
|
|
167
169
|
| `PI_RESEARCH_MOCK_SCRAPE` | Return fabricated scrape results instead of real page content. |
|
|
168
|
-
| `PI_RESEARCH_FORCE_READY` | Bypass readiness checks and run even when critical services failed to initialize. |
|
|
170
|
+
| `PI_RESEARCH_FORCE_READY` | Bypass readiness checks and run even when critical services failed to initialize. **pi extension only** — the CLI, agent skill and SDK do not consult it. |
|
|
169
171
|
| `PI_RESEARCH_ALLOW_LOOPBACK_SCRAPE` | Permit scraping loopback/private/internal addresses, bypassing SSRF protection. |
|
|
170
172
|
|
|
171
173
|
### How configuration is layered
|
|
@@ -245,8 +247,8 @@ All pi-research state lives under its own namespace, `~/.pi/research/`:
|
|
|
245
247
|
| `~/.pi/research/state/project-settings.json` | Project registry (per-directory settings). |
|
|
246
248
|
| `~/.pi/research/state/` | Active sessions, browser status, locks. |
|
|
247
249
|
| `~/.pi/research/knowledge_db/` | The knowledge store (LanceDB), unless `PI_RESEARCH_KNOWLEDGE_DIR` is set. |
|
|
248
|
-
| `~/.cache/pi-research/profiles/` | Transient browser profiles, unless `PI_RESEARCH_TMP_DIR` is set. |
|
|
249
|
-
| `~/.cache/pi-research/webgpu-viability.json` | Cached WebGPU-viability verdict (see the knowledge store doc). |
|
|
250
|
+
| `~/.cache/pi-research/profiles/` | Transient browser profiles, unless `PI_RESEARCH_TMP_DIR` is set. Rooted at `$XDG_CACHE_HOME` when that is set. |
|
|
251
|
+
| `~/.cache/pi-research/webgpu-viability.json` | Cached WebGPU-viability verdict (see the knowledge store doc). Rooted at `$XDG_CACHE_HOME` when that is set. |
|
|
250
252
|
|
|
251
253
|
Paths can be relocated with `PI_RESEARCH_STATE_DIR`, `PI_RESEARCH_KNOWLEDGE_DIR`,
|
|
252
254
|
and `PI_RESEARCH_TMP_DIR`.
|
package/docs/KNOWLEDGE-STORE.md
CHANGED
|
@@ -12,7 +12,7 @@ in two distinct ways, and it is an optional cache — research works without it:
|
|
|
12
12
|
store; instead the orchestrator hands each researcher the previously useful URLs for
|
|
13
13
|
its goal as starting points to re-scrape live.
|
|
14
14
|
|
|
15
|
-

|
|
15
|
+

|
|
16
16
|
|
|
17
17
|
Together these make repeat work faster and cheaper.
|
|
18
18
|
|
|
@@ -177,8 +177,9 @@ built with, the store is migrated according to `PI_RESEARCH_MIGRATION_STRATEGY`:
|
|
|
177
177
|
If `re-embed` fails, pi-research falls back to `backup`. A failed `backup` (or
|
|
178
178
|
`drop`) aborts the migration instead: the store stays on the old model and the
|
|
179
179
|
next open retries — data is never dropped unless `drop` was chosen explicitly.
|
|
180
|
-
Changing the model from the `/research-config` menu
|
|
181
|
-
store and
|
|
180
|
+
Changing the model from the `/research-config` menu prompts for confirmation
|
|
181
|
+
before clearing the current store and starting fresh; declining reverts the model
|
|
182
|
+
change and leaves the store intact.
|
|
182
183
|
|
|
183
184
|
### Managing the store
|
|
184
185
|
|
package/docs/PI-EXTENSION.md
CHANGED
|
@@ -22,7 +22,7 @@ Three slash commands are also registered:
|
|
|
22
22
|
| `/research-config` | Opens the interactive TUI settings dashboard. |
|
|
23
23
|
| `/knowledge-store <query>` | Searches the local knowledge store for a query and returns a synthesised answer from previously researched findings. Unavailable when Knowledge Mode is `none`. The store auto-manages its own compaction, so there is no maintenance subcommand. |
|
|
24
24
|
|
|
25
|
-

|
|
25
|
+

|
|
26
26
|
|
|
27
27
|
### Tools
|
|
28
28
|
|
package/docs/SDK.md
CHANGED
|
@@ -98,6 +98,61 @@ report caused by a sparse topic apart from one where most researchers failed). B
|
|
|
98
98
|
> observer with `onRunQueued(slots, maxWaitMs)` to tell a waiting user the run is
|
|
99
99
|
> queued rather than hung.
|
|
100
100
|
|
|
101
|
+
### Cancellation
|
|
102
|
+
|
|
103
|
+
`runDeepResearch`, `runQuickResearch`, `verifyUrl` and `scrapeUrl` all take an
|
|
104
|
+
optional `AbortSignal` as their **last positional argument** (not a field of the
|
|
105
|
+
options object):
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
setTimeout(() => controller.abort(), 60_000);
|
|
110
|
+
|
|
111
|
+
const markdown = await runDeepResearch('…', { depth: 2 }, controller.signal);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The orchestrator checks the signal at every round boundary and threads it into
|
|
115
|
+
search, scraping and the LLM calls, so an abort stops work rather than merely
|
|
116
|
+
detaching from it.
|
|
117
|
+
|
|
118
|
+
**An abort does not always reject.** The outcome depends on whether anything was
|
|
119
|
+
collected before the signal arrived:
|
|
120
|
+
|
|
121
|
+
| State when aborted | Result | Observer |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| At least one researcher report collected | **Resolves** with a partial synthesis built from what was gathered | `onComplete` |
|
|
124
|
+
| Nothing collected yet | **Rejects** (`Research aborted` / `Research cancelled`) | `onError` |
|
|
125
|
+
|
|
126
|
+
So a caller must not treat "resolved" as "ran to completion" when it aborted the
|
|
127
|
+
run itself — check your own signal, not just the promise. Exactly one of
|
|
128
|
+
`onComplete` / `onError` fires either way.
|
|
129
|
+
|
|
130
|
+
The CLI reports cancellation through the exit code, always: a signalled run exits
|
|
131
|
+
in the cancellation range — **`128 + signal`** (`130` Ctrl-C/SIGINT, `143` SIGTERM,
|
|
132
|
+
`129` SIGHUP, `131` SIGQUIT) — and a programmatic abort where no signal was involved
|
|
133
|
+
exits `130`. A cancelled run never exits `0`, because `0` means the research
|
|
134
|
+
succeeded and an agent relaying it would report a completed run to the user.
|
|
135
|
+
|
|
136
|
+
Whether a *partial* report reaches stdout first depends on how far the run got
|
|
137
|
+
before the abort landed: the handler aborts the in-flight run before tearing down,
|
|
138
|
+
so an orchestrator that can still synthesise what it gathered may print that
|
|
139
|
+
material ahead of the exit. Treat it as a best-effort bonus, not a guarantee — the
|
|
140
|
+
exit code is the part you can rely on.
|
|
141
|
+
|
|
142
|
+
Treat any code ≥ 128 as a cancellation; `pi-research --help` lists the full set, and
|
|
143
|
+
the agent-facing contract is the exit-code table in
|
|
144
|
+
[`SKILL.md`](../skills/pi-research/SKILL.md).
|
|
145
|
+
|
|
146
|
+
These are deliberately not the `70` runtime-error code and never carry
|
|
147
|
+
`retryable: true` — a cancel is a completed intention, not a fault to retry. The
|
|
148
|
+
codes are derived per-signal rather than fixed because the CLI *handles* those
|
|
149
|
+
signals rather than dying from them: a fixed code would make the observed exit
|
|
150
|
+
status depend on whether the handler beat a force-kill, whereas `128 + N` matches
|
|
151
|
+
what the shell reports either way.
|
|
152
|
+
|
|
153
|
+
You still must call `shutdownResearchSDK()` afterwards: aborting a run releases
|
|
154
|
+
that run, not the browser pool, LanceDB handles or worker processes.
|
|
155
|
+
|
|
101
156
|
The SDK does not write report files. Report export is a front-end concern — the pi
|
|
102
157
|
extension and the CLI / agent skill do it when `PI_RESEARCH_REPORT_EXPORT_ENABLED=true`.
|
|
103
158
|
|
|
@@ -19,7 +19,7 @@ copyleft). It is resolved at install time and **not bundled** into this package'
|
|
|
19
19
|
artifacts (the bundled entry points are built `--packages=external`), so no MPL-covered source is
|
|
20
20
|
combined into or redistributed by this MIT package. It is used in-process only to
|
|
21
21
|
configure and launch the Camoufox (Firefox-fork) browser **binary as a separate
|
|
22
|
-
operating-system process**, which this package then drives over the Playwright/
|
|
22
|
+
operating-system process**, which this package then drives over the Playwright/Juggler
|
|
23
23
|
protocol. The Camoufox binary is downloaded and run separately and is subject to its
|
|
24
24
|
own license.
|
|
25
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A set of web research tools (search, scrape, and others) and research agent system that uses them safely and efficiently.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -93,13 +93,14 @@
|
|
|
93
93
|
"build": "node scripts/build.cjs all",
|
|
94
94
|
"prepublishOnly": "node scripts/build.cjs all && node scripts/verify-package.cjs manifest",
|
|
95
95
|
"prepare": "node scripts/prepare.cjs",
|
|
96
|
-
"version": "node
|
|
96
|
+
"version": "node scripts/sync-skill-version.cjs && git add skills/pi-research/SKILL.md",
|
|
97
97
|
"postinstall": "node scripts/setup.cjs",
|
|
98
98
|
"preuninstall": "node scripts/cleanup.cjs",
|
|
99
|
+
"cleanup": "node scripts/cleanup.cjs",
|
|
99
100
|
"setup": "node scripts/setup.cjs",
|
|
100
101
|
"install:system-deps": "node scripts/setup.cjs --system-deps",
|
|
101
102
|
"deps:check": "depcruise --config config/tooling/dependency-cruiser.cjs --output-type err-long src",
|
|
102
|
-
"deps:generate": "npx --yes madge --image docs/deps.svg src/index.ts",
|
|
103
|
+
"deps:generate": "npx --yes madge@8.0.0 --image docs/deps.svg src/index.ts",
|
|
103
104
|
"analyze:dependencies": "ncu",
|
|
104
105
|
"analyze:compatibility": "depngn"
|
|
105
106
|
},
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
"pdf-oxide-wasm": "^0.3.75",
|
|
127
128
|
"playwright-core": "1.60.0",
|
|
128
129
|
"poolifier": "^5.3.2",
|
|
129
|
-
"typebox": "1.
|
|
130
|
+
"typebox": "1.3.7",
|
|
130
131
|
"undici": "^8.9.0",
|
|
131
132
|
"youtubei.js": "^17.2.0"
|
|
132
133
|
},
|