pi-usereq 0.41.0 → 0.43.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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "PI-useReq Requirements"
3
3
  description: Software requirements specification
4
- version: "0.0.73"
4
+ version: "0.0.75"
5
5
  date: "2026-07-10"
6
6
  author: "OpenAI Codex"
7
7
  scope:
@@ -70,6 +70,7 @@ PI-useReq is a TypeScript pi extension plus companion Node CLI and standalone ex
70
70
  - **CTN-016**: MUST NOT modify any path under `docs/` during analysis, implementation, verification, or bug fixing.
71
71
  - **CTN-017**: MUST NOT modify any path under `pi.dev-src/` during analysis, implementation, verification, or bug fixing.
72
72
  - **CTN-019**: MUST persist local `DEBUG_TOOL_COMMANDS_ENABLED` with allowed values `enable` and `disable`, defaulting to `disable`.
73
+ - **CTN-020**: MUST declare `tsx` as a runtime `package.json` dependency so `node --import tsx` package scripts, the `postinstall` checker installer, and the `.ts` extension entry resolve on clean consumer installs.
73
74
 
74
75
  ## 3. Requirements
75
76
 
@@ -90,6 +91,8 @@ PI-useReq is a TypeScript pi extension plus companion Node CLI and standalone ex
90
91
  - **DES-011**: MUST implement `.github/workflows/release-npm.yml` as a two-job GitHub Actions pipeline where `check-branch` gates `build-release`, preserving changelog-driven GitHub Release creation while adding npm publication.
91
92
  - **DES-015**: MUST implement config-gated `debug-compress`, `debug-references`, `debug-static-check`, and `debug-tokens` slash-command wrappers in `src/index.ts` that reuse existing tool-runner execution paths.
92
93
  - **DES-016**: MUST deliver rendered bundled-prompt content to the LLM through `sendMessage` as a `display:false` custom message with `triggerTurn:true` and MUST NOT use `sendUserMessage` when `sendMessage` is available.
94
+ - **DES-017**: MUST declare `pyright`, `ruff`, and `eslint` as pinned caret-range npm dependencies and register a `postinstall` script that installs bundled static checkers.
95
+ - **DES-018**: MUST resolve checker executables by probing bundled `node_modules/.bin` paths relative to the installation path before falling back to `PATH` scan.
93
96
 
94
97
  ### 3.2 Functions
95
98
  - **REQ-001**: MUST access bundled prompts, git execution instructions, templates, and guidelines from `<installation-path>/resources` without requiring user-home resource copies before prompt or tool execution.
@@ -338,7 +341,7 @@ PI-useReq is a TypeScript pi extension plus companion Node CLI and standalone ex
338
341
  - **REQ-020**: MUST parse user `--enable-static-check` specs in `LANG=Command,CMD[,PARAM...]` format and normalize supported language names plus `Command` case-insensitively.
339
342
  - **REQ-021**: MUST reject user `--enable-static-check` specs with missing `=`, missing `Command`, missing `cmd`, unknown language, or any module other than `Command`.
340
343
  - **REQ-022**: MUST preserve persisted `Dummy` static-check entries during config loading and execute them only when present in configuration or `--test-static-check dummy` input.
341
- - **REQ-023**: MUST require `Command`-module executables to exist on `PATH` before static-check execution.
344
+ - **REQ-023**: MUST require `Command`-module executables to resolve via bundled `node_modules/.bin` or `PATH` before static-check execution.
342
345
  - **REQ-030**: MUST set static `base-path` from the bootstrap cwd and static `local-config-path` to `<base-path>/.pi-usereq.json`.
343
346
  - **REQ-317**: MUST set static `global-config-path` to `~/.config/pi-usereq/config.json`.
344
347
  - **REQ-259**: MUST set dynamic `context-path` from the bootstrap cwd and keep it aligned with `ctx.cwd`.
@@ -363,7 +366,7 @@ PI-useReq is a TypeScript pi extension plus companion Node CLI and standalone ex
363
366
  - **REQ-035**: MUST parse repeatable `--enable-static-check LANG=Command,CMD[,PARAM...]` CLI options before command dispatch and merge resulting entries into persisted global checker lists.
364
367
  - **REQ-253**: MUST set `static-check.<language>.enabled=enable` whenever guided or CLI `--enable-static-check` entry creation targets that language.
365
368
  - **REQ-036**: MUST preserve existing global `static-check` checker entries, append non-duplicate `--enable-static-check` entries in argument order, and treat canonical language, module, cmd, and params as the duplicate identity.
366
- - **REQ-037**: MUST reject `--enable-static-check` `Command` entries whose executable is unavailable on `PATH` and MUST NOT modify persisted local or global configuration when validation fails.
369
+ - **REQ-037**: MUST reject `--enable-static-check` `Command` entries whose executable is unresolvable via bundled `node_modules/.bin` or `PATH` and MUST NOT modify persisted local or global configuration when validation fails.
367
370
  - **REQ-038**: MUST honor `--verbose` only for `files-summarize`, `files-compress`, `files-find`, `summarize`, `compress`, and `find`, emitting command progress to stderr while leaving stdout payload format unchanged.
368
371
  - **REQ-039**: MUST support `--enable-line-numbers` only for `files-compress`, `compress`, `files-find`, and `find`, and MUST leave corresponding outputs unnumbered when the flag is absent.
369
372
  - **REQ-040**: MUST store canonical expected CLI result fixtures as UTF-8 text files under `tests/fixtures_attended_results/`, preserving normalized exit code, stdout, and stderr for each archived scenario.
@@ -402,6 +405,12 @@ PI-useReq is a TypeScript pi extension plus companion Node CLI and standalone ex
402
405
  - **REQ-331**: MUST omit the `%%CONTEXT_FILES%%` section for any disabled flag or missing context file without surfacing an error.
403
406
  - **REQ-332**: MUST inject `%%CONTEXT_FILES%%` content verbatim after every other prompt replacement so literal `%%...%%` tokens inside context files are not substituted.
404
407
  - **REQ-333**: MUST restore all three `Context Files` flags to disabled when the `Context Files` subtree `Reset defaults` is approved.
408
+ - **REQ-339**: MUST run `scripts/install-static-checkers.ts` as a best-effort exit-code-0 `postinstall` installer that probes bundled npm checkers, attempts `npm install` on miss, prints platform guidance for native checkers, and never modifies git-tracked files.
409
+ - **REQ-340**: MUST default the `TypeScript` static-check `Command` entry to `cmd: "eslint"` with empty `params`.
410
+ - **REQ-341**: MUST emit one warning notification during `session_start` listing missing enabled static checkers without transitioning workflow state or aborting.
411
+ - **REQ-342**: MUST keep `STATIC_CHECK_MODULES`, `dispatchStaticCheckForFile` signature, and `StaticCheckEntry` shape unchanged when adding bundled executable resolution.
412
+ - **REQ-343**: MUST NOT transition workflow state or abort `session_start` when one or more enabled static checkers are missing.
413
+ - **REQ-344**: MUST place the `session_start` missing-checker helper in `src/core/static-check.ts` so `src/index.ts` remains thin.
405
414
 
406
415
  ## 4. Test Requirements
407
416
  - **TST-001**: MUST verify extension activation registers every documented prompt command, agent tool, and configuration command while omitting tool-name slash commands, `test-static-check`, and the removed standalone config-viewer command.
@@ -524,6 +533,9 @@ PI-useReq is a TypeScript pi extension plus companion Node CLI and standalone ex
524
533
  - **TST-119**: MUST verify `%%CONTEXT_FILES%%` replacement emits one section per enabled existing file in the documented order and omits disabled or missing files.
525
534
  - **TST-120**: MUST verify `%%CONTEXT_FILES%%` sections use the file-name heading, the pre-substituted HTML file reference, and four-backtick `markdown` fences around raw content.
526
535
  - **TST-122**: MUST verify the command invocation summary renders `none` for `context files`, `static code checks`, and `enabled tools` when no items are enabled in each respective category.
536
+ - **TST-123**: MUST verify `resolveCheckerExecutable` probes bundled `node_modules/.bin` paths before `PATH` scan.
537
+ - **TST-124**: MUST verify `scripts/install-static-checkers.ts` always returns exit code `0` regardless of probe or install outcomes.
538
+ - **TST-125**: MUST verify `session_start` emits one warning notification for missing enabled checkers without aborting or transitioning workflow state.
527
539
 
528
540
  ## 5. Observed Component Model
529
541
 
@@ -545,7 +557,7 @@ PI-useReq is a TypeScript pi extension plus companion Node CLI and standalone ex
545
557
  - `@sinclair/typebox` provides runtime tool parameter schemas and is declared as a peer dependency evidenced by `src/index.ts`, `package.json`, and `package-lock.json`.
546
558
  - `js-tiktoken` provides token counting evidence in `src/core/token-counter.ts`, `package.json`, and `package-lock.json`.
547
559
  - `fast-glob` provides wildcard expansion for static-check inputs evidence in `src/core/static-check.ts`, `package.json`, and `package-lock.json`.
548
- - `tsx` is the manifest-declared TypeScript execution runner for tests and CLI scripts evidenced by `package.json` and `package-lock.json`.
560
+ - `tsx` is the manifest-declared runtime dependency and TypeScript execution runner for package scripts, the `postinstall` checker installer, and the `.ts` extension entry, evidenced by `package.json` and `package-lock.json`.
549
561
  - `typescript` is the manifest-declared compiler and type-checker evidenced by `package.json`, `package-lock.json`, and `tsconfig.json`.
550
562
  - `git` CLI is a runtime dependency for repository discovery, source-file collection, bundled prompt-command worktree orchestration, and direct `req-references` commit orchestration evidenced in `src/core/tool-runner.ts`, `src/core/prompt-command-runtime.ts`, and `src/core/req-references-command.ts`.
551
563
 
@@ -49,6 +49,13 @@
49
49
  - `build-release(...)` [`.github/workflows/release-npm.yml`]
50
50
  - Parent Process: none
51
51
  - Threads: no explicit threads detected
52
+ - ID: `PROC:install-static-checkers`
53
+ - Type: Process
54
+ - Role: Best-effort npm `postinstall` installer that probes bundled static-checker executables, attempts `npm install` on miss, and prints platform guidance for native checkers.
55
+ - Entrypoints:
56
+ - `main(...)` [`scripts/install-static-checkers.ts`]
57
+ - Parent Process: none
58
+ - Threads: no explicit threads detected
52
59
 
53
60
  ## Execution Units
54
61
  ### `PROC:main`
@@ -83,8 +90,9 @@
83
90
  - `buildStaticCheckEntryIdentity(...)`: normalize entry duplicate identity [`src/core/static-check.ts`]
84
91
  - `parseEnableStaticCheck(...)`: parse one static-check enable specification [`src/core/static-check.ts`]
85
92
  - `validateStaticCheckEntry(...)`: validate command-backed entry before config write [`src/core/static-check.ts`]
86
- - `findExecutable(...)`: resolve executable on PATH or explicit path [`src/core/static-check.ts`]
87
- - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
93
+ - `resolveCheckerExecutable(...)`: resolve executable via bundled `node_modules/.bin` or PATH scan [`src/core/static-check.ts`]
94
+ - `findExecutable(...)`: resolve executable on PATH or explicit path [`src/core/static-check.ts`]
95
+ - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
88
96
  - `saveConfig(...)`: persist split local/global config JSON [`src/core/config.ts`]
89
97
  - `saveLocalConfig(...)`: write the local config file [`src/core/config.ts`]
90
98
  - `getProjectConfigPath(...)`: resolve local config file path [`src/core/config.ts`]
@@ -208,8 +216,9 @@
208
216
  - `StaticCheckBase.headerLine(...)`: format checker header [`src/core/static-check.ts`]
209
217
  - `StaticCheckBase.emitLine(...)`: write checker output [`src/core/static-check.ts`]
210
218
  - `StaticCheckCommand.run(...)`: iterate resolved files for external command path [`src/core/static-check.ts`]
211
- - `findExecutable(...)`: resolve executable on `PATH` or explicit path [`src/core/static-check.ts`]
212
- - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
219
+ - `resolveCheckerExecutable(...)`: resolve executable via bundled `node_modules/.bin` or `PATH` scan [`src/core/static-check.ts`]
220
+ - `findExecutable(...)`: resolve executable on `PATH` or explicit path [`src/core/static-check.ts`]
221
+ - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
213
222
  - `StaticCheckCommand.checkFile(...)`: run external executable against one file [`src/core/static-check.ts`]
214
223
  - `StaticCheckBase.headerLine(...)`: format checker header [`src/core/static-check.ts`]
215
224
  - `StaticCheckBase.emitLine(...)`: write checker output [`src/core/static-check.ts`]
@@ -227,8 +236,9 @@
227
236
  - `StaticCheckBase.headerLine(...)`: format checker header [`src/core/static-check.ts`]
228
237
  - `StaticCheckBase.emitLine(...)`: write checker output [`src/core/static-check.ts`]
229
238
  - `StaticCheckCommand.run(...)`: execute external command checker [`src/core/static-check.ts`]
230
- - `findExecutable(...)`: resolve executable on `PATH` or explicit path [`src/core/static-check.ts`]
231
- - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
239
+ - `resolveCheckerExecutable(...)`: resolve executable via bundled `node_modules/.bin` or `PATH` scan [`src/core/static-check.ts`]
240
+ - `findExecutable(...)`: resolve executable on `PATH` or explicit path [`src/core/static-check.ts`]
241
+ - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
232
242
  - `StaticCheckCommand.checkFile(...)`: run external executable against one file [`src/core/static-check.ts`]
233
243
  - `StaticCheckBase.headerLine(...)`: format checker header [`src/core/static-check.ts`]
234
244
  - `StaticCheckBase.emitLine(...)`: write checker output [`src/core/static-check.ts`]
@@ -366,8 +376,9 @@
366
376
  - `StaticCheckBase.headerLine(...)`: format checker header [`src/core/static-check.ts`]
367
377
  - `StaticCheckBase.emitLine(...)`: write checker output [`src/core/static-check.ts`]
368
378
  - `StaticCheckCommand.run(...)`: execute external command checker [`src/core/static-check.ts`]
369
- - `findExecutable(...)`: resolve executable on `PATH` or explicit path [`src/core/static-check.ts`]
370
- - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
379
+ - `resolveCheckerExecutable(...)`: resolve executable via bundled `node_modules/.bin` or `PATH` scan [`src/core/static-check.ts`]
380
+ - `findExecutable(...)`: resolve executable on `PATH` or explicit path [`src/core/static-check.ts`]
381
+ - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
371
382
  - `StaticCheckCommand.checkFile(...)`: run external executable against one file [`src/core/static-check.ts`]
372
383
  - `StaticCheckBase.headerLine(...)`: format checker header [`src/core/static-check.ts`]
373
384
  - `StaticCheckBase.emitLine(...)`: write checker output [`src/core/static-check.ts`]
@@ -494,6 +505,7 @@
494
505
  - `RecordingExtensionAPI.setActiveTools(...)`: replace active-tool set with runtime-visible names [`scripts/lib/recording-extension-api.ts`]
495
506
  - `RecordingExtensionAPI.getAllTools(...)`: expose supported builtin plus extension tool inventory [`scripts/lib/recording-extension-api.ts`]
496
507
  - `setPiUsereqStatusConfig(...)`: cache docs/tests/src and pi-notify values for later status renders [`src/core/extension-status.ts`]
508
+ - `checkDefaultCheckersAvailability(...)`: enumerate missing enabled static-check executables and emit one warning notification without aborting [`src/core/static-check.ts`]
497
509
  - `updateExtensionStatus(...)`: refresh stored context usage, reset or restore elapsed timers, load the active runtime sound level from persisted config during `session_start`, reset workflow state for startup/new/reload, and render status text [`src/core/extension-status.ts`]
498
510
  - `refreshContextUsage(...)`: sync the latest context-usage snapshot [`src/core/extension-status.ts`]
499
511
  - `RecordingCommandContext.getContextUsage(...)`: return the offline context-usage placeholder [`scripts/lib/recording-extension-api.ts`]
@@ -992,6 +1004,7 @@
992
1004
  - `normalizeEnabledPiUsereqTools(...)`: canonicalize configurable active tools [`src/core/pi-usereq-tools.ts`]
993
1005
  - `getPiUsereqStartupTools(...)`: enumerate configurable tools from runtime inventory in documented menu order [`src/index.ts`]
994
1006
  - `setPiUsereqStatusConfig(...)`: cache docs/tests/src and pi-notify values for later status renders [`src/core/extension-status.ts`]
1007
+ - `checkDefaultCheckersAvailability(...)`: enumerate missing enabled static-check executables and emit one warning notification without aborting [`src/core/static-check.ts`]
995
1008
  - `activatePromptCommandExecution(...)`: re-verify the prepared execution session through replacement-session-aware `ctx.switchSession(sessionPath, { withSession })` handling, reuse persisted replacement-session contexts when lifecycle hooks omit `switchSession()`, re-align `process.cwd()` to the execution path, confirm `process.cwd()` plus the persisted execution-session file header cwd when the file is already persisted before agent start, and refresh reusable command-context persistence [`src/core/prompt-command-runtime.ts`]
996
1009
  - `resolveDebugProjectBase(...)`: prefer static `base-path` for debug-log writes during worktree-backed prompt runs and recover a live bootstrap cwd when no prompt plan is active [`src/index.ts`]
997
1010
  - `notifyContextSafely(...)`: suppress stale replacement-context notification failures after session replacement while preserving closure progress [`src/index.ts`]
@@ -1280,6 +1293,27 @@
1280
1293
  - External Boundaries:
1281
1294
  - GitHub Actions event routing, hosted-runner lifecycle, checkout action, setup-node action, npm CLI, npm registry, changelog-builder action, GitHub Releases API, and repository secrets.
1282
1295
 
1296
+ ### `PROC:install-static-checkers`
1297
+ - Entrypoints:
1298
+ - `main(...)`: best-effort bundled-checker installer root [`scripts/install-static-checkers.ts`]
1299
+ - Lifecycle/trigger:
1300
+ - Start trigger: npm invokes the `postinstall` script declared in `package.json` after dependency installation.
1301
+ - Stop trigger: returns numeric exit code `0` unconditionally after probing, optional best-effort installs, and native-checker guidance output.
1302
+ - Looping model: single-pass probe plus optional install with no persistent loop.
1303
+ - Threads: no explicit threads detected.
1304
+ - Internal Call-Trace Tree:
1305
+ - `main(...)`: probe each bundled npm checker, attempt best-effort install on miss, print native-checker guidance, and return `0` [`scripts/install-static-checkers.ts`]
1306
+ - `resolveCheckerExecutable(...)`: resolve one checker across bundled `node_modules/.bin` locations and PATH scan [`src/core/static-check.ts`]
1307
+ - `findExecutable(...)`: resolve executable on PATH or explicit path [`src/core/static-check.ts`]
1308
+ - `isExecutableFile(...)`: verify executable access bits [`src/core/static-check.ts`]
1309
+ - `attemptBundledInstall(...)`: best-effort `npm install <pkg>@<range> --no-save --prefix <install-root>` swallowing all errors [`scripts/install-static-checkers.ts`]
1310
+ - `getInstallationPath(...)`: resolve the installed extension root that owns `scripts/` and `src/` [`src/core/path-context.ts`]
1311
+ - `printNativeCheckerGuidance(...)`: emit platform-specific install guidance for native checkers [`scripts/install-static-checkers.ts`]
1312
+ - External Boundaries:
1313
+ - Node process APIs for argv, stdout, stderr, and exit code.
1314
+ - Filesystem access for executable probing under the installation path.
1315
+ - npm CLI subprocess spawned best-effort for missing bundled checkers.
1316
+
1283
1317
  ## Communication Edges
1284
1318
  - `PROC:req-debug` -> `PROC:tool-args-to-params`
1285
1319
  - Mechanism: child-process spawn through resolved `tsx` executable.
@@ -1294,4 +1328,4 @@
1294
1328
  - Endpoint/channel: `is_master` job output and shared tag-run context.
1295
1329
  - Payload/data-shape: boolean branch-gate flag derived from `origin/master` containment for the tagged commit [`.github/workflows/release-npm.yml`]
1296
1330
  - Internal thread communication edges: none.
1297
- - Relationship note: `PROC:main`, `PROC:req-debug`, `PROC:tool-args-to-params`, `PROC:debug-ext`, `PROC:pi-host`, `PROC:gh-release-check`, and `PROC:gh-release-build` are distinct runtime entry modes; only `PROC:req-debug` directly spawns child processes, while the GitHub Actions units coordinate through workflow job dependencies.
1331
+ - Relationship note: `PROC:main`, `PROC:req-debug`, `PROC:tool-args-to-params`, `PROC:debug-ext`, `PROC:pi-host`, `PROC:install-static-checkers`, `PROC:gh-release-check`, and `PROC:gh-release-build` are distinct runtime entry modes; only `PROC:req-debug` and `PROC:install-static-checkers` directly spawn child processes, while the GitHub Actions units coordinate through workflow job dependencies.
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file
4
+ * @brief Implements the best-effort postinstall installer for bundled static checkers.
5
+ * @details Probes bundled npm-backed static-check executables (`pyright`, `ruff`, `eslint`), attempts a best-effort `npm install --no-save --prefix` on miss, and prints platform-specific guidance for native checkers (`cppcheck`, `clang-format`). Runtime is dominated by child-process probing and optional npm execution. Side effects include stdout/stderr writes, filesystem reads, and best-effort `npm install` subprocess spawning. The script always exits with code `0` and never modifies git-tracked files.
6
+ */
7
+
8
+ import process from "node:process";
9
+ import { spawnSync } from "node:child_process";
10
+ import path from "node:path";
11
+ import { resolveCheckerExecutable } from "../src/core/static-check.js";
12
+ import { getInstallationPath } from "../src/core/path-context.js";
13
+
14
+ /**
15
+ * @brief Lists the npm-bundled static-check package names that the extension ships as dependencies.
16
+ * @details Each entry maps to one executable name resolvable through bundled `node_modules/.bin` after `npm install` completes. Access complexity is O(1).
17
+ */
18
+ export const BUNDLED_NPM_CHECKERS = ["pyright", "ruff", "eslint"] as const;
19
+
20
+ /**
21
+ * @brief Lists the native system-package-manager static-check executables that the extension cannot bundle.
22
+ * @details Each entry maps to one executable name that users must install through their operating system package manager. Access complexity is O(1).
23
+ */
24
+ export const NATIVE_CHECKERS = ["cppcheck", "clang-format"] as const;
25
+
26
+ /**
27
+ * @brief Maps one bundled npm checker name to its pinned caret install range.
28
+ * @details Returns the caret range matching the package manifest so best-effort installs stay aligned with declared dependencies. Runtime is O(1). No side effects occur.
29
+ * @param[in] pkg {string} Bundled npm package name.
30
+ * @return {string} Pinned caret install range.
31
+ */
32
+ function bundledCheckerRange(pkg: string): string {
33
+ switch (pkg) {
34
+ case "pyright":
35
+ return "^1.1.411";
36
+ case "ruff":
37
+ return "^1.5.4";
38
+ case "eslint":
39
+ return "^10.2.0";
40
+ default:
41
+ return "latest";
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @brief Prints platform-specific install guidance for native checkers.
47
+ * @details Detects the current platform and emits one consolidated stderr line per native checker describing the recommended system package manager command. Runtime is O(1). Side effect: writes to stderr.
48
+ * @return {void} No return value.
49
+ */
50
+ function printNativeCheckerGuidance(): void {
51
+ const platform = process.platform;
52
+ let manager: string;
53
+ let installPrefix: string;
54
+ if (platform === "linux") {
55
+ manager = "apt";
56
+ installPrefix = "sudo apt install";
57
+ } else if (platform === "darwin") {
58
+ manager = "brew";
59
+ installPrefix = "brew install";
60
+ } else if (platform === "win32") {
61
+ manager = "choco/scoop";
62
+ installPrefix = "choco install";
63
+ } else {
64
+ manager = "system package manager";
65
+ installPrefix = "install";
66
+ }
67
+ for (const checker of NATIVE_CHECKERS) {
68
+ process.stderr.write(
69
+ `Native checker '${checker}' not found. Install via ${manager}: ${installPrefix} ${checker}\n`,
70
+ );
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @brief Attempts a best-effort `npm install` for one missing bundled checker.
76
+ * @details Runs `npm install <pkg>@<range> --no-save --prefix <installation-root>` inside the installation parent directory, swallowing all errors so the postinstall flow never fails. Runtime is dominated by npm execution. Side effects include subprocess spawning and optional writes under the installation root.
77
+ * @param[in] pkg {string} Bundled npm package name.
78
+ * @return {void} No return value.
79
+ */
80
+ function attemptBundledInstall(pkg: string): void {
81
+ const range = bundledCheckerRange(pkg);
82
+ const installRoot = path.resolve(getInstallationPath(), "..");
83
+ try {
84
+ const result = spawnSync(
85
+ "npm",
86
+ ["install", `${pkg}@${range}`, "--no-save", "--prefix", installRoot],
87
+ { encoding: "utf8" },
88
+ );
89
+ if (result.error) {
90
+ process.stderr.write(`Warning: failed to install bundled checker '${pkg}': ${result.error.message}\n`);
91
+ }
92
+ } catch (error) {
93
+ process.stderr.write(
94
+ `Warning: failed to install bundled checker '${pkg}': ${error instanceof Error ? error.message : String(error)}\n`,
95
+ );
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @brief Executes the postinstall static-checker installation flow.
101
+ * @details Probes each bundled npm checker through `resolveCheckerExecutable`, attempts a best-effort install on miss, prints native-checker guidance for unresolvable native checkers, and always returns `0` so `npm install` never fails because of missing optional checkers. Runtime is dominated by PATH probing and optional npm execution. Side effects include stdout/stderr writes and best-effort `npm install` subprocess spawning. The script never modifies git-tracked files.
102
+ * @param[in] argv {string[]} Raw CLI arguments (unused, retained for CLI convention parity).
103
+ * @return {number} Always returns `0`.
104
+ * @satisfies REQ-339, DES-017
105
+ */
106
+ export function main(argv = process.argv.slice(2)): number {
107
+ void argv;
108
+ for (const checker of BUNDLED_NPM_CHECKERS) {
109
+ if (resolveCheckerExecutable(checker)) {
110
+ process.stdout.write(`Bundled static checker '${checker}' is available.\n`);
111
+ continue;
112
+ }
113
+ process.stderr.write(`Bundled static checker '${checker}' not found; attempting best-effort install.\n`);
114
+ attemptBundledInstall(checker);
115
+ }
116
+ const missingNative = NATIVE_CHECKERS.filter((checker) => !resolveCheckerExecutable(checker));
117
+ if (missingNative.length > 0) {
118
+ printNativeCheckerGuidance();
119
+ }
120
+ return 0;
121
+ }
122
+
123
+ if (import.meta.url === `file://${process.argv[1]}`) {
124
+ process.exitCode = main();
125
+ }
@@ -316,8 +316,8 @@ const DEFAULT_STATIC_CHECK_CHECKERS: Record<string, StaticCheckEntry[]> = {
316
316
  TypeScript: [
317
317
  {
318
318
  module: "Command",
319
- cmd: "npx",
320
- params: ["eslint"],
319
+ cmd: "eslint",
320
+ params: [],
321
321
  },
322
322
  ],
323
323
  };
@@ -9,8 +9,9 @@ import path from "node:path";
9
9
  import process from "node:process";
10
10
  import { spawnSync } from "node:child_process";
11
11
  import fg from "fast-glob";
12
- import type { StaticCheckEntry } from "./config.js";
12
+ import type { StaticCheckEntry, UseReqConfig } from "./config.js";
13
13
  import { ReqError } from "./errors.js";
14
+ import { getInstallationPath } from "./path-context.js";
14
15
 
15
16
  /**
16
17
  * @brief Maps user-facing language aliases to canonical static-check language names.
@@ -264,7 +265,7 @@ export function validateStaticCheckEntry(entry: StaticCheckEntry): void {
264
265
  if (!cmd) {
265
266
  throw new ReqError("Error: Command module requires a cmd argument in --enable-static-check. Format: LANG=Command,CMD[,PARAM...]", 1);
266
267
  }
267
- if (!findExecutable(cmd)) {
268
+ if (!resolveCheckerExecutable(cmd)) {
268
269
  throw new ReqError(`Error: --enable-static-check Command cmd '${cmd}' is not an executable program on this system.`, 1);
269
270
  }
270
271
  }
@@ -398,7 +399,7 @@ export class StaticCheckCommand extends StaticCheckBase {
398
399
  * @throws {ReqError} Throws when the executable cannot be found on PATH.
399
400
  */
400
401
  constructor(cmd: string, inputs: string[], extraArgs?: string[], failOnly = false) {
401
- if (!findExecutable(cmd)) {
402
+ if (!resolveCheckerExecutable(cmd)) {
402
403
  throw new ReqError(`Error: external command '${cmd}' not found on PATH.`, 1);
403
404
  }
404
405
  super(inputs, extraArgs, failOnly);
@@ -460,7 +461,7 @@ function isExecutableFile(candidate: string): boolean {
460
461
  * @param[in] cmd {string} Executable name to locate.
461
462
  * @return {string | undefined} Absolute executable path, or `undefined` when not found.
462
463
  */
463
- function findExecutable(cmd: string): string | undefined {
464
+ export function findExecutable(cmd: string): string | undefined {
464
465
  if (cmd.includes(path.sep)) {
465
466
  return isExecutableFile(cmd) ? path.resolve(cmd) : undefined;
466
467
  }
@@ -473,6 +474,59 @@ function findExecutable(cmd: string): string | undefined {
473
474
  return undefined;
474
475
  }
475
476
 
477
+ /**
478
+ * @brief Resolves one checker executable across bundled `node_modules/.bin` locations and PATH.
479
+ * @details Probes the installation-owned `node_modules/.bin` directory, the project-scope parent `node_modules/.bin` directory used by `--prefix` layouts, and finally the system PATH scan, returning the first executable match. Runtime is O(p) in PATH entry count plus bounded filesystem metadata checks. Side effects are limited to filesystem reads.
480
+ * @param[in] cmd {string} Executable name or relative path to resolve.
481
+ * @return {string | undefined} Absolute executable path, or `undefined` when not found in any probed location.
482
+ * @satisfies REQ-023, REQ-037, DES-018
483
+ */
484
+ export function resolveCheckerExecutable(cmd: string): string | undefined {
485
+ if (cmd.includes(path.sep)) {
486
+ return isExecutableFile(cmd) ? path.resolve(cmd) : undefined;
487
+ }
488
+ const installationPath = getInstallationPath();
489
+ const bundledBinCandidates = [
490
+ path.join(installationPath, "..", "node_modules", ".bin", cmd),
491
+ path.join(installationPath, "..", "..", "node_modules", ".bin", cmd),
492
+ ];
493
+ for (const candidate of bundledBinCandidates) {
494
+ if (isExecutableFile(candidate)) {
495
+ return candidate;
496
+ }
497
+ }
498
+ return findExecutable(cmd);
499
+ }
500
+
501
+ /**
502
+ * @brief Determines which enabled static-check executables are missing from the runtime environment.
503
+ * @details Iterates every enabled language checker entry in the effective config, resolves each `Command` entry executable through `resolveCheckerExecutable`, and collects the missing command names so the caller can surface one consolidated warning. Runtime is O(l * c * p) where l is language count, c is checker count, and p is PATH entry count. Side effects are limited to filesystem reads.
504
+ * @param[in] config {UseReqConfig} Effective merged configuration.
505
+ * @return {string[]} Unique command names that are enabled but unresolvable.
506
+ * @satisfies REQ-341, REQ-343, REQ-344
507
+ */
508
+ export function checkDefaultCheckersAvailability(config: UseReqConfig): string[] {
509
+ const missing = new Set<string>();
510
+ for (const languageConfig of Object.values(config["static-check"] ?? {})) {
511
+ if (languageConfig.enabled !== "enable") {
512
+ continue;
513
+ }
514
+ for (const entry of languageConfig.checkers) {
515
+ if (String(entry.module ?? "").trim().toLowerCase() !== "command") {
516
+ continue;
517
+ }
518
+ const cmd = typeof entry.cmd === "string" ? entry.cmd.trim() : "";
519
+ if (!cmd) {
520
+ continue;
521
+ }
522
+ if (!resolveCheckerExecutable(cmd)) {
523
+ missing.add(cmd);
524
+ }
525
+ }
526
+ }
527
+ return [...missing];
528
+ }
529
+
476
530
  /**
477
531
  * @brief Dispatches one configured static checker for a single file.
478
532
  * @details Selects the debug `Dummy` or user-facing `Command` implementation by module name, normalizes parameter arrays, and runs exactly one checker instance against the target file. Runtime is dominated by the selected checker. Side effects include console output and possible process spawning.
@@ -527,6 +581,9 @@ export function runStaticCheck(argv: string[]): number {
527
581
  throw new ReqError("Error: --test-static-check command requires a <cmd> argument.", 1);
528
582
  }
529
583
  const [cmd, ...files] = rest;
584
+ if (!resolveCheckerExecutable(cmd!)) {
585
+ throw new ReqError(`Error: external command '${cmd}' not found on PATH.`, 1);
586
+ }
530
587
  return new StaticCheckCommand(cmd!, files).run();
531
588
  }
532
589
  default:
package/src/index.ts CHANGED
@@ -172,6 +172,7 @@ import {
172
172
  } from "./core/tool-runner.js";
173
173
  import { LANGUAGE_TAGS } from "./core/find-constructs.js";
174
174
  import {
175
+ checkDefaultCheckersAvailability,
175
176
  getSupportedStaticCheckLanguageSupport,
176
177
  } from "./core/static-check.js";
177
178
  import { makeRelativeIfContainsProject, shellSplit } from "./core/utils.js";
@@ -1282,6 +1283,13 @@ async function handleExtensionStatusEvent(
1282
1283
  const config = loadProjectConfig(startupCwd);
1283
1284
  applyConfiguredPiUsereqTools(pi, config);
1284
1285
  setPiUsereqStatusConfig(statusController, config);
1286
+ const missingCheckers = checkDefaultCheckersAvailability(config);
1287
+ if (missingCheckers.length > 0) {
1288
+ ctx.ui.notify(
1289
+ `Missing static checkers: ${missingCheckers.join(", ")}\nInstall: run 'npm install' or install each missing checker via your system package manager.`,
1290
+ "warning",
1291
+ );
1292
+ }
1285
1293
  }
1286
1294
  if (hookName === "before_agent_start") {
1287
1295
  const requestForActivation = activePromptRequest ?? pendingPromptRequest;
@@ -5885,7 +5885,7 @@ test("default configuration applies the documented static-check, debug, notify,
5885
5885
  { module: "Command", cmd: "node", params: ["--check"] },
5886
5886
  ], "enable"));
5887
5887
  assert.deepEqual(config["static-check"].TypeScript, createStaticCheckLanguageConfig([
5888
- { module: "Command", cmd: "npx", params: ["eslint"] },
5888
+ { module: "Command", cmd: "eslint", params: [] },
5889
5889
  ], "enable"));
5890
5890
  assert.deepEqual(config["static-check"].Ruby, createStaticCheckLanguageConfig([], "disable"));
5891
5891
  assert.equal(config.DEBUG_ENABLED, "disable");
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @file
3
+ * @brief Verifies bundled static-checker resolution, postinstall exit-code invariants, and session_start missing-checker notification.
4
+ * @details Exercises `resolveCheckerExecutable` bundled-bin precedence, the `scripts/install-static-checkers.ts` exit-code-0 invariant, and the `checkDefaultCheckersAvailability` helper used during `session_start`. Runtime is dominated by filesystem metadata checks and optional child-process execution. Side effects are limited to temporary filesystem reads and bounded writes under the installation-owned `node_modules/.bin` with cleanup.
5
+ */
6
+
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { spawnSync } from "node:child_process";
13
+ import { createRequire } from "node:module";
14
+ import {
15
+ resolveCheckerExecutable,
16
+ checkDefaultCheckersAvailability,
17
+ } from "../src/core/static-check.js";
18
+ import { getInstallationPath } from "../src/core/path-context.js";
19
+ import { getDefaultConfig } from "../src/core/config.js";
20
+ import { main as installStaticCheckersMain } from "../scripts/install-static-checkers.ts";
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
24
+ const TSX_LOADER = require.resolve("tsx", { paths: [ROOT] });
25
+
26
+ /**
27
+ * @brief Resolves the bundled `node_modules/.bin` directory probed by `resolveCheckerExecutable`.
28
+ * @details Computes the first bundled-bin candidate directory derived from the runtime installation path so tests can create and clean up temporary fake executables at the exact probed location. Runtime is O(1). No side effects occur.
29
+ * @return {string} Absolute bundled `node_modules/.bin` directory path.
30
+ */
31
+ function getBundledBinDir(): string {
32
+ return path.join(getInstallationPath(), "..", "node_modules", ".bin");
33
+ }
34
+
35
+ test("resolveCheckerExecutable probes bundled node_modules/.bin before PATH scan", () => {
36
+ const binDir = getBundledBinDir();
37
+ const fakeName = "pi-usereq-fake-checker";
38
+ const fakeBin = path.join(binDir, fakeName);
39
+ const dirExisted = fs.existsSync(binDir);
40
+ try {
41
+ fs.mkdirSync(binDir, { recursive: true });
42
+ fs.writeFileSync(fakeBin, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
43
+ const resolved = resolveCheckerExecutable(fakeName);
44
+ assert.ok(resolved, "expected bundled-bin candidate to resolve");
45
+ assert.equal(resolved, fakeBin);
46
+ } finally {
47
+ if (fs.existsSync(fakeBin)) {
48
+ fs.rmSync(fakeBin, { force: true });
49
+ }
50
+ if (!dirExisted && fs.existsSync(binDir)) {
51
+ fs.rmSync(binDir, { recursive: true, force: true });
52
+ }
53
+ }
54
+ });
55
+
56
+ test("resolveCheckerExecutable falls back to PATH scan for system executables", () => {
57
+ const resolved = resolveCheckerExecutable("node");
58
+ assert.ok(resolved, "expected 'node' to resolve via PATH scan");
59
+ });
60
+
61
+ test("resolveCheckerExecutable returns undefined for non-existent commands", () => {
62
+ const resolved = resolveCheckerExecutable("pi-usereq-definitely-not-a-real-binary-xyz");
63
+ assert.equal(resolved, undefined);
64
+ });
65
+
66
+ test("scripts/install-static-checkers.ts always returns exit code 0", () => {
67
+ const exitCode = installStaticCheckersMain([]);
68
+ assert.equal(exitCode, 0);
69
+ const exitCodeWithArgs = installStaticCheckersMain(["--ignored"]);
70
+ assert.equal(exitCodeWithArgs, 0);
71
+ });
72
+
73
+ test("scripts/install-static-checkers.ts exits 0 when invoked as a subprocess", () => {
74
+ const result = spawnSync(
75
+ "node",
76
+ ["--import", TSX_LOADER, path.join(ROOT, "scripts", "install-static-checkers.ts")],
77
+ { encoding: "utf8" },
78
+ );
79
+ assert.equal(result.status, 0);
80
+ });
81
+
82
+ test("checkDefaultCheckersAvailability returns missing enabled checkers for default config", () => {
83
+ const config = getDefaultConfig(process.cwd());
84
+ const missing = checkDefaultCheckersAvailability(config);
85
+ assert.ok(Array.isArray(missing), "expected an array of missing checker names");
86
+ for (const checker of ["cppcheck", "clang-format"]) {
87
+ if (!resolveCheckerExecutable(checker)) {
88
+ assert.ok(missing.includes(checker), `expected missing checker '${checker}' to be reported`);
89
+ }
90
+ }
91
+ });
92
+
93
+ test("checkDefaultCheckersAvailability returns empty array when all checkers disabled", () => {
94
+ const config = getDefaultConfig(process.cwd());
95
+ for (const languageConfig of Object.values(config["static-check"])) {
96
+ languageConfig.enabled = "disable";
97
+ }
98
+ const missing = checkDefaultCheckersAvailability(config);
99
+ assert.equal(missing.length, 0);
100
+ });