pi-usereq 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -689,7 +689,7 @@ export function setPiUsereqStatusConfig(
689
689
 
690
690
  /**
691
691
  * @brief Returns the active runtime sound level tracked by the status controller.
692
- * @details Exposes the in-memory runtime sound state so shortcut handlers and prompt-end notification dispatch can stay decoupled from the persisted boot value stored in `.pi-usereq.json`. Runtime is O(1). No external state is mutated.
692
+ * @details Exposes the in-memory runtime sound state so shortcut handlers and prompt-end notification dispatch can stay decoupled from the persisted boot value stored in global configuration. Runtime is O(1). No external state is mutated.
693
693
  * @param[in] controller {PiUsereqStatusController} Mutable status controller.
694
694
  * @return {PiNotifySoundLevel} Active runtime sound level.
695
695
  * @satisfies REQ-180, REQ-285
@@ -702,7 +702,7 @@ export function getPiUsereqRuntimeSoundLevel(
702
702
 
703
703
  /**
704
704
  * @brief Stores one new runtime sound level and refreshes the status bar.
705
- * @details Mutates only the in-memory runtime sound state so shortcut-driven sound changes do not update `.pi-usereq.json`, then re-renders the footer when an active extension context is available. Runtime is O(1). Side effect: mutates `controller.state.runtimeSoundLevel` and may update `ctx.ui` status.
705
+ * @details Mutates only the in-memory runtime sound state so shortcut-driven sound changes do not update persisted local or global configuration, then re-renders the footer when an active extension context is available. Runtime is O(1). Side effect: mutates `controller.state.runtimeSoundLevel` and may update `ctx.ui` status.
706
706
  * @param[in,out] controller {PiUsereqStatusController} Mutable status controller.
707
707
  * @param[in] runtimeSoundLevel {PiNotifySoundLevel} Next active runtime sound level.
708
708
  * @param[in] ctx {ExtensionContext | undefined} Optional active extension context.
@@ -11,11 +11,17 @@ import { fileURLToPath } from "node:url";
11
11
  import type { UseReqConfig } from "./config.js";
12
12
 
13
13
  /**
14
- * @brief Defines the per-project configuration file name.
15
- * @details The file lives directly under `base-path` and stores only persisted project configuration. Access complexity is O(1).
14
+ * @brief Defines the per-project local configuration file name.
15
+ * @details The file lives directly under `base-path` and stores only persisted project-scoped configuration. Access complexity is O(1).
16
16
  */
17
17
  export const PROJECT_CONFIG_FILENAME = ".pi-usereq.json";
18
18
 
19
+ /**
20
+ * @brief Defines the home-relative global configuration file path.
21
+ * @details The path is resolved against the current user home directory and stores persisted cross-project configuration. Access complexity is O(1).
22
+ */
23
+ export const GLOBAL_CONFIG_RELATIVE_PATH = ".config/pi-usereq/config.json";
24
+
19
25
  /**
20
26
  * @brief Defines the bundled resources directory name under the installation path.
21
27
  * @details The directory contains prompts, templates, and guidelines shipped with the installed extension payload. Access complexity is O(1).
@@ -155,15 +161,24 @@ export function normalizeRelativeDirContract(value: string): string {
155
161
  }
156
162
 
157
163
  /**
158
- * @brief Computes the absolute project config path for one base path.
164
+ * @brief Computes the absolute local project config path for one base path.
159
165
  * @details Appends `.pi-usereq.json` to the supplied base path using the canonical repository-local configuration layout. Runtime is O(1). No external state is mutated.
160
166
  * @param[in] basePath {string} Absolute or relative base path.
161
- * @return {string} Absolute config-file path.
167
+ * @return {string} Absolute local config-file path.
162
168
  */
163
169
  export function getConfigPath(basePath: string): string {
164
170
  return path.join(path.resolve(basePath), PROJECT_CONFIG_FILENAME);
165
171
  }
166
172
 
173
+ /**
174
+ * @brief Computes the absolute global config path for the current user.
175
+ * @details Resolves `~/.config/pi-usereq/config.json` from the current user home directory without consulting project state. Runtime is O(1). No external state is mutated.
176
+ * @return {string} Absolute global config-file path.
177
+ */
178
+ export function getGlobalConfigPath(): string {
179
+ return path.join(os.homedir(), GLOBAL_CONFIG_RELATIVE_PATH);
180
+ }
181
+
167
182
  /**
168
183
  * @brief Tests whether one path is identical to or an ancestor of another path.
169
184
  * @details Resolves both inputs, computes a relative traversal from the candidate ancestor to the candidate child, and accepts only exact matches or descendant traversals that stay within the ancestor subtree. Runtime is O(p) in path length. No external state is mutated.
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * @brief Declares the extension version string.
9
9
  * @details The value is exported for external inspection and packaging metadata alignment. Access complexity is O(1).
10
10
  */
11
- export const VERSION = "0.12.0";
11
+ export const VERSION = "0.13.0";
12
12
 
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
@@ -32,6 +32,7 @@ import {
32
32
  createStaticCheckLanguageConfig,
33
33
  getDefaultConfig,
34
34
  getDefaultStaticCheckConfig,
35
+ getGlobalConfigPath,
35
36
  getProjectConfigPath,
36
37
  loadConfig,
37
38
  normalizeConfigPaths,
@@ -268,12 +269,12 @@ function loadProjectConfig(cwd: string): UseReqConfig {
268
269
  }
269
270
 
270
271
  /**
271
- * @brief Persists project configuration from the extension runtime.
272
- * @details Resolves the project base, normalizes configured directory paths into project-relative form, and delegates persistence to `saveConfig` without serializing runtime-derived path metadata. Runtime is O(n) in config size. Side effects include config-file writes.
272
+ * @brief Persists effective project configuration from the extension runtime.
273
+ * @details Resolves the project base, normalizes configured local directory paths into project-relative form, and delegates split local/global persistence to `saveConfig` without serializing runtime-derived path metadata. Runtime is O(n) in config size. Side effects include config-file writes.
273
274
  * @param[in] cwd {string} Current working directory.
274
- * @param[in] config {UseReqConfig} Configuration to persist.
275
+ * @param[in] config {UseReqConfig} Effective configuration to persist.
275
276
  * @return {void} No return value.
276
- * @satisfies REQ-146
277
+ * @satisfies REQ-146, REQ-315
277
278
  */
278
279
  function saveProjectConfig(cwd: string, config: UseReqConfig): void {
279
280
  const projectBase = getProjectBase(cwd);
@@ -281,18 +282,28 @@ function saveProjectConfig(cwd: string, config: UseReqConfig): void {
281
282
  }
282
283
 
283
284
  /**
284
- * @brief Formats the current project config path for top-level menu display.
285
- * @details Resolves `<base-path>/.pi-usereq.json` from the cwd-derived project base, reuses the shared runtime-path formatter, and rewrites a leading POSIX `$HOME` token to `~` for the `Show configuration` row only. Runtime is O(p) in path length. No external state is mutated.
285
+ * @brief Formats the current local config path for top-level menu display.
286
+ * @details Resolves `<base-path>/.pi-usereq.json` from the cwd-derived project base and reuses the shared runtime-path formatter so the `Show local configuration` row uses the documented `~`-relative display contract. Runtime is O(p) in path length. No external state is mutated.
286
287
  * @param[in] cwd {string} Current working directory.
287
- * @return {string} `~`-relative or absolute config path display value.
288
+ * @return {string} `~`-relative or absolute local config-path display value.
288
289
  * @satisfies REQ-162
289
290
  */
290
- function formatProjectConfigPathForMenu(cwd: string): string {
291
+ function formatLocalConfigPathForMenu(cwd: string): string {
291
292
  return formatRuntimePathForDisplay(
292
293
  getProjectConfigPath(getProjectBase(cwd)),
293
294
  );
294
295
  }
295
296
 
297
+ /**
298
+ * @brief Formats the current global config path for top-level menu display.
299
+ * @details Resolves `~/.config/pi-usereq/config.json` through the shared runtime-path formatter so the `Show global configuration` row uses the documented `~`-relative display contract. Runtime is O(p) in path length. No external state is mutated.
300
+ * @return {string} `~`-relative or absolute global config-path display value.
301
+ * @satisfies REQ-319
302
+ */
303
+ function formatGlobalConfigPathForMenu(): string {
304
+ return formatRuntimePathForDisplay(getGlobalConfigPath());
305
+ }
306
+
296
307
  /**
297
308
  * @brief Builds the standardized terminal rows appended to every configuration menu.
298
309
  * @details Returns the canonical value-less `Reset defaults` row so all configuration menus and descendant selector menus share the same terminal ordering contract without rendering `Save and close`. Runtime is O(1). No external state is mutated.
@@ -409,21 +420,46 @@ async function confirmResetChanges(
409
420
  }
410
421
 
411
422
  /**
412
- * @brief Writes the already-persisted project configuration file text into the editor.
413
- * @details Reads the current `.pi-usereq.json` file content from disk after the caller has saved any pending configuration changes and forwards that exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
423
+ * @brief Writes one already-persisted config file text into the editor.
424
+ * @details Reads the target config file from disk after the caller has saved any pending changes and forwards the exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
425
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
426
+ * @param[in] configPath {string} Absolute persisted config path.
427
+ * @return {void} No return value.
428
+ */
429
+ function writePersistedConfigToEditor(
430
+ ctx: ExtensionCommandContext,
431
+ configPath: string,
432
+ ): void {
433
+ ctx.ui.setEditorText(fs.readFileSync(configPath, "utf8"));
434
+ }
435
+
436
+ /**
437
+ * @brief Writes the already-persisted local configuration file text into the editor.
438
+ * @details Reads `<base-path>/.pi-usereq.json` from disk after the caller has saved any pending local and global configuration changes, then forwards that exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
414
439
  * @param[in] ctx {ExtensionCommandContext} Active command context.
415
440
  * @param[in] cwd {string} Current working directory.
416
- * @param[in] _config {UseReqConfig} Unused effective project configuration retained for stable call-site shape.
417
441
  * @return {void} No return value.
418
442
  * @satisfies REQ-031
419
443
  */
420
- function writePersistedProjectConfigToEditor(
444
+ function writePersistedLocalConfigToEditor(
421
445
  ctx: ExtensionCommandContext,
422
446
  cwd: string,
423
- _config: UseReqConfig,
424
447
  ): void {
425
448
  const projectBase = getProjectBase(cwd);
426
- ctx.ui.setEditorText(fs.readFileSync(getProjectConfigPath(projectBase), "utf8"));
449
+ writePersistedConfigToEditor(ctx, getProjectConfigPath(projectBase));
450
+ }
451
+
452
+ /**
453
+ * @brief Writes the already-persisted global configuration file text into the editor.
454
+ * @details Reads `~/.config/pi-usereq/config.json` from disk after the caller has saved any pending local and global configuration changes, then forwards that exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
455
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
456
+ * @return {void} No return value.
457
+ * @satisfies REQ-318
458
+ */
459
+ function writePersistedGlobalConfigToEditor(
460
+ ctx: ExtensionCommandContext,
461
+ ): void {
462
+ writePersistedConfigToEditor(ctx, getGlobalConfigPath());
427
463
  }
428
464
 
429
465
  /**
@@ -2314,7 +2350,7 @@ async function selectPiNotifySoundLevel(
2314
2350
 
2315
2351
  /**
2316
2352
  * @brief Runs the interactive notification-configuration menu.
2317
- * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, delegates completed/interrupted/failed toggles to dedicated event submenus, persists boot-sound changes without altering the active runtime sound level, keeps `Enable pushover` locked until both credentials are populated, decodes escaped control-sequence input for `Pushover text`, and preserves row focus across menu re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2353
+ * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, persists every notification subtree mutation into global configuration, delegates completed/interrupted/failed toggles to dedicated event submenus, preserves boot-sound edits without altering the active runtime sound level, keeps `Enable pushover` locked until both credentials are populated, decodes escaped control-sequence input for `Pushover text`, and preserves row focus across menu re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2318
2354
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2319
2355
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2320
2356
  * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
@@ -2610,10 +2646,10 @@ async function configurePiNotifyMenu(
2610
2646
 
2611
2647
  /**
2612
2648
  * @brief Registers the configurable notification-sound shortcut when supported.
2613
- * @details Loads the current project config, registers one raw pi shortcut when
2649
+ * @details Loads the current effective config, registers one raw pi shortcut when
2614
2650
  * the runtime exposes `registerShortcut(...)`, cycles only the active runtime
2615
- * sound level on invocation, leaves `.pi-usereq.json` unchanged, refreshes the
2616
- * status bar, and emits one info notification. Runtime is O(1) for registration
2651
+ * sound level on invocation, leaves persisted local and global configuration
2652
+ * unchanged, refreshes the status bar, and emits one info notification. Runtime is O(1) for registration
2617
2653
  * plus one status update per shortcut use. Side effects include shortcut
2618
2654
  * registration and status updates.
2619
2655
  * @param[in] pi {ExtensionAPI} Active extension API instance.
@@ -3325,7 +3361,7 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
3325
3361
 
3326
3362
  /**
3327
3363
  * @brief Runs the interactive active-tool configuration menu.
3328
- * @details Synchronizes runtime active tools with persisted config, renders startup-tool actions through the shared settings-menu UI, preserves the documented per-tool ordering, and updates configuration state in response to selections until the user exits. Runtime depends on user interaction count. Side effects include UI updates, active-tool changes, and config mutation.
3364
+ * @details Synchronizes runtime active tools with the effective config, renders startup-tool actions through the shared settings-menu UI, persists enablement changes into global configuration, preserves the documented per-tool ordering, and updates configuration state in response to selections until the user exits. Runtime depends on user interaction count. Side effects include UI updates, active-tool changes, and config mutation.
3329
3365
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3330
3366
  * @param[in] ctx {ExtensionCommandContext} Active command context.
3331
3367
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
@@ -3599,7 +3635,7 @@ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUser
3599
3635
 
3600
3636
  /**
3601
3637
  * @brief Runs the interactive static-check configuration menu.
3602
- * @details Lets the user add Command entries by guided prompts, remove configured language entries, toggle direct per-language enable flags, and reset the subtree to documented defaults through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
3638
+ * @details Lets the user add and remove global Command entries, toggle direct local per-language enable flags, and reset the subtree to documented defaults through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
3603
3639
  * @param[in] ctx {ExtensionCommandContext} Active command context.
3604
3640
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
3605
3641
  * @return {Promise<void>} Promise resolved when the menu closes.
@@ -3755,11 +3791,11 @@ async function configureStaticCheckMenu(
3755
3791
 
3756
3792
  /**
3757
3793
  * @brief Builds the shared settings-menu choices for the top-level pi-usereq configuration UI.
3758
- * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer, including automatic git-commit mode, effective prompt-command worktree state, notification summary, debug summary, locked worktree rows when automatic git commit is disabled, and the display-only config path beside `show-config`. Runtime is O(s) in source-directory count. No external state is mutated.
3794
+ * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer, including automatic git-commit mode, effective prompt-command worktree state, notification summary, debug summary, locked worktree rows when automatic git commit is disabled, and display-only local plus global config paths. Runtime is O(s) in source-directory count. No external state is mutated.
3759
3795
  * @param[in] cwd {string} Current working directory.
3760
3796
  * @param[in] config {UseReqConfig} Effective project configuration.
3761
3797
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
3762
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240
3798
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-314, REQ-318, REQ-319, REQ-320
3763
3799
  */
3764
3800
  function buildPiUsereqMenuChoices(
3765
3801
  cwd: string,
@@ -3822,32 +3858,39 @@ function buildPiUsereqMenuChoices(
3822
3858
  id: "static-check",
3823
3859
  label: "Language static code checkers",
3824
3860
  value: formatStaticCheckLanguagesSummary(config),
3825
- description: "Manage guided Command static-check entries and per-language enable flags.",
3861
+ description: "Manage global Command static-check entries and local per-language enable flags.",
3826
3862
  },
3827
3863
  {
3828
3864
  id: "startup-tools",
3829
3865
  label: "Enable tools",
3830
3866
  value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
3831
- description: "Manage which configurable tools become active during session_start.",
3867
+ description: "Manage the global configurable tool set activated during session_start.",
3832
3868
  },
3833
3869
  {
3834
3870
  id: "notifications",
3835
3871
  label: "Notifications",
3836
3872
  value: `notification:${formatPiNotifyStatus(config)} • sound:${config["notify-sound"]} • pushover:${formatPiNotifyPushoverStatus(config)}`,
3837
- description: "Manage command-notify, sound, and Pushover settings with dedicated event submenus.",
3873
+ description: "Manage global command-notify, sound, and Pushover settings with dedicated event submenus.",
3838
3874
  },
3839
3875
  {
3840
3876
  id: "debug",
3841
3877
  label: "Debug",
3842
3878
  value: formatDebugMenuSummary(config),
3843
- description: "Manage debug logging for tools and `req-*` prompt orchestration.",
3879
+ description: "Manage project-local debug logging for tools and `req-*` prompt orchestration.",
3880
+ },
3881
+ {
3882
+ id: "show-local-config",
3883
+ label: "Show local configuration",
3884
+ value: formatLocalConfigPathForMenu(cwd),
3885
+ valueTone: "dim",
3886
+ description: "Persist pending configuration changes and write the exact local config file text into the editor.",
3844
3887
  },
3845
3888
  {
3846
- id: "show-config",
3847
- label: "Show configuration",
3848
- value: formatProjectConfigPathForMenu(cwd),
3889
+ id: "show-global-config",
3890
+ label: "Show global configuration",
3891
+ value: formatGlobalConfigPathForMenu(),
3849
3892
  valueTone: "dim",
3850
- description: "Persist the current project configuration file and write its exact text into the editor.",
3893
+ description: "Persist pending configuration changes and write the exact global config file text into the editor.",
3851
3894
  },
3852
3895
  ...buildTerminalSettingsMenuChoices({
3853
3896
  resetDefaultsDescription: "Restore the default pi-usereq configuration for the current project base.",
@@ -3905,12 +3948,12 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
3905
3948
 
3906
3949
  /**
3907
3950
  * @brief Runs the top-level pi-usereq configuration menu.
3908
- * @details Loads project config, exposes docs/test/source/automatic-commit/worktree/static-check/startup-tool/notification/debug actions through the shared settings-menu renderer, forces worktree disablement when automatic git commit is disabled, prevents locked row edits, persists changes on exit, closes immediately after `Show configuration`, and refreshes the single-line status bar. Runtime depends on user interaction count. Side effects include UI updates, config writes, active-tool changes, and editor text updates.
3951
+ * @details Loads the effective merged config, exposes docs/test/source/automatic-commit/worktree/static-check/startup-tool/notification/debug actions through the shared settings-menu renderer, forces worktree disablement when automatic git commit is disabled, prevents locked row edits, persists changes on exit, closes immediately after `Show local configuration` or `Show global configuration`, and refreshes the single-line status bar. Runtime depends on user interaction count. Side effects include UI updates, config writes, active-tool changes, and editor text updates.
3909
3952
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3910
3953
  * @param[in] ctx {ExtensionCommandContext} Active command context.
3911
3954
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3912
3955
  * @return {Promise<void>} Promise resolved when configuration is saved and the menu closes.
3913
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162, REQ-190, REQ-191, REQ-192, REQ-194, REQ-195, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243
3956
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162, REQ-190, REQ-191, REQ-192, REQ-194, REQ-195, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-314, REQ-318, REQ-319, REQ-320
3914
3957
  */
3915
3958
  async function configurePiUsereq(
3916
3959
  pi: ExtensionAPI,
@@ -4152,12 +4195,16 @@ async function configurePiUsereq(
4152
4195
  ctx.ui.notify("Restored all default configuration values", "info");
4153
4196
  continue;
4154
4197
  }
4155
- if (choice === "show-config") {
4198
+ if (choice === "show-local-config" || choice === "show-global-config") {
4156
4199
  persistConfigChange();
4157
4200
  if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
4158
4201
  ctx.ui.notify("Sound toggle hotkey bind updated; run /reload to apply the new binding", "info");
4159
4202
  }
4160
- writePersistedProjectConfigToEditor(ctx, ctx.cwd, config);
4203
+ if (choice === "show-local-config") {
4204
+ writePersistedLocalConfigToEditor(ctx, ctx.cwd);
4205
+ } else {
4206
+ writePersistedGlobalConfigToEditor(ctx);
4207
+ }
4161
4208
  return;
4162
4209
  }
4163
4210
  }
@@ -14,11 +14,13 @@ import { detectLanguage } from "../src/core/generate-markdown.js";
14
14
  import { LANGUAGE_TAGS } from "../src/core/find-constructs.js";
15
15
  import {
16
16
  createStaticCheckLanguageConfig,
17
- getProjectConfigPath,
17
+ getDefaultConfig,
18
18
  type UseReqConfig,
19
19
  } from "../src/core/config.js";
20
20
  import {
21
21
  initFixtureRepo,
22
+ readGlobalConfigJson,
23
+ readProjectConfigJson,
22
24
  runNodeCli,
23
25
  runPythonCli,
24
26
  runPythonInline,
@@ -450,10 +452,14 @@ function buildProjectScenarios(): AttendedScenario[] {
450
452
  normalize: createProjectNormalizer(projectBase),
451
453
  cleanup: () => removePath(projectBase),
452
454
  postAssert: () => {
453
- const payload = JSON.parse(fs.readFileSync(getProjectConfigPath(projectBase), "utf8")) as UseReqConfig;
454
- assert.deepEqual(payload["static-check"].Python, createStaticCheckLanguageConfig([
455
+ const defaultConfig = getDefaultConfig(projectBase);
456
+ const localPayload = readProjectConfigJson(projectBase) as unknown as Record<string, unknown>;
457
+ const globalPayload = readGlobalConfigJson() as unknown as Record<string, unknown>;
458
+ assert.equal((localPayload["static-check"] as Record<string, any>).Python.enabled, "enable");
459
+ assert.deepEqual((globalPayload["static-check"] as Record<string, any>).Python.checkers, [
460
+ ...defaultConfig["static-check"].Python.checkers,
455
461
  { module: "Command", cmd: "git", params: ["--version"] },
456
- ], "enable"));
462
+ ]);
457
463
  },
458
464
  };
459
465
  },
@@ -12,6 +12,7 @@ import { spawnSync } from "node:child_process";
12
12
  import {
13
13
  createStaticCheckLanguageConfig,
14
14
  DEFAULT_DOCS_DIR,
15
+ getDefaultConfig,
15
16
  getProjectConfigPath,
16
17
  type UseReqConfig,
17
18
  } from "../src/core/config.js";
@@ -21,6 +22,7 @@ import {
21
22
  createTempDir,
22
23
  getFixtureFiles,
23
24
  initFixtureRepo,
25
+ readGlobalConfigJson,
24
26
  readProjectConfigJson,
25
27
  runNodeCli,
26
28
  runPythonCli,
@@ -115,11 +117,11 @@ function getFixtureTagFilter(filePath: string): string {
115
117
  }
116
118
 
117
119
  /**
118
- * @brief Returns the persisted per-language static-check config for one language.
119
- * @details Reads the raw project config JSON so tests can assert entry order, duplicate suppression, enable-flag persistence, and metadata preservation exactly as written. Runtime is O(n) in config file size. Side effects are limited to filesystem reads.
120
+ * @brief Returns the persisted local static-check config for one language.
121
+ * @details Reads the raw local project config JSON so tests can assert enable-flag persistence and local-scope shape exactly as written. Runtime is O(n) in config file size. Side effects are limited to filesystem reads.
120
122
  * @param[in] projectBase {string} Fixture project root.
121
123
  * @param[in] language {string} Canonical language key.
122
- * @return {Record<string, unknown> | undefined} Persisted language config object when present.
124
+ * @return {Record<string, unknown> | undefined} Persisted local language config object when present.
123
125
  */
124
126
  function getStaticCheckLanguageConfig(projectBase: string, language: string): Record<string, unknown> | undefined {
125
127
  const payload = readProjectConfigJson(projectBase);
@@ -128,14 +130,17 @@ function getStaticCheckLanguageConfig(projectBase: string, language: string): Re
128
130
  }
129
131
 
130
132
  /**
131
- * @brief Returns the persisted static-check entries for one language.
132
- * @details Reads the raw project config JSON and extracts the `checkers` array so tests can assert entry order and duplicate suppression exactly as written. Runtime is O(n) in config file size. Side effects are limited to filesystem reads.
133
- * @param[in] projectBase {string} Fixture project root.
133
+ * @brief Returns the persisted global static-check entries for one language.
134
+ * @details Reads the raw global config JSON and extracts the `checkers` array so tests can assert entry order and duplicate suppression exactly as written. Runtime is O(n) in config file size. Side effects are limited to filesystem reads.
135
+ * @param[in] projectBase {string} Fixture project root retained for stable call-site shape.
134
136
  * @param[in] language {string} Canonical language key.
135
- * @return {Array<Record<string, unknown>>} Persisted checker-entry array or an empty array.
137
+ * @return {Array<Record<string, unknown>>} Persisted global checker-entry array or an empty array.
136
138
  */
137
139
  function getStaticCheckEntries(projectBase: string, language: string): Array<Record<string, unknown>> {
138
- const languageConfig = getStaticCheckLanguageConfig(projectBase, language);
140
+ void projectBase;
141
+ const payload = readGlobalConfigJson();
142
+ const staticCheck = (payload["static-check"] ?? {}) as Record<string, Record<string, unknown>>;
143
+ const languageConfig = staticCheck[language];
139
144
  return Array.isArray(languageConfig?.checkers)
140
145
  ? languageConfig.checkers as Array<Record<string, unknown>>
141
146
  : [];
@@ -594,6 +599,7 @@ const TARGET_CASES: TargetParityCase[] = [
594
599
  fs.mkdirSync(path.join(projectBase, "src"), { recursive: true });
595
600
  fs.mkdirSync(path.join(projectBase, "tests"), { recursive: true });
596
601
  fs.mkdirSync(path.join(projectBase, ...DEFAULT_DOCS_DIR.split("/")), { recursive: true });
602
+ const originalGlobalConfig = structuredClone(readGlobalConfigJson());
597
603
  const result = runNodeCli(
598
604
  ["--base", projectBase, "--enable-static-check", "C=Command,nonexistent_tool_xyz_12345"],
599
605
  projectBase,
@@ -601,6 +607,7 @@ const TARGET_CASES: TargetParityCase[] = [
601
607
  assert.equal(result.status, 1);
602
608
  assert.match(result.stderr, /not an executable program/);
603
609
  assert.ok(!fs.existsSync(getProjectConfigPath(projectBase)));
610
+ assert.deepEqual(readGlobalConfigJson(), originalGlobalConfig);
604
611
 
605
612
  const removedModule = runNodeCli(
606
613
  ["--base", projectBase, "--enable-static-check", "Python=Ruff"],
@@ -609,6 +616,7 @@ const TARGET_CASES: TargetParityCase[] = [
609
616
  assert.equal(removedModule.status, 1);
610
617
  assert.match(removedModule.stderr, /unknown module/i);
611
618
  assert.ok(!fs.existsSync(getProjectConfigPath(projectBase)));
619
+ assert.deepEqual(readGlobalConfigJson(), originalGlobalConfig);
612
620
  },
613
621
  },
614
622
  {
@@ -629,13 +637,18 @@ const TARGET_CASES: TargetParityCase[] = [
629
637
  projectBase,
630
638
  );
631
639
  assert.equal(persisted.status, 0, persisted.stderr);
640
+ const defaultConfig = getDefaultConfig(projectBase);
632
641
  assert.equal(getStaticCheckLanguageConfig(projectBase, "Python")?.enabled, "enable");
633
642
  assert.equal(getStaticCheckLanguageConfig(projectBase, "C")?.enabled, "enable");
634
643
  assert.deepEqual(getStaticCheckEntries(projectBase, "Python"), [
644
+ ...defaultConfig["static-check"].Python.checkers,
635
645
  { module: "Command", cmd: "git", params: ["--version"] },
636
646
  { module: "Command", cmd: "git", params: ["--help"] },
637
647
  ]);
638
- assert.deepEqual(getStaticCheckEntries(projectBase, "C"), [{ module: "Command", cmd: "git", params: ["--version"] }]);
648
+ assert.deepEqual(getStaticCheckEntries(projectBase, "C"), [
649
+ ...defaultConfig["static-check"].C.checkers,
650
+ { module: "Command", cmd: "git", params: ["--version"] },
651
+ ]);
639
652
  },
640
653
  },
641
654
  {
@@ -658,10 +671,15 @@ const TARGET_CASES: TargetParityCase[] = [
658
671
  projectBase,
659
672
  );
660
673
  assert.equal(result.status, 0, result.stderr);
674
+ const defaultConfig = getDefaultConfig(projectBase);
661
675
  assert.equal(getStaticCheckLanguageConfig(projectBase, "Python")?.enabled, "enable");
662
676
  assert.equal(getStaticCheckLanguageConfig(projectBase, "C")?.enabled, "enable");
663
- assert.deepEqual(getStaticCheckEntries(projectBase, "Python"), [{ module: "Command", cmd: "git", params: ["--version"] }]);
677
+ assert.deepEqual(getStaticCheckEntries(projectBase, "Python"), [
678
+ ...defaultConfig["static-check"].Python.checkers,
679
+ { module: "Command", cmd: "git", params: ["--version"] },
680
+ ]);
664
681
  assert.deepEqual(getStaticCheckEntries(projectBase, "C"), [
682
+ ...defaultConfig["static-check"].C.checkers,
665
683
  { module: "Command", cmd: "git", params: ["--version"] },
666
684
  { module: "Command", cmd: "git", params: ["--help"] },
667
685
  ]);
@@ -12,22 +12,21 @@ import {
12
12
  type OfflineContractSnapshot,
13
13
  } from "../scripts/lib/extension-debug-harness.js";
14
14
  import { buildParityReport, type SdkContractSnapshot } from "../scripts/lib/sdk-smoke.js";
15
- import { getProjectConfigPath } from "../src/core/config.js";
15
+ import { loadConfig } from "../src/core/config.js";
16
16
  import { PI_USEREQ_STATUS_HOOK_NAMES } from "../src/core/extension-status.js";
17
- import { initFixtureRepo } from "./helpers.js";
17
+ import { initFixtureRepo, saveFixtureConfigs } from "./helpers.js";
18
18
 
19
19
  /**
20
20
  * @brief Persists a targeted enabled-tool list into a fixture project config.
21
- * @details Loads `.pi-usereq.json`, replaces the `enabled-tools` array with the supplied values, and writes the updated JSON back to disk with a trailing newline. Runtime is O(n) in config size. Side effects include filesystem reads and file overwrite.
21
+ * @details Loads the effective split config, replaces the global `enabled-tools` array with the supplied values, and rewrites the local/global plus oracle mirror files so offline harness tests observe the same runtime state. Runtime is O(n) in config size. Side effects include filesystem reads and file overwrite.
22
22
  * @param[in] projectBase {string} Fixture project root.
23
23
  * @param[in] enabledTools {string[]} Enabled-tool names to persist.
24
24
  * @return {void} No return value.
25
25
  */
26
26
  function writeEnabledTools(projectBase: string, enabledTools: string[]): void {
27
- const configPath = getProjectConfigPath(projectBase);
28
- const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
27
+ const config = loadConfig(projectBase);
29
28
  config["enabled-tools"] = enabledTools;
30
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
29
+ saveFixtureConfigs(projectBase, config);
31
30
  }
32
31
 
33
32
  /**
@@ -257,10 +256,9 @@ test("replaySessionStart captures active tools, statuses, and cwd semantics", as
257
256
  test("replayCommand captures prompt command payloads", async () => {
258
257
  const { projectBase } = initFixtureRepo();
259
258
  try {
260
- const configPath = getProjectConfigPath(projectBase);
261
- const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
259
+ const config = loadConfig(projectBase);
262
260
  config.GIT_WORKTREE_ENABLED = "disable";
263
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
261
+ saveFixtureConfigs(projectBase, config);
264
262
  assert.equal(spawnSync("git", ["add", ".pi-usereq.json", ".req/config.json"], {
265
263
  cwd: projectBase,
266
264
  encoding: "utf8",