@mrclrchtr/supi-review 2.7.0 → 3.0.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 (49) hide show
  1. package/README.md +75 -163
  2. package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
  9. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  10. package/package.json +3 -3
  11. package/src/config.ts +64 -0
  12. package/src/git-command.ts +78 -0
  13. package/src/git.ts +306 -505
  14. package/src/history/collect.ts +43 -120
  15. package/src/model.ts +35 -0
  16. package/src/review-path.ts +85 -0
  17. package/src/review-result.ts +43 -92
  18. package/src/review.ts +154 -288
  19. package/src/session/review-plan-store.ts +20 -51
  20. package/src/target/packet.ts +46 -369
  21. package/src/tool/agent-review-schemas.ts +101 -104
  22. package/src/tool/agent-review-tools.ts +269 -301
  23. package/src/tool/child-failure-diagnostics.ts +1 -1
  24. package/src/tool/child-resource-loader.ts +37 -0
  25. package/src/tool/child-session-runner.ts +83 -0
  26. package/src/tool/output-page.ts +47 -0
  27. package/src/tool/planner-runner.ts +69 -0
  28. package/src/tool/review-runner.ts +42 -257
  29. package/src/tool/review-system-prompt.ts +12 -110
  30. package/src/tool/review-tools.ts +119 -0
  31. package/src/tool/review-workflow.ts +309 -0
  32. package/src/tool/runner-helpers.ts +3 -8
  33. package/src/tool/schemas.ts +75 -62
  34. package/src/tui/common.ts +175 -0
  35. package/src/tui/prepare.ts +132 -0
  36. package/src/tui/run.ts +160 -0
  37. package/src/types.ts +153 -275
  38. package/src/history/synthesize.ts +0 -121
  39. package/src/tool/agent-review-workflow.ts +0 -398
  40. package/src/tool/brief-runner.ts +0 -180
  41. package/src/tool/guidance.ts +0 -21
  42. package/src/tool/review-handlers.ts +0 -314
  43. package/src/tool/snapshot-tools.ts +0 -117
  44. package/src/ui/flow.ts +0 -189
  45. package/src/ui/format-content.ts +0 -143
  46. package/src/ui/renderer.ts +0 -274
  47. package/src/ui/review-plan-inspector.ts +0 -391
  48. package/src/ui/review-tool-format.ts +0 -138
  49. package/src/ui/review-tool-renderer.ts +0 -234
package/README.md CHANGED
@@ -1,14 +1,6 @@
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>
8
-
9
1
  # @mrclrchtr/supi-review
10
2
 
11
- Adds human-driven and agent-driven Session-Aware Review workflows to the [pi coding agent](https://github.com/earendil-works/pi).
3
+ Runs caller-defined, read-only code review tasks in managed Pi child sessions.
12
4
 
13
5
  ## Install
14
6
 
@@ -16,177 +8,97 @@ Adds human-driven and agent-driven Session-Aware Review workflows to the [pi cod
16
8
  pi install npm:@mrclrchtr/supi-review
17
9
  ```
18
10
 
19
- This is a **beta** package. Install individually.
20
-
21
- For local development:
22
-
23
- ```bash
24
- pi install ./packages/supi-review
11
+ This is a beta package with intentionally unstable interfaces.
12
+
13
+ ## Surfaces
14
+
15
+ - `/supi-review` — interactive Direct or Planner-assisted review
16
+ - `supi_review_prepare` — optional one-shot preparation with `planning: "none" | "suggest"`
17
+ - `supi_review_run` — universal Direct or Prepared Review execution
18
+
19
+ Preparation is optional. Skills and agents that already know how to review should use Direct Review.
20
+
21
+ ## Direct Review
22
+
23
+ ```json
24
+ {
25
+ "mode": "direct",
26
+ "target": {
27
+ "kind": "comparison",
28
+ "baseCommit": "<full commit object id>"
29
+ },
30
+ "review": {
31
+ "sharedContext": "The change implements issue #123.",
32
+ "tasks": [
33
+ {
34
+ "id": "standards",
35
+ "instructions": "Review against the repository standards."
36
+ },
37
+ {
38
+ "id": "spec",
39
+ "instructions": "Review against issue #123 and cite unmet requirements."
40
+ }
41
+ ]
42
+ }
43
+ }
25
44
  ```
26
45
 
27
- ## What you get
28
-
29
- After install, pi gets one human-driven command and two agent-facing tools:
30
-
31
- - `/supi-review` — launch a guided review flow over a concrete git snapshot
32
- - `supi_review_prepare` — synthesize a versioned brief and return a session-scoped `planId`
33
- - `supi_review_run` — accept the main agent's structured brief critique, freshness-check the plan, and run focused reviewers concurrently
34
-
35
- The run tool is initially inactive and is dynamically enabled after `supi_review_prepare` returns a plan. This keeps the initial agent-facing prompt surface small.
36
-
37
- The reviewer runs in managed child agent sessions:
38
-
39
- - a **brief synthesizer** creates a structured review brief from the active session branch
40
- - a **read-only reviewer** inspects the selected snapshot (without receiving bulk inline diffs) and submits structured review items
41
-
42
- ![Review target selection](https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-review-1.png)
43
-
44
- ![Review brief preview](https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-review-2.png)
45
-
46
- ![Review result](https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-review-3.png)
47
-
48
- ![Review progress](https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-review-4.png)
49
-
50
- ## Review flow
51
-
52
- `/supi-review` walks you through:
53
-
54
- 1. choose a review target
55
- 2. choose the reviewer model
56
- 3. optionally add a short note
57
- 4. resolve the snapshot
58
- 5. synthesize a review brief from the current session history
59
- 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`)
60
- 7. the reviewer fetches per-file diffs on demand via snapshot-aware tools; live progress widget shows activity
61
- 8. normalize the submitted review items into a host-derived verdict + structured result
62
- 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`)
46
+ All tasks use one reviewer model and run concurrently. A run accepts one to four tasks. Direct and Prepared adapters use the same canonical packet compiler; every task result includes the SHA-256 of the exact packet bytes.
63
47
 
64
- ## Agent-driven review flow
48
+ ## Prepared Review
65
49
 
66
- The agent-facing path deliberately separates preparation from execution:
67
-
68
- 1. `supi_review_prepare` resolves the target, serializes the active session context, and synthesizes a generated brief.
69
- 2. The main agent compares that brief with the user request, session evidence, and snapshot.
70
- 3. The main agent calls `supi_review_run` with:
71
- - the prepared `planId`
72
- - an `accept` or `revise` critique
73
- - evidence-backed critique findings
74
- - at least one evidence-backed finding and a full `revisedBrief` when revision is required
75
- - one to four independent reviewer assignments
76
- 4. The host atomically consumes the plan, checks snapshot freshness, and runs reviewer child sessions concurrently.
77
- 5. The tool returns each normalized review result plus a retained brief evaluation artifact.
78
-
79
- The evaluation artifact keeps the generated brief, main-agent critique, effective brief, synthesis prompt version, model id, and snapshot fingerprint separate. This makes brief-synthesis prompt regressions inspectable instead of hiding main-agent repairs. The full artifact is stored in tool-result `details`; a summary is also recorded as a `supi-review/brief-critique` SuPi Debug event when debug capture is enabled.
80
-
81
- Prepared plans are session-scoped. File or git changes before or during review invalidate the run, so `supi_review_run` should not share a tool batch with mutating tools.
82
-
83
- ## Review targets
84
-
85
- Current targets:
86
-
87
- - working tree
88
- - branch diff vs a selected local base branch (`merge-base..HEAD`, excluding dirty worktree changes)
89
- - one commit (recent-commit picker in the command; hexadecimal object id in the agent tool)
90
-
91
- Agent-provided branch targets must name an existing local branch. Commit targets must be unique 7–64 character hexadecimal commit-object ids: object-only disambiguation rejects ambiguous prefixes, non-commit objects, and hexadecimal branch/tag fallback. Git revision arguments are option-hardened before execution.
92
-
93
- ## Session-aware brief synthesis
94
-
95
- The generated review prompt is **not** just a static diff wrapper.
96
-
97
- Before the actual review starts, the package:
98
-
99
- - resolves the **active session branch into the current LLM-visible context**
100
- - **serializes** that resolved context into a compaction-style transcript
101
- - feeds the serialized transcript (plus snapshot + optional note) to a dedicated brief synthesizer
102
- - synthesizes a structured brief with:
103
- - summary
104
- - intended outcome
105
- - constraints to preserve
106
- - focus areas
107
- - risky files
108
- - unresolved questions
109
- - `reviewInstructionBlockIds` selected from a fixed host-owned catalog
110
-
111
- The synthesizer also receives a bounded diff excerpt from the snapshot so it can reason about actual code changes, not just filenames.
112
-
113
- 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.
114
-
115
- 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.
50
+ ```json
51
+ {
52
+ "target": { "kind": "working-tree" },
53
+ "planning": "suggest"
54
+ }
55
+ ```
116
56
 
117
- ## Review-plan inspector
57
+ Preparation returns a session-scoped, one-shot `planId` and optional Planner Draft. Execute it with an explicit decision:
118
58
 
119
- Before the reviewer runs, the plan preview stays inside Pi:
59
+ ```json
60
+ {
61
+ "mode": "prepared",
62
+ "planId": "review-plan-...",
63
+ "decision": { "kind": "accept-draft" }
64
+ }
65
+ ```
120
66
 
121
- - `v` opens an in-app inspector instead of spawning an external pager
122
- - the inspector opens in **Overview** mode first
123
- - `tab` toggles between **Overview** and **Raw Prompt**
124
- - `↑↓` or `j` / `k` scroll long content in the inspector
125
- - `q` or `esc` returns to the summary preview without canceling the review
126
- - `e` exports the raw prompt to a temp file as a debugging fallback
67
+ Use `use-review` with a complete replacement review when the draft needs editing.
127
68
 
128
- 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.
69
+ ## Targets
129
70
 
130
- ## Model selection
71
+ - `working-tree` — net `HEAD` to current filesystem, plus non-ignored untracked files, computed through a temporary HEAD-seeded index rather than the caller's real index
72
+ - `comparison` — merge base of a full `baseCommit` and captured `HEAD` to captured `HEAD`
73
+ - `commit` — first parent (or empty tree) to a full commit object id
131
74
 
132
- Every `/supi-review` command run asks you to choose the reviewer model.
75
+ Agent-facing commit values must be full hexadecimal object ids. The repository must remain stable from preparation or Direct invocation until reviewers finish; drift is not detected.
133
76
 
134
- - the picker only shows **scoped models** from Pi's `enabledModels` configuration
135
- - the current session model is preselected only when it is inside that scoped set
136
- - the selected model is used for both brief synthesis and the final review
137
- - no review model is persisted in settings
77
+ ## Reviewer protocol
138
78
 
139
- Agent-driven tool runs use the current session model for preparation and all focused reviewers. The selected model is retained in the prepared plan so a model change between tool calls cannot silently alter the run.
79
+ Callers own task methodology. The Review Engine owns:
140
80
 
141
- ## Result shape
81
+ - target resolution
82
+ - target-aware read-only tools
83
+ - minimal finding-quality instructions
84
+ - structured submission
85
+ - child lifecycle handling
86
+ - per-task verdict derivation
142
87
 
143
- A successful review includes:
88
+ Reviewers receive no shell, arbitrary extension tools, Pi context files, skills, or prompt templates. Discovered `SYSTEM.md` and `APPEND_SYSTEM.md` files are explicitly suppressed; only the package-owned reviewer protocol is appended to Pi's base system prompt. In-memory child settings disable compaction and retries so provider limits apply to the original packet. Inspection-tool and parent-visible outputs are paged/bounded; canonical packet bytes are never truncated.
144
89
 
145
- - a host-derived binary verdict:
146
- - `PATCH IS CORRECT`
147
- - `PATCH HAS ISSUES`
148
- - overall explanation
149
- - overall confidence score
150
- - normalized action/category summary counts
151
- - structured review items with:
152
- - title
153
- - body
154
- - category
155
- - impact
156
- - effort
157
- - recommended action
158
- - confidence score
159
- - suggested fix
160
- - verification hint
161
- - optional code location
162
- - the synthesized brief that drove the review
90
+ ## Result
163
91
 
164
- The renderer also handles failed, canceled, and timed-out reviews.
92
+ Each successful task returns a summary and ordered findings. Findings contain:
165
93
 
166
- 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`).
94
+ - `blocksAcceptance`
95
+ - `impact`: `low | medium | high`
96
+ - `effort`: `small | medium | large`
97
+ - `confidence`: `0..1`
98
+ - optional target-relative location
167
99
 
168
- 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:
169
-
170
- - Fix all
171
- - Fix selected
172
- - Verify findings
173
- - Skip
100
+ The Review Engine derives `pass` or `issues` per task and never aggregates or reranks tasks. Batch details also record `mode`, caller/planner provenance, and each task's packet hash.
174
101
 
175
- ## Source
102
+ ## Models
176
103
 
177
- - `src/review.ts` command orchestration and interactive flow
178
- - `src/model.ts` — explicit model selection helpers
179
- - `src/git.ts` — git snapshot resolution
180
- - `src/history/collect.ts` — compaction-style session-context serialization
181
- - `src/history/synthesize.ts` — brief synthesis orchestration
182
- - `src/review-result.ts` — review-item normalization, verdict derivation, and summary counts
183
- - `src/target/review-instruction-blocks.ts` — fixed catalog of host-owned review instruction blocks
184
- - `src/target/packet.ts` — final reviewer packet builder + shared preview-data derivation for the inspector
185
- - `src/session/review-plan-store.ts` — session-scoped, one-shot prepared review plans
186
- - `src/tool/agent-review-tools.ts` — prepare/run tool registration, dynamic activation, progress, and debug artifact recording
187
- - `src/tool/agent-review-workflow.ts` — preparation, critique validation, freshness checks, and concurrent reviewer fan-out
188
- - `src/tool/brief-runner.ts` — brief synthesis child session
189
- - `src/tool/review-runner.ts` — read-only reviewer child session with snapshot-aware tools
190
- - `src/tool/snapshot-tools.ts` — per-file diff and before/after content tools scoped to the selected snapshot
191
- - `src/ui/review-plan-inspector.ts` — in-app summary/inspector preview with Overview + Raw Prompt modes and export fallback
192
- - `src/ui/renderer.ts` — structured result rendering
104
+ `/supi-review` asks for the reviewer model. Agent-triggered runs use `review.agentModel`. Optional planning uses the separately configured `review.plannerModel` at low thinking effort. Both settings default to the current session model for availability; configure `review.plannerModel` to a lightweight model when using Planner suggestions.
@@ -43,7 +43,7 @@ Config file locations:
43
43
  - `registerSettingsCommand(pi)` — register `/supi-settings` (used by `@mrclrchtr/supi-settings`)
44
44
  - `openSettingsOverlay(pi, ctx)` — open the shared settings UI directly
45
45
  - `createInputSubmenu()` — helper for simple text-entry submenus
46
- - `createModelPickerSubmenu()` — helper for scoped model selection submenus
46
+ - `createModelPickerSubmenu()` — helper for scoped model selection submenus, with optional host-owned choices and `disabled` control
47
47
 
48
48
  The built-in settings UI supports:
49
49
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "2.7.0",
3
+ "version": "3.0.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -55,6 +55,7 @@
55
55
  "./config": "./src/config.ts",
56
56
  "./context": "./src/context.ts",
57
57
  "./debug": "./src/debug-registry.ts",
58
+ "./evidence-badge": "./src/evidence-badge.ts",
58
59
  "./footer-registry": "./src/footer-registry.ts",
59
60
  "./llm": "./src/llm.ts",
60
61
  "./model-selection": "./src/model-selection.ts",
@@ -15,6 +15,8 @@ export * from "./context.ts";
15
15
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
16
  export * from "./debug-registry.ts";
17
17
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
18
+ export * from "./evidence-badge.ts";
19
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
18
20
  export * from "./footer-registry.ts";
19
21
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
20
22
  export * from "./llm.ts";
@@ -0,0 +1,40 @@
1
+ /**
2
+ * TUI-facing evidence badge string formatter.
3
+ *
4
+ * Pure string formatting — no pi-tui dependency. Consumes evidence
5
+ * completeness metadata and produces compact human-readable badges.
6
+ */
7
+
8
+ /** Metadata describing how many evidence atoms were shown vs exist. */
9
+ export interface EvidenceBadgeInput {
10
+ shownCount: number;
11
+ totalCount: number | null;
12
+ omittedCount: number | null;
13
+ partialReason: string | null;
14
+ /** Human-readable label for the badge, e.g. "references", "symbols". */
15
+ label: string;
16
+ }
17
+
18
+ /**
19
+ * Format a compact evidence completeness badge.
20
+ *
21
+ * | Input | Output |
22
+ * |------------------------------------------------------|---------------------------------------|
23
+ * | shown=12, total=12, omitted=0, label="references" | `12 references` |
24
+ * | shown=8, total=20, omitted=12, label="symbols" | `8 of 20 symbols (12 omitted)` |
25
+ * | shown=5, total=null, reason="timeout", label="matches" | `5 matches — timeout` |
26
+ */
27
+ export function formatEvidenceBadge(input: EvidenceBadgeInput): string {
28
+ const { shownCount, totalCount, omittedCount, partialReason, label } = input;
29
+
30
+ if (totalCount === null) {
31
+ const reasonSuffix = partialReason ? ` — ${partialReason}` : "";
32
+ return `${shownCount} ${label}${reasonSuffix}`;
33
+ }
34
+
35
+ if (omittedCount !== null && omittedCount > 0) {
36
+ return `${shownCount} of ${totalCount} ${label} (${omittedCount} omitted)`;
37
+ }
38
+
39
+ return `${shownCount} ${label}`;
40
+ }
@@ -324,6 +324,7 @@ export class ScopedSettingsList {
324
324
  this.tui.requestRender();
325
325
  },
326
326
  this.ctx,
327
+ row.field.field,
327
328
  ),
328
329
  onDone: () => {
329
330
  this.submenu = null;
@@ -91,9 +91,23 @@ export interface StringListField extends BaseField {
91
91
  kind: "stringList";
92
92
  }
93
93
 
94
+ /** One non-model choice shown before the scoped models in a model picker. */
95
+ export interface ModelPickerStaticOption {
96
+ /** Persisted value for the choice. */
97
+ value: string;
98
+ /** Human-readable picker label. */
99
+ label: string;
100
+ /** Optional explanation shown alongside the label. */
101
+ description?: string;
102
+ }
103
+
94
104
  /** Model picker backed by the scoped model set. */
95
105
  export interface ModelPickerField extends BaseField {
96
106
  kind: "modelPicker";
107
+ /** Additional host-owned choices shown before scoped models. */
108
+ staticOptions?: ModelPickerStaticOption[];
109
+ /** Whether to include the built-in `disabled` choice. Defaults to true. */
110
+ includeDisabled?: boolean;
97
111
  }
98
112
 
99
113
  /**
@@ -14,6 +14,7 @@ import {
14
14
  Text,
15
15
  } from "@earendil-works/pi-tui";
16
16
  import { getSelectableModels } from "../model-selection.ts";
17
+ import type { ModelPickerField } from "./settings-schema.ts";
17
18
 
18
19
  /**
19
20
  * Creates a pi-tui Input-backed submenu component with enter-to-confirm
@@ -57,25 +58,26 @@ export function createInputSubmenu(
57
58
  }
58
59
 
59
60
  /**
60
- * Creates a model picker submenu for settings with "disabled" as the first option.
61
+ * Creates a model picker submenu backed by the scoped model set.
62
+ *
63
+ * The built-in `disabled` choice remains enabled by default. Callers can add
64
+ * host-owned static choices or omit `disabled` through the field options.
61
65
  */
62
66
  export function createModelPickerSubmenu(
63
67
  currentValue: string,
64
68
  done: (selectedValue?: string) => void,
65
69
  ctx?: ExtensionContext,
70
+ options: Pick<ModelPickerField, "includeDisabled" | "staticOptions"> = {},
66
71
  ): {
67
72
  render: (width: number) => string[];
68
73
  invalidate: () => void;
69
74
  handleInput: (data: string) => boolean;
70
75
  } {
71
- const items = buildModelItems(ctx);
72
- const initialIndex =
73
- currentValue === "disabled"
74
- ? 0
75
- : Math.max(
76
- 0,
77
- items.findIndex((item) => item.value === currentValue),
78
- );
76
+ const items = buildModelItems(ctx, options);
77
+ const initialIndex = Math.max(
78
+ 0,
79
+ items.findIndex((item) => item.value === currentValue),
80
+ );
79
81
 
80
82
  const container = new Container();
81
83
  container.addChild(new Text(" Select model", 1, 0));
@@ -105,20 +107,35 @@ export function createModelPickerSubmenu(
105
107
  };
106
108
  }
107
109
 
108
- /** Build selectable model items with "disabled" as the first option. */
109
- function buildModelItems(ctx?: ExtensionContext): SelectItem[] {
110
- const items: SelectItem[] = [
111
- { value: "disabled", label: "disabled", description: "No model selected" },
112
- ];
110
+ /** Build static choices followed by the selectable scoped models. */
111
+ function buildModelItems(
112
+ ctx: ExtensionContext | undefined,
113
+ options: Pick<ModelPickerField, "includeDisabled" | "staticOptions">,
114
+ ): SelectItem[] {
115
+ const items: SelectItem[] = [];
116
+ const seen = new Set<string>();
117
+ for (const option of options.staticOptions ?? []) {
118
+ if (seen.has(option.value)) continue;
119
+ items.push({ ...option });
120
+ seen.add(option.value);
121
+ }
122
+
123
+ if (options.includeDisabled !== false && !seen.has("disabled")) {
124
+ items.push({ value: "disabled", label: "disabled", description: "No model selected" });
125
+ seen.add("disabled");
126
+ }
127
+
113
128
  if (!ctx) return items;
114
129
  const models = getSelectableModels(ctx);
115
130
  for (const model of models) {
131
+ if (seen.has(model.canonicalId)) continue;
116
132
  const suffix = model.isCurrent ? " [current]" : "";
117
133
  items.push({
118
134
  value: model.canonicalId,
119
135
  label: `${model.canonicalId}${suffix}`,
120
136
  description: model.label !== model.canonicalId ? model.label : undefined,
121
137
  });
138
+ seen.add(model.canonicalId);
122
139
  }
123
140
  return items;
124
141
  }
@@ -20,6 +20,7 @@ export type {
20
20
  DeclarativeSettingsOptions,
21
21
  EnumField,
22
22
  ModelPickerField,
23
+ ModelPickerStaticOption,
23
24
  NumberField,
24
25
  ScopedFieldValue,
25
26
  SettingsField,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-review",
3
- "version": "2.7.0",
4
- "description": "SuPi Review extension — session-aware review command and agent tools",
3
+ "version": "3.0.0",
4
+ "description": "SuPi Review extension — caller-defined read-only review tasks for users and agents",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@mrclrchtr/supi-core": "2.7.0"
34
+ "@mrclrchtr/supi-core": "3.0.0"
35
35
  },
36
36
  "bundledDependencies": [
37
37
  "@mrclrchtr/supi-core"
package/src/config.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadSupiConfig } from "@mrclrchtr/supi-core/config";
3
+ import { registerDeclarativeSettings } from "@mrclrchtr/supi-core/settings";
4
+ import { CURRENT_SESSION_REVIEW_MODEL } from "./model.ts";
5
+
6
+ /** Persisted model choices for agent-triggered reviews and optional planning. */
7
+ export interface ReviewConfig extends Record<string, unknown> {
8
+ /** Canonical reviewer model id, or `current` for the active session model. */
9
+ agentModel: string;
10
+ /** Canonical Planner model id, or `current` for the active session model. */
11
+ plannerModel: string;
12
+ }
13
+
14
+ /** Shared SuPi configuration section owned by this package. */
15
+ export const REVIEW_CONFIG_SECTION = "review";
16
+ /** Availability-safe defaults; users can configure a separate lightweight Planner model. */
17
+ export const REVIEW_DEFAULTS: ReviewConfig = {
18
+ agentModel: CURRENT_SESSION_REVIEW_MODEL,
19
+ plannerModel: CURRENT_SESSION_REVIEW_MODEL,
20
+ };
21
+
22
+ /** Load merged and normalized review configuration. */
23
+ export function loadReviewConfig(cwd: string, homeDir?: string): ReviewConfig {
24
+ const raw = loadSupiConfig(REVIEW_CONFIG_SECTION, cwd, REVIEW_DEFAULTS, { homeDir });
25
+ const readModel = (value: unknown, fallback: string) =>
26
+ typeof value === "string" && value.trim() ? value.trim() : fallback;
27
+ return {
28
+ agentModel: readModel(raw.agentModel, REVIEW_DEFAULTS.agentModel),
29
+ plannerModel: readModel(raw.plannerModel, REVIEW_DEFAULTS.plannerModel),
30
+ };
31
+ }
32
+
33
+ /** Contribute reviewer and Planner model pickers to `/supi-settings`. */
34
+ export function registerReviewSettings(pi: ExtensionAPI, homeDir?: string): void {
35
+ const currentOption = {
36
+ value: CURRENT_SESSION_REVIEW_MODEL,
37
+ label: "current session model",
38
+ };
39
+ registerDeclarativeSettings(pi, {
40
+ id: REVIEW_CONFIG_SECTION,
41
+ label: "Review",
42
+ section: REVIEW_CONFIG_SECTION,
43
+ defaults: REVIEW_DEFAULTS,
44
+ fields: [
45
+ {
46
+ kind: "modelPicker",
47
+ key: "agentModel",
48
+ label: "Reviewer model",
49
+ description: "Model shared by all tasks in an agent-triggered review run.",
50
+ includeDisabled: false,
51
+ staticOptions: [currentOption],
52
+ },
53
+ {
54
+ kind: "modelPicker",
55
+ key: "plannerModel",
56
+ label: "Planner model",
57
+ description: "Lightweight model used only when planning is set to suggest.",
58
+ includeDisabled: false,
59
+ staticOptions: [currentOption],
60
+ },
61
+ ],
62
+ ...(homeDir ? { homeDir } : {}),
63
+ });
64
+ }
@@ -0,0 +1,78 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+ const GIT_TIMEOUT_MS = 30_000;
9
+
10
+ function gitOptions(cwd: string, indexFile?: string) {
11
+ const env = { ...process.env };
12
+ for (const key of Object.keys(env)) if (key.startsWith("GIT_")) delete env[key];
13
+ if (indexFile) env.GIT_INDEX_FILE = indexFile;
14
+ return { cwd, env, timeout: GIT_TIMEOUT_MS, maxBuffer: 50 * 1024 * 1024 };
15
+ }
16
+
17
+ /** Run Git with bounded resources and a sanitized environment. */
18
+ export async function runGit(cwd: string, args: string[], indexFile?: string): Promise<string> {
19
+ return (await execFileAsync("git", args, gitOptions(cwd, indexFile))).stdout;
20
+ }
21
+
22
+ export function expectedGitExitOutput(error: unknown, allowedCodes: number[]): string | undefined {
23
+ if (!error || typeof error !== "object") return undefined;
24
+ const failure = error as {
25
+ code?: unknown;
26
+ killed?: unknown;
27
+ signal?: unknown;
28
+ stdout?: unknown;
29
+ };
30
+ if (
31
+ typeof failure.code !== "number" ||
32
+ !allowedCodes.includes(failure.code) ||
33
+ failure.killed === true ||
34
+ failure.signal
35
+ ) {
36
+ return undefined;
37
+ }
38
+ if (typeof failure.stdout === "string") return failure.stdout;
39
+ if (Buffer.isBuffer(failure.stdout)) return failure.stdout.toString("utf8");
40
+ return "";
41
+ }
42
+
43
+ /** Allow only documented non-zero Git outcomes while preserving operational failures. */
44
+ export async function runGitAllowExit(
45
+ cwd: string,
46
+ args: string[],
47
+ allowedCodes: number[],
48
+ indexFile?: string,
49
+ ): Promise<string> {
50
+ try {
51
+ return await runGit(cwd, args, indexFile);
52
+ } catch (error) {
53
+ const stdout = expectedGitExitOutput(error, allowedCodes);
54
+ if (stdout !== undefined) return stdout;
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /** Execute an operation against a temporary index seeded only from HEAD. */
60
+ export async function withHeadIndex<T>(
61
+ cwd: string,
62
+ head: string,
63
+ operation: (indexFile: string) => Promise<T>,
64
+ ): Promise<T> {
65
+ const directory = await mkdtemp(join(tmpdir(), "supi-review-index-"));
66
+ const indexFile = join(directory, "index");
67
+ try {
68
+ await runGit(cwd, ["read-tree", head], indexFile);
69
+ return await operation(indexFile);
70
+ } finally {
71
+ await rm(directory, { recursive: true, force: true });
72
+ }
73
+ }
74
+
75
+ /** Disable Git pathspec magic for a path-taking command. */
76
+ export function literalPathspec(args: string[]): string[] {
77
+ return ["--literal-pathspecs", ...args];
78
+ }