@mrclrchtr/supi-review 1.9.1 → 1.11.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.
Files changed (45) hide show
  1. package/README.md +49 -11
  2. package/node_modules/@mrclrchtr/supi-core/README.md +103 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +59 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +32 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +182 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +206 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/config.ts +11 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/context.ts +16 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/index.ts +30 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
  17. package/{src/ui → node_modules/@mrclrchtr/supi-core/src}/progress-widget.ts +32 -10
  18. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  19. package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
  20. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +86 -0
  21. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  22. package/node_modules/@mrclrchtr/supi-core/src/session.ts +4 -0
  23. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
  24. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +41 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +226 -0
  26. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +2 -0
  27. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +9 -0
  28. package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +11 -0
  29. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  30. package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +182 -0
  31. package/node_modules/@mrclrchtr/supi-core/src/types.ts +2 -0
  32. package/package.json +7 -1
  33. package/src/history/synthesize.ts +13 -0
  34. package/src/review-result.ts +98 -0
  35. package/src/review.ts +90 -147
  36. package/src/target/packet.ts +81 -17
  37. package/src/target/review-instruction-blocks.ts +60 -0
  38. package/src/tool/brief-runner.ts +1 -1
  39. package/src/tool/review-runner.ts +152 -34
  40. package/src/tool/schemas.ts +55 -12
  41. package/src/types.ts +81 -9
  42. package/src/ui/flow.ts +10 -166
  43. package/src/ui/format-content.ts +81 -33
  44. package/src/ui/renderer.ts +103 -51
  45. package/src/ui/review-plan-inspector.ts +396 -0
package/README.md CHANGED
@@ -1,4 +1,10 @@
1
- ![SuPi](assets/logo.png)
1
+ <div align="center">
2
+ <a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-review">
3
+ <picture>
4
+ <img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-review/assets/logo.png" alt="SuPi" width="50%">
5
+ </picture>
6
+ </a>
7
+ </div>
2
8
 
3
9
  # @mrclrchtr/supi-review
4
10
 
@@ -27,7 +33,7 @@ After install, pi gets one command:
27
33
  The reviewer runs in managed child agent sessions:
28
34
 
29
35
  - a **brief synthesizer** creates a structured review brief from the active session branch
30
- - a **read-only reviewer** inspects the selected snapshot (without receiving bulk inline diffs) and submits structured findings
36
+ - a **read-only reviewer** inspects the selected snapshot (without receiving bulk inline diffs) and submits structured review items
31
37
 
32
38
  ![Review target selection](https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-review-1.png)
33
39
 
@@ -46,10 +52,10 @@ The reviewer runs in managed child agent sessions:
46
52
  3. optionally add a short note
47
53
  4. resolve the snapshot
48
54
  5. synthesize a review brief from the current session history
49
- 6. preview the synthesized brief + compact prompt preview
55
+ 6. preview the synthesized brief + compact prompt preview, then press `v` for an in-app inspector (Overview first, Raw Prompt via `tab`, export via `e`)
50
56
  7. the reviewer fetches per-file diffs on demand via snapshot-aware tools; live progress widget shows activity
51
- 8. show the structured result as a custom message
52
- 9. if findings exist, hand off to the main agent so it can ask what to do next
57
+ 8. normalize the submitted review items into a host-derived verdict + structured result
58
+ 9. if review items exist, hand off to the main agent so it can ask what to do next with fixed options (`Fix all`, `Fix selected`, `Verify findings`, `Skip`)
53
59
 
54
60
  ## Review targets
55
61
 
@@ -75,13 +81,27 @@ Before the actual review starts, the package:
75
81
  - focus areas
76
82
  - risky files
77
83
  - unresolved questions
84
+ - `reviewInstructionBlockIds` selected from a fixed host-owned catalog
78
85
 
79
86
  The synthesizer also receives a bounded diff excerpt from the snapshot so it can reason about actual code changes, not just filenames.
80
87
 
81
- That synthesized brief is then combined with the git snapshot into a compact reviewer prompt. The prompt contains the brief, file manifest, and per-file overview, but no large inline diffs. Instead, the reviewer session gets snapshot-aware tools (`read_snapshot_diff`, `read_snapshot_file`) to fetch exact per-file diffs and before/after file contents on demand.
88
+ That synthesized brief is then combined with the git snapshot into a compact reviewer prompt. The host owns a fixed catalog of review instruction blocks, and the brief selects zero or more block IDs from that catalog when extra review guidance is warranted. The resulting prompt contains the brief, file manifest, per-file overview, and any brief-selected **mandatory review instructions**, but no large inline diffs. Instead, the reviewer session gets snapshot-aware tools (`read_snapshot_diff`, `read_snapshot_file`) to fetch exact per-file diffs and before/after file contents on demand.
82
89
 
83
90
  The session-transcript approach mirrors how Pi summarizes context for compaction: the entire resolved conversation is rendered in a readable label format and sent to the model as a whole, rather than relying on heuristic excerpt ranking.
84
91
 
92
+ ## Review-plan inspector
93
+
94
+ Before the reviewer runs, the plan preview stays inside Pi:
95
+
96
+ - `v` opens an in-app inspector instead of spawning an external pager
97
+ - the inspector opens in **Overview** mode first
98
+ - `tab` toggles between **Overview** and **Raw Prompt**
99
+ - `↑↓` or `j` / `k` scroll long content in the inspector
100
+ - `q` or `esc` returns to the summary preview without canceling the review
101
+ - `e` exports the raw prompt to a temp file as a debugging fallback
102
+
103
+ The Overview mode uses the same structured packet data that feeds the reviewer prompt: mandatory review instructions, file overview rows, and truncated snapshot notes all come from shared packet derivation rather than re-parsing the raw prompt text.
104
+
85
105
  ## Model selection
86
106
 
87
107
  Every `/supi-review` run asks you to choose the reviewer model.
@@ -95,20 +115,35 @@ Every `/supi-review` run asks you to choose the reviewer model.
95
115
 
96
116
  A successful review includes:
97
117
 
98
- - overall correctness verdict
118
+ - a host-derived binary verdict:
119
+ - `PATCH IS CORRECT`
120
+ - `PATCH HAS ISSUES`
99
121
  - overall explanation
100
122
  - overall confidence score
101
- - structured findings with title, body, priority, confidence score, and code location
123
+ - normalized action/category summary counts
124
+ - structured review items with:
125
+ - title
126
+ - body
127
+ - category
128
+ - impact
129
+ - effort
130
+ - recommended action
131
+ - confidence score
132
+ - suggested fix
133
+ - verification hint
134
+ - optional code location
102
135
  - the synthesized brief that drove the review
103
136
 
104
137
  The renderer also handles failed, canceled, and timed-out reviews.
105
138
 
106
- When a successful review contains findings, `supi-review` also injects an agent-visible hidden follow-up message that asks the main agent to decide the next step with the user. If `ask_user` is available, the main agent is instructed to use it and offer:
139
+ The reviewer model does **not** decide the final binary verdict directly. It submits review items plus overall explanation/confidence, then the host derives the verdict from the normalized items (`must-fix` items => `PATCH HAS ISSUES`).
140
+
141
+ When a successful review contains review items, `supi-review` also injects an agent-visible hidden follow-up message that asks the main agent to decide the next step with the user. If `ask_user` is available, the main agent is instructed to use it and offer:
107
142
 
108
- - Done
109
143
  - Fix all
110
144
  - Fix selected
111
145
  - Verify findings
146
+ - Skip
112
147
 
113
148
  ## Source
114
149
 
@@ -117,8 +152,11 @@ When a successful review contains findings, `supi-review` also injects an agent-
117
152
  - `src/git.ts` — git snapshot resolution
118
153
  - `src/history/collect.ts` — compaction-style session-context serialization
119
154
  - `src/history/synthesize.ts` — brief synthesis orchestration
120
- - `src/target/packet.ts` — final reviewer packet builder
155
+ - `src/review-result.ts` — review-item normalization, verdict derivation, and summary counts
156
+ - `src/target/review-instruction-blocks.ts` — fixed catalog of host-owned review instruction blocks
157
+ - `src/target/packet.ts` — final reviewer packet builder + shared preview-data derivation for the inspector
121
158
  - `src/tool/brief-runner.ts` — brief synthesis child session
122
159
  - `src/tool/review-runner.ts` — read-only reviewer child session with snapshot-aware tools
123
160
  - `src/tool/snapshot-tools.ts` — per-file diff and before/after content tools scoped to the selected snapshot
161
+ - `src/ui/review-plan-inspector.ts` — in-app summary/inspector preview with Overview + Raw Prompt modes and export fallback
124
162
  - `src/ui/renderer.ts` — structured result rendering
@@ -0,0 +1,103 @@
1
+ <div align="center">
2
+ <a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-core">
3
+ <picture>
4
+ <img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/logo.png" alt="SuPi" width="50%">
5
+ </picture>
6
+ </a>
7
+ </div>
8
+
9
+ # @mrclrchtr/supi-core
10
+
11
+ Shared infrastructure for SuPi extensions.
12
+
13
+ This is a **pure library** — it does not register any pi commands or tools. The `/supi-settings` command is now available through `@mrclrchtr/supi-settings`.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @mrclrchtr/supi-core
19
+ ```
20
+
21
+ ## Package surfaces
22
+
23
+ - `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
24
+
25
+ ## What you get from the API
26
+
27
+ ### Config helpers
28
+
29
+ - `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
30
+ - `loadSupiConfigForScope()` — load one scope at a time for settings UIs
31
+ - `writeSupiConfig()` — persist values
32
+ - `removeSupiConfigKey()` — remove a key or override
33
+
34
+ Config file locations:
35
+
36
+ - global: `~/.pi/agent/supi/config.json`
37
+ - project: `.pi/supi/config.json`
38
+
39
+ ### Settings helpers
40
+
41
+ - `registerSettings()` — register an arbitrary settings section
42
+ - `registerConfigSettings()` — register a config-backed settings section with scoped persistence helpers
43
+ - `registerSettingsCommand()` — register `/supi-settings`
44
+ - `openSettingsOverlay()` — open the shared settings UI directly
45
+ - `createInputSubmenu()` — helper for simple text-entry submenus
46
+
47
+ The built-in settings UI supports:
48
+
49
+ - project/global scope toggle
50
+ - grouped extension sections
51
+ - searchable setting lists
52
+
53
+ ### Context helpers
54
+
55
+ - `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
56
+ - `findLastUserMessageIndex()`
57
+ - `getContextToken()`
58
+ - `getPromptContent()`
59
+ - `pruneAndReorderContextMessages()`
60
+ - `restorePromptContent()`
61
+
62
+ ### Shared registries
63
+
64
+ - context-provider registry for `/supi-context`
65
+ - debug-event registry for producers that want shared debug capture
66
+ - settings registry used by `/supi-settings`
67
+
68
+ ### Project and session helpers
69
+
70
+ - project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
71
+ - active-branch session helper: `getActiveBranchEntries()`
72
+ - terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
73
+
74
+ ## Example
75
+
76
+ ```ts
77
+ import { loadSupiConfig, registerConfigSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
78
+
79
+ const config = loadSupiConfig("my-extension", process.cwd(), {
80
+ enabled: true,
81
+ });
82
+
83
+ registerConfigSettings({
84
+ id: "my-extension",
85
+ label: "My Extension",
86
+ section: "my-extension",
87
+ defaults: { enabled: true },
88
+ buildItems: () => [],
89
+ persistChange: () => {},
90
+ });
91
+
92
+ const message = wrapExtensionContext("my-extension", "hello", {
93
+ file: "CLAUDE.md",
94
+ turn: 1,
95
+ });
96
+ ```
97
+
98
+ ## Source
99
+
100
+ - `src/api.ts` — exported library surface
101
+ - `src/config.ts` — shared config loading and writing
102
+ - `src/config-settings.ts` — config-backed settings registration helper
103
+ - `src/settings-ui.ts` — shared settings overlay
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@mrclrchtr/supi-core",
3
+ "version": "1.11.0",
4
+ "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mrclrchtr/supi.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "keywords": [
14
+ "pi",
15
+ "pi-coding-agent"
16
+ ],
17
+ "files": [
18
+ "src/**/*.ts",
19
+ "!__tests__"
20
+ ],
21
+ "peerDependencies": {
22
+ "@earendil-works/pi-ai": "*",
23
+ "@earendil-works/pi-coding-agent": "*",
24
+ "@earendil-works/pi-tui": "*",
25
+ "typebox": "*"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "@earendil-works/pi-ai": {
29
+ "optional": true
30
+ },
31
+ "@earendil-works/pi-coding-agent": {
32
+ "optional": true
33
+ },
34
+ "@earendil-works/pi-tui": {
35
+ "optional": true
36
+ },
37
+ "typebox": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "main": "src/api.ts",
42
+ "exports": {
43
+ "./api": "./src/api.ts",
44
+ "./config": "./src/config.ts",
45
+ "./context": "./src/context.ts",
46
+ "./debug": "./src/debug-registry.ts",
47
+ "./llm": "./src/llm.ts",
48
+ "./package.json": "./package.json",
49
+ "./path": "./src/path.ts",
50
+ "./progress-widget": "./src/progress-widget.ts",
51
+ "./project": "./src/project.ts",
52
+ "./session": "./src/session.ts",
53
+ "./settings": "./src/settings.ts",
54
+ "./settings-ui": "./src/settings-ui.ts",
55
+ "./terminal": "./src/terminal.ts",
56
+ "./tool-framework": "./src/tool-framework.ts",
57
+ "./types": "./src/types.ts"
58
+ }
59
+ }
@@ -0,0 +1,32 @@
1
+ // supi-core — shared infrastructure for SuPi extensions.
2
+ // Provides XML context tag wrapping, unified config system, context-message utilities,
3
+ // settings registry for supi-wide TUI settings, and a shared tool-spec/registration framework.
4
+ //
5
+ // Convenience barrel — re-exports all domain entry points.
6
+ // For lighter imports, use one of the domain subpaths directly
7
+ // (e.g. @mrclrchtr/supi-core/config, @mrclrchtr/supi-core/context).
8
+
9
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
10
+ export * from "./config.ts";
11
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
+ export * from "./context.ts";
13
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
+ export * from "./debug-registry.ts";
15
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
+ export * from "./llm.ts";
17
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
18
+ export * from "./path.ts";
19
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
20
+ export * from "./project.ts";
21
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
22
+ export * from "./session.ts";
23
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
24
+ export * from "./settings.ts";
25
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
26
+ export * from "./settings-ui.ts";
27
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
28
+ export * from "./terminal.ts";
29
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
30
+ export * from "./tool-framework.ts";
31
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
32
+ export * from "./types.ts";
@@ -0,0 +1,182 @@
1
+ // Config-aware settings helper for SuPi config-backed settings sections.
2
+ // Wraps registerSettings() and centralizes selected-scope loading + scoped persistence.
3
+ //
4
+ // Setting items can declare a `configType` ("boolean" | "number" | "stringList")
5
+ // to enable auto-generated persistChange. When all items have a configType,
6
+ // the persistChange callback can be omitted.
7
+
8
+ import type { SettingItem } from "@earendil-works/pi-tui";
9
+ import type { SettingsScope } from "../settings/settings-registry.ts";
10
+ import { registerSettings } from "../settings/settings-registry.ts";
11
+ import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./config.ts";
12
+
13
+ // ── Types ──────────────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Supported config value types for declarative persistChange.
17
+ *
18
+ * - `"boolean"`: maps "on" → true, "off" → false
19
+ * - `"number"`: parses integer via Number.parseInt, falls back to unset on invalid
20
+ * - `"stringList"`: splits on comma, trims whitespace, unsets on empty
21
+ */
22
+ export type ConfigSettingType = "boolean" | "number" | "stringList";
23
+
24
+ /**
25
+ * Extended setting item that can declare its config type for auto-generated
26
+ * persistence handling.
27
+ */
28
+ export interface ConfigSettingItem extends SettingItem {
29
+ /**
30
+ * When set, persistChange for this item is auto-generated.
31
+ * All items must declare a configType for auto-generation to activate.
32
+ */
33
+ configType?: ConfigSettingType;
34
+ }
35
+
36
+ /**
37
+ * Helpers provided to the persistChange callback for writing or removing
38
+ * scoped config values.
39
+ */
40
+ export interface ConfigSettingsHelpers {
41
+ /** Write a key to the selected scope's config section. */
42
+ set(key: string, value: unknown): void;
43
+ /** Remove a key from the selected scope's config section. */
44
+ unset(key: string): void;
45
+ }
46
+
47
+ export interface ConfigSettingsOptions<T> {
48
+ /** Extension identifier — e.g. "lsp", "claude-md" */
49
+ id: string;
50
+ /** Human-readable label shown in the UI */
51
+ label: string;
52
+ /** SuPi config section name — e.g. "lsp", "claude-md" */
53
+ section: string;
54
+ /** Default config values */
55
+ defaults: T;
56
+ /**
57
+ * Build SettingItem[] from scoped config. Called by loadValues.
58
+ *
59
+ * Items can include a `configType` property for auto-generated
60
+ * persistChange handling. When ALL items declare a configType,
61
+ * the `persistChange` callback can be omitted.
62
+ */
63
+ buildItems: (settings: T, scope: SettingsScope, cwd: string) => ConfigSettingItem[];
64
+ /**
65
+ * Handle a settings change with scoped persistence helpers.
66
+ *
67
+ * Optional when all items returned by `buildItems` declare a `configType`.
68
+ * Required when any item lacks a `configType`.
69
+ */
70
+ persistChange?: (
71
+ scope: SettingsScope,
72
+ cwd: string,
73
+ settingId: string,
74
+ value: string,
75
+ helpers: ConfigSettingsHelpers,
76
+ ) => void;
77
+ /** Optional home directory for config resolution (testing). */
78
+ homeDir?: string;
79
+ }
80
+
81
+ // ── Auto-generated persistChange ───────────────────────────────────────────
82
+
83
+ function autoPersistChange(
84
+ settingId: string,
85
+ value: string,
86
+ helpers: ConfigSettingsHelpers,
87
+ items: ConfigSettingItem[],
88
+ ): void {
89
+ const item = items.find((i) => i.id === settingId);
90
+ if (!item?.configType) return;
91
+
92
+ switch (item.configType) {
93
+ case "boolean": {
94
+ helpers.set(settingId, value === "on");
95
+ break;
96
+ }
97
+ case "number": {
98
+ const num = Number.parseInt(value, 10);
99
+ if (Number.isFinite(num) && num > 0) {
100
+ helpers.set(settingId, num);
101
+ } else {
102
+ helpers.unset(settingId);
103
+ }
104
+ break;
105
+ }
106
+ case "stringList": {
107
+ const names = value
108
+ .split(",")
109
+ .map((s) => s.trim())
110
+ .filter((s) => s.length > 0);
111
+ if (names.length > 0) {
112
+ helpers.set(settingId, names);
113
+ } else {
114
+ helpers.unset(settingId);
115
+ }
116
+ break;
117
+ }
118
+ }
119
+ }
120
+
121
+ function areAllItemsDeclarative(items: ConfigSettingItem[]): boolean {
122
+ return items.length > 0 && items.every((i) => i.configType !== undefined);
123
+ }
124
+
125
+ // ── Registration ───────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Register a config-backed settings section.
129
+ *
130
+ * Loads display values from the selected scope only (`defaults <- selected scope`)
131
+ * instead of merged effective runtime config. Provides scoped `set` / `unset`
132
+ * persistence helpers so extensions don't need to wire `writeSupiConfig` /
133
+ * `removeSupiConfigKey` by hand.
134
+ *
135
+ * When every item returned by `buildItems` declares a `configType`, the
136
+ * `persistChange` callback is optional and will be auto-generated.
137
+ */
138
+ export function registerConfigSettings<T>(options: ConfigSettingsOptions<T>): void {
139
+ let cachedItems: ConfigSettingItem[] | undefined;
140
+
141
+ registerSettings({
142
+ id: options.id,
143
+ label: options.label,
144
+ loadValues: (scope, cwd) => {
145
+ const settings = loadSupiConfigForScope(options.section, cwd, options.defaults, {
146
+ scope,
147
+ homeDir: options.homeDir,
148
+ });
149
+ const items = options.buildItems(settings, scope, cwd);
150
+ cachedItems = items;
151
+ return items;
152
+ },
153
+ persistChange: (scope, cwd, settingId, value) => {
154
+ const helpers: ConfigSettingsHelpers = {
155
+ set: (key, val) => {
156
+ writeSupiConfig(
157
+ { section: options.section, scope, cwd },
158
+ { [key]: val },
159
+ { homeDir: options.homeDir },
160
+ );
161
+ },
162
+ unset: (key) => {
163
+ removeSupiConfigKey({ section: options.section, scope, cwd }, key, {
164
+ homeDir: options.homeDir,
165
+ });
166
+ },
167
+ };
168
+
169
+ // Use manual persistChange when provided
170
+ if (options.persistChange) {
171
+ options.persistChange(scope, cwd, settingId, value, helpers);
172
+ return;
173
+ }
174
+
175
+ // Auto-generate when all items are declarative
176
+ const items = cachedItems ?? options.buildItems(options.defaults, scope, cwd);
177
+ if (areAllItemsDeclarative(items)) {
178
+ autoPersistChange(settingId, value, helpers, items);
179
+ }
180
+ },
181
+ });
182
+ }