@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.
- package/README.md +75 -163
- package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
- package/package.json +3 -3
- package/src/config.ts +64 -0
- package/src/git-command.ts +78 -0
- package/src/git.ts +306 -505
- package/src/history/collect.ts +43 -120
- package/src/model.ts +35 -0
- package/src/review-path.ts +85 -0
- package/src/review-result.ts +43 -92
- package/src/review.ts +154 -288
- package/src/session/review-plan-store.ts +20 -51
- package/src/target/packet.ts +46 -369
- package/src/tool/agent-review-schemas.ts +101 -104
- package/src/tool/agent-review-tools.ts +269 -301
- package/src/tool/child-failure-diagnostics.ts +1 -1
- package/src/tool/child-resource-loader.ts +37 -0
- package/src/tool/child-session-runner.ts +83 -0
- package/src/tool/output-page.ts +47 -0
- package/src/tool/planner-runner.ts +69 -0
- package/src/tool/review-runner.ts +42 -257
- package/src/tool/review-system-prompt.ts +12 -110
- package/src/tool/review-tools.ts +119 -0
- package/src/tool/review-workflow.ts +309 -0
- package/src/tool/runner-helpers.ts +3 -8
- package/src/tool/schemas.ts +75 -62
- package/src/tui/common.ts +175 -0
- package/src/tui/prepare.ts +132 -0
- package/src/tui/run.ts +160 -0
- package/src/types.ts +153 -275
- package/src/history/synthesize.ts +0 -121
- package/src/tool/agent-review-workflow.ts +0 -398
- package/src/tool/brief-runner.ts +0 -180
- package/src/tool/guidance.ts +0 -21
- package/src/tool/review-handlers.ts +0 -314
- package/src/tool/snapshot-tools.ts +0 -117
- package/src/ui/flow.ts +0 -189
- package/src/ui/format-content.ts +0 -143
- package/src/ui/renderer.ts +0 -274
- package/src/ui/review-plan-inspector.ts +0 -391
- package/src/ui/review-tool-format.ts +0 -138
- 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
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-

|
|
43
|
-
|
|
44
|
-

|
|
45
|
-
|
|
46
|
-

|
|
47
|
-
|
|
48
|
-

|
|
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
|
-
##
|
|
48
|
+
## Prepared Review
|
|
65
49
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
57
|
+
Preparation returns a session-scoped, one-shot `planId` and optional Planner Draft. Execute it with an explicit decision:
|
|
118
58
|
|
|
119
|
-
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mode": "prepared",
|
|
62
|
+
"planId": "review-plan-...",
|
|
63
|
+
"decision": { "kind": "accept-draft" }
|
|
64
|
+
}
|
|
65
|
+
```
|
|
120
66
|
|
|
121
|
-
|
|
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
|
-
|
|
69
|
+
## Targets
|
|
129
70
|
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
Callers own task methodology. The Review Engine owns:
|
|
140
80
|
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
Each successful task returns a summary and ordered findings. Findings contain:
|
|
165
93
|
|
|
166
|
-
|
|
94
|
+
- `blocksAcceptance`
|
|
95
|
+
- `impact`: `low | medium | high`
|
|
96
|
+
- `effort`: `small | medium | large`
|
|
97
|
+
- `confidence`: `0..1`
|
|
98
|
+
- optional target-relative location
|
|
167
99
|
|
|
168
|
-
|
|
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
|
-
##
|
|
102
|
+
## Models
|
|
176
103
|
|
|
177
|
-
- `
|
|
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": "
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
109
|
-
function buildModelItems(
|
|
110
|
-
|
|
111
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-review",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "SuPi Review extension —
|
|
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": "
|
|
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
|
+
}
|