@mrclrchtr/supi-review 2.6.1 → 2.8.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 +24 -3
- package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- 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 +2 -2
- package/src/config.ts +55 -0
- package/src/model.ts +33 -0
- package/src/review.ts +45 -29
- package/src/tool/agent-review-tools.ts +20 -11
- package/src/tool/agent-review-workflow.ts +41 -23
- package/src/tool/brief-runner.ts +25 -24
- package/src/tool/child-failure-diagnostics.ts +145 -0
- package/src/tool/child-lifecycle-trace.ts +329 -0
- package/src/tool/review-handlers.ts +9 -30
- package/src/tool/review-runner.ts +25 -43
- package/src/tool/runner-helpers.ts +0 -14
- package/src/tool/session-lifecycle.ts +105 -10
- package/src/types.ts +51 -37
- package/src/ui/format-content.ts +47 -38
- package/src/ui/renderer.ts +67 -45
- package/src/ui/review-tool-renderer.ts +32 -1
- package/src/tool/review-debug.ts +0 -124
package/README.md
CHANGED
|
@@ -134,9 +134,29 @@ Every `/supi-review` command run asks you to choose the reviewer model.
|
|
|
134
134
|
- the picker only shows **scoped models** from Pi's `enabledModels` configuration
|
|
135
135
|
- the current session model is preselected only when it is inside that scoped set
|
|
136
136
|
- the selected model is used for both brief synthesis and the final review
|
|
137
|
-
-
|
|
137
|
+
- the command selection is not persisted
|
|
138
138
|
|
|
139
|
-
Agent-driven tool runs use the
|
|
139
|
+
Agent-driven tool runs use the **Review → Agent tool model** setting in `/supi-settings` when `@mrclrchtr/supi-settings` is installed:
|
|
140
|
+
|
|
141
|
+
- `current session model` is the default and preserves the active-session behavior
|
|
142
|
+
- an explicit choice stores a canonical `provider/model-id` from Pi's scoped `enabledModels` set
|
|
143
|
+
- the configured model is used for brief synthesis and every focused reviewer
|
|
144
|
+
- project settings override global settings through the normal SuPi settings scopes
|
|
145
|
+
- a configured model that is no longer scoped or available causes preparation to fail with a corrective error
|
|
146
|
+
|
|
147
|
+
The resolved model is retained in the prepared plan, so changing the setting or active session model between tool calls cannot silently alter the run.
|
|
148
|
+
|
|
149
|
+
For a standalone `supi-review` install without `supi-settings`, set the same value directly in the global `~/.pi/agent/supi/config.json` or project `.pi/supi/config.json` file:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"review": {
|
|
154
|
+
"agentModel": "openai/gpt-5"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use `"current"` instead of a canonical model id to follow the active session model.
|
|
140
160
|
|
|
141
161
|
## Result shape
|
|
142
162
|
|
|
@@ -175,7 +195,8 @@ When a successful review contains review items, `supi-review` also injects an ag
|
|
|
175
195
|
## Source
|
|
176
196
|
|
|
177
197
|
- `src/review.ts` — command orchestration and interactive flow
|
|
178
|
-
- `src/
|
|
198
|
+
- `src/config.ts` — persisted agent-tool model setting and `/supi-settings` registration
|
|
199
|
+
- `src/model.ts` — explicit and configured model selection helpers
|
|
179
200
|
- `src/git.ts` — git snapshot resolution
|
|
180
201
|
- `src/history/collect.ts` — compaction-style session-context serialization
|
|
181
202
|
- `src/history/synthesize.ts` — brief synthesis orchestration
|
|
@@ -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
|
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "SuPi Review extension — session-aware review command and agent tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@mrclrchtr/supi-core": "2.
|
|
34
|
+
"@mrclrchtr/supi-core": "2.8.0"
|
|
35
35
|
},
|
|
36
36
|
"bundledDependencies": [
|
|
37
37
|
"@mrclrchtr/supi-core"
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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 configuration for agent-driven Session-Aware Review. */
|
|
7
|
+
export interface ReviewConfig extends Record<string, unknown> {
|
|
8
|
+
/** Canonical `provider/model-id`, or `current` to use the active session model. */
|
|
9
|
+
agentModel: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Shared SuPi config section owned by supi-review. */
|
|
13
|
+
export const REVIEW_CONFIG_SECTION = "review";
|
|
14
|
+
|
|
15
|
+
/** Package defaults for supi-review configuration. */
|
|
16
|
+
export const REVIEW_DEFAULTS: ReviewConfig = {
|
|
17
|
+
agentModel: CURRENT_SESSION_REVIEW_MODEL,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Load merged, validated supi-review configuration for a workspace. */
|
|
21
|
+
export function loadReviewConfig(cwd: string, homeDir?: string): ReviewConfig {
|
|
22
|
+
const raw = loadSupiConfig(REVIEW_CONFIG_SECTION, cwd, REVIEW_DEFAULTS, { homeDir });
|
|
23
|
+
const agentModel =
|
|
24
|
+
typeof raw.agentModel === "string" && raw.agentModel.trim()
|
|
25
|
+
? raw.agentModel.trim()
|
|
26
|
+
: REVIEW_DEFAULTS.agentModel;
|
|
27
|
+
return { agentModel };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Register the Review section contributed to `/supi-settings`. */
|
|
31
|
+
export function registerReviewSettings(pi: ExtensionAPI, homeDir?: string): void {
|
|
32
|
+
registerDeclarativeSettings(pi, {
|
|
33
|
+
id: REVIEW_CONFIG_SECTION,
|
|
34
|
+
label: "Review",
|
|
35
|
+
section: REVIEW_CONFIG_SECTION,
|
|
36
|
+
defaults: REVIEW_DEFAULTS,
|
|
37
|
+
fields: [
|
|
38
|
+
{
|
|
39
|
+
kind: "modelPicker",
|
|
40
|
+
key: "agentModel",
|
|
41
|
+
label: "Agent tool model",
|
|
42
|
+
description: "Model used for brief synthesis and reviewers started by supi_review_prepare.",
|
|
43
|
+
includeDisabled: false,
|
|
44
|
+
staticOptions: [
|
|
45
|
+
{
|
|
46
|
+
value: CURRENT_SESSION_REVIEW_MODEL,
|
|
47
|
+
label: "current session model",
|
|
48
|
+
description: "Use the model active when supi_review_prepare starts",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
...(homeDir ? { homeDir } : {}),
|
|
54
|
+
});
|
|
55
|
+
}
|
package/src/model.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { getSelectableModels } from "@mrclrchtr/supi-core/model-selection";
|
|
3
3
|
import type { ReviewModelSelection } from "./types.ts";
|
|
4
4
|
|
|
5
|
+
/** Sentinel that resolves to the model active when review preparation starts. */
|
|
6
|
+
export const CURRENT_SESSION_REVIEW_MODEL = "current";
|
|
7
|
+
|
|
5
8
|
/** Build the canonical `provider/modelId` string used throughout the review flow. */
|
|
6
9
|
export { toCanonicalModelId } from "@mrclrchtr/supi-core/model-selection";
|
|
7
10
|
|
|
@@ -35,3 +38,33 @@ export function getCurrentReviewModel(
|
|
|
35
38
|
isCurrent: true,
|
|
36
39
|
};
|
|
37
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the configured model for an agent-driven review.
|
|
44
|
+
*
|
|
45
|
+
* `current` preserves the historical behavior. Explicit canonical model ids
|
|
46
|
+
* must be both available and present in Pi's current scoped model set.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveAgentReviewModel(
|
|
49
|
+
ctx: Pick<ExtensionContext, "cwd" | "modelRegistry" | "model">,
|
|
50
|
+
configuredModelId: string,
|
|
51
|
+
enabledModelPatterns?: string[],
|
|
52
|
+
): ReviewModelSelection | undefined {
|
|
53
|
+
const modelId = configuredModelId.trim();
|
|
54
|
+
if (modelId === CURRENT_SESSION_REVIEW_MODEL) {
|
|
55
|
+
return getCurrentReviewModel(ctx);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const selection = getSelectableReviewModels(
|
|
59
|
+
{ cwd: ctx.cwd, modelRegistry: ctx.modelRegistry, model: undefined },
|
|
60
|
+
enabledModelPatterns,
|
|
61
|
+
).find((candidate) => candidate.canonicalId === modelId);
|
|
62
|
+
if (!selection) return undefined;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...selection,
|
|
66
|
+
isCurrent: ctx.model
|
|
67
|
+
? `${ctx.model.provider}/${ctx.model.id}` === selection.canonicalId
|
|
68
|
+
: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/review.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { buildSessionContext, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { WidgetProgress } from "@mrclrchtr/supi-core/progress-widget";
|
|
3
3
|
import { runWithProgressWidget } from "@mrclrchtr/supi-core/tool-framework";
|
|
4
|
+
import { registerReviewSettings } from "./config.ts";
|
|
4
5
|
import { resolveBranchSnapshot, resolveCommitSnapshot, resolveWorkingTreeSnapshot } from "./git.ts";
|
|
5
6
|
import { serializeSessionContext } from "./history/collect.ts";
|
|
6
7
|
import { synthesizeReviewBrief } from "./history/synthesize.ts";
|
|
7
8
|
import { normalizeReviewResult } from "./review-result.ts";
|
|
8
9
|
import { buildReviewPacket } from "./target/packet.ts";
|
|
9
10
|
import { registerAgentReviewTools } from "./tool/agent-review-tools.ts";
|
|
11
|
+
import { createUnobservedChildFailureDiagnostics } from "./tool/child-failure-diagnostics.ts";
|
|
10
12
|
import { runReviewer } from "./tool/review-runner.ts";
|
|
11
13
|
import type {
|
|
12
14
|
BriefSynthesisRunResult,
|
|
@@ -17,13 +19,18 @@ import type {
|
|
|
17
19
|
ReviewTargetSpec,
|
|
18
20
|
} from "./types.ts";
|
|
19
21
|
import { collectReviewNote, previewReviewPlan, selectModel, selectTarget } from "./ui/flow.ts";
|
|
20
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
formatBriefSynthesisFailureContent,
|
|
24
|
+
formatBriefSynthesisFailureCopy,
|
|
25
|
+
formatReviewContent,
|
|
26
|
+
} from "./ui/format-content.ts";
|
|
21
27
|
import { registerReviewRenderer } from "./ui/renderer.ts";
|
|
22
28
|
|
|
23
29
|
type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
|
|
24
30
|
|
|
25
31
|
export default function reviewExtension(pi: ExtensionAPI) {
|
|
26
32
|
registerReviewRenderer(pi);
|
|
33
|
+
registerReviewSettings(pi);
|
|
27
34
|
registerAgentReviewTools(pi);
|
|
28
35
|
|
|
29
36
|
pi.registerCommand("supi-review", {
|
|
@@ -74,22 +81,21 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
|
|
|
74
81
|
);
|
|
75
82
|
|
|
76
83
|
if (!synthesis) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
86
|
-
ctx.ui.notify("Brief synthesis was canceled or failed", "warning");
|
|
84
|
+
const failure: BriefSynthesisRunResult = {
|
|
85
|
+
kind: "failed",
|
|
86
|
+
failureCode: "unexpected-runner-failure",
|
|
87
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
88
|
+
};
|
|
89
|
+
notifyBriefDone(pi, failure, snapshot, model.canonicalId);
|
|
90
|
+
injectBriefSynthesisFailureMessage(pi, failure, snapshot, model.canonicalId);
|
|
91
|
+
ctx.ui.notify(formatBriefSynthesisFailureCopy(failure), "warning");
|
|
87
92
|
return;
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
notifyBriefDone(pi, synthesis, snapshot, model.canonicalId);
|
|
91
96
|
|
|
92
97
|
if (synthesis.kind !== "success") {
|
|
98
|
+
injectBriefSynthesisFailureMessage(pi, synthesis, snapshot, model.canonicalId);
|
|
93
99
|
notifySynthesisFailure(synthesis, ctx);
|
|
94
100
|
return;
|
|
95
101
|
}
|
|
@@ -122,14 +128,17 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
|
|
|
122
128
|
);
|
|
123
129
|
|
|
124
130
|
if (!rawResult) {
|
|
125
|
-
|
|
131
|
+
const failure: ReviewResult = {
|
|
126
132
|
kind: "failed",
|
|
127
|
-
|
|
133
|
+
failureCode: "unexpected-runner-failure",
|
|
134
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
128
135
|
snapshot,
|
|
129
136
|
brief,
|
|
130
137
|
modelId: model.canonicalId,
|
|
131
|
-
}
|
|
132
|
-
|
|
138
|
+
};
|
|
139
|
+
notifyReviewDone(pi, failure);
|
|
140
|
+
injectReviewMessage(pi, failure);
|
|
141
|
+
ctx.ui.notify("Reviewer ended unexpectedly.", "warning");
|
|
133
142
|
return;
|
|
134
143
|
}
|
|
135
144
|
|
|
@@ -172,20 +181,27 @@ function notifySynthesisFailure(
|
|
|
172
181
|
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
173
182
|
ctx: CommandContext,
|
|
174
183
|
): void {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
ctx.ui.notify(
|
|
185
|
+
formatBriefSynthesisFailureCopy(result),
|
|
186
|
+
result.kind === "failed" ? "error" : "warning",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Persist a brief-synthesis failure so bounded diagnostics remain inspectable in parent history. */
|
|
191
|
+
function injectBriefSynthesisFailureMessage(
|
|
192
|
+
pi: ExtensionAPI,
|
|
193
|
+
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
194
|
+
snapshot: ReviewSnapshot,
|
|
195
|
+
modelId: string,
|
|
196
|
+
): void {
|
|
197
|
+
pi.sendMessage({
|
|
198
|
+
customType: "supi-review",
|
|
199
|
+
content: formatBriefSynthesisFailureContent(result),
|
|
200
|
+
display: true,
|
|
201
|
+
details: {
|
|
202
|
+
briefSynthesisFailure: { result, snapshot, modelId },
|
|
203
|
+
},
|
|
204
|
+
});
|
|
189
205
|
}
|
|
190
206
|
|
|
191
207
|
/** Ring the terminal bell so the user knows to check back. */
|
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
truncateHead,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
|
|
13
|
+
import { loadReviewConfig } from "../config.ts";
|
|
13
14
|
import { isCommitObjectId, summarizeReviewSnapshot } from "../git.ts";
|
|
14
15
|
import { serializeSessionContext } from "../history/collect.ts";
|
|
15
|
-
import {
|
|
16
|
+
import { CURRENT_SESSION_REVIEW_MODEL, resolveAgentReviewModel } from "../model.ts";
|
|
16
17
|
import { ReviewPlanStore } from "../session/review-plan-store.ts";
|
|
17
18
|
import type {
|
|
18
19
|
BriefEvaluation,
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
ReviewProgress,
|
|
21
22
|
ReviewTargetSpec,
|
|
22
23
|
} from "../types.ts";
|
|
24
|
+
import { formatBriefSynthesisFailureCopy } from "../ui/format-content.ts";
|
|
23
25
|
import { formatAgentReviewBatch, formatPreparedAgentReview } from "../ui/review-tool-format.ts";
|
|
24
26
|
import {
|
|
25
27
|
type AgentReviewProgressDetails,
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
runAgentReviewSchema,
|
|
36
38
|
} from "./agent-review-schemas.ts";
|
|
37
39
|
import { prepareAgentReviewPlan, runAgentReviewBatch } from "./agent-review-workflow.ts";
|
|
40
|
+
import { formatChildLifecycleTrace } from "./child-lifecycle-trace.ts";
|
|
38
41
|
import {
|
|
39
42
|
PREPARE_REVIEW_TOOL_NAME,
|
|
40
43
|
prepareReviewPromptGuidelines,
|
|
@@ -62,8 +65,16 @@ export function registerAgentReviewTools(
|
|
|
62
65
|
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
63
66
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
64
67
|
const input = params as PrepareAgentReviewInput;
|
|
65
|
-
const
|
|
66
|
-
|
|
68
|
+
const configuredModelId = loadReviewConfig(ctx.cwd).agentModel;
|
|
69
|
+
const model = resolveAgentReviewModel(ctx, configuredModelId);
|
|
70
|
+
if (!model) {
|
|
71
|
+
if (configuredModelId === CURRENT_SESSION_REVIEW_MODEL) {
|
|
72
|
+
throw new Error("No current session model is available for review preparation.");
|
|
73
|
+
}
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Configured agent review model "${configuredModelId}" is not available in Pi's scoped model set. Choose another model in /supi-settings.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
67
78
|
|
|
68
79
|
const target = parseTarget(input);
|
|
69
80
|
const sessionContext = buildSessionContext(
|
|
@@ -228,14 +239,12 @@ function formatPreparationFailure(
|
|
|
228
239
|
outcome: Exclude<Awaited<ReturnType<typeof prepareAgentReviewPlan>>, { kind: "prepared" }>,
|
|
229
240
|
): string {
|
|
230
241
|
if (outcome.kind === "no-snapshot") return outcome.reason;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return `Brief synthesis timed out after ${(outcome.result.timeoutMs / 1_000).toFixed(0)}s.`;
|
|
238
|
-
}
|
|
242
|
+
|
|
243
|
+
const { result } = outcome;
|
|
244
|
+
const trace = result.diagnostics?.lifecycleTrace;
|
|
245
|
+
return trace
|
|
246
|
+
? `${formatBriefSynthesisFailureCopy(result)}\n\n${formatChildLifecycleTrace(trace)}`
|
|
247
|
+
: formatBriefSynthesisFailureCopy(result);
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
function activateRunTool(pi: ExtensionAPI): void {
|
|
@@ -22,8 +22,8 @@ import type {
|
|
|
22
22
|
ReviewTargetSpec,
|
|
23
23
|
SynthesizedReviewBrief,
|
|
24
24
|
} from "../types.ts";
|
|
25
|
+
import { createUnobservedChildFailureDiagnostics } from "./child-failure-diagnostics.ts";
|
|
25
26
|
import { runReviewer } from "./review-runner.ts";
|
|
26
|
-
|
|
27
27
|
/** Inputs required to synthesize and retain one agent-driven review plan. */
|
|
28
28
|
export interface PrepareAgentReviewWorkflowInput {
|
|
29
29
|
cwd: string;
|
|
@@ -35,13 +35,11 @@ export interface PrepareAgentReviewWorkflowInput {
|
|
|
35
35
|
onProgress?: (progress: ReviewProgress) => void;
|
|
36
36
|
planStore: ReviewPlanStore;
|
|
37
37
|
}
|
|
38
|
-
|
|
39
38
|
/** Typed preparation outcome before any reviewer child session starts. */
|
|
40
39
|
export type PrepareAgentReviewWorkflowOutcome =
|
|
41
40
|
| { kind: "prepared"; plan: StoredAgentReviewPlan }
|
|
42
41
|
| { kind: "no-snapshot"; reason: string }
|
|
43
42
|
| { kind: "synthesis-failed"; result: Exclude<BriefSynthesisRunResult, { kind: "success" }> };
|
|
44
|
-
|
|
45
43
|
/** Inputs required to critique and execute one prepared review plan. */
|
|
46
44
|
export interface RunAgentReviewWorkflowInput {
|
|
47
45
|
cwd: string;
|
|
@@ -55,13 +53,11 @@ export interface RunAgentReviewWorkflowInput {
|
|
|
55
53
|
onReviewerDone?: (reviewerId: string) => void;
|
|
56
54
|
planStore: ReviewPlanStore;
|
|
57
55
|
}
|
|
58
|
-
|
|
59
56
|
/** Typed batch outcome after validation and snapshot freshness checks. */
|
|
60
57
|
export type RunAgentReviewWorkflowOutcome =
|
|
61
58
|
| { kind: "completed"; details: AgentReviewBatchDetails }
|
|
62
59
|
| { kind: "invalid"; reason: string }
|
|
63
60
|
| { kind: "stale"; reason: string };
|
|
64
|
-
|
|
65
61
|
/** Prepare one session-scoped review plan without starting reviewer sessions. */
|
|
66
62
|
export async function prepareAgentReviewPlan(
|
|
67
63
|
input: PrepareAgentReviewWorkflowInput,
|
|
@@ -74,15 +70,27 @@ export async function prepareAgentReviewPlan(
|
|
|
74
70
|
};
|
|
75
71
|
}
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
let synthesis: BriefSynthesisRunResult;
|
|
74
|
+
try {
|
|
75
|
+
synthesis = await synthesizeReviewBrief({
|
|
76
|
+
model: input.model,
|
|
77
|
+
cwd: input.cwd,
|
|
78
|
+
snapshot,
|
|
79
|
+
serializedContext: input.serializedContext,
|
|
80
|
+
note: input.note,
|
|
81
|
+
signal: input.signal,
|
|
82
|
+
onProgress: input.onProgress,
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
return {
|
|
86
|
+
kind: "synthesis-failed",
|
|
87
|
+
result: {
|
|
88
|
+
kind: "failed",
|
|
89
|
+
failureCode: "unexpected-runner-failure",
|
|
90
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
86
94
|
if (synthesis.kind !== "success") {
|
|
87
95
|
return { kind: "synthesis-failed", result: synthesis };
|
|
88
96
|
}
|
|
@@ -195,13 +203,14 @@ async function runAssignment(
|
|
|
195
203
|
onProgress: (progress) => input.onReviewerProgress?.(assignment.id, progress),
|
|
196
204
|
});
|
|
197
205
|
return { assignment, result: compactReviewResult(normalizeReviewResult(rawResult)) };
|
|
198
|
-
} catch
|
|
206
|
+
} catch {
|
|
199
207
|
return {
|
|
200
208
|
assignment,
|
|
201
209
|
result: {
|
|
202
210
|
kind: "failed",
|
|
203
|
-
|
|
211
|
+
failureCode: "unexpected-runner-failure",
|
|
204
212
|
modelId: plan.model.canonicalId,
|
|
213
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
205
214
|
},
|
|
206
215
|
};
|
|
207
216
|
}
|
|
@@ -212,21 +221,30 @@ function compactReviewResult(result: ReviewResult): AgentReviewerResult {
|
|
|
212
221
|
case "success":
|
|
213
222
|
return { kind: "success", output: result.output, modelId: result.modelId };
|
|
214
223
|
case "failed":
|
|
224
|
+
return result.failureCode === "session-creation-failed"
|
|
225
|
+
? {
|
|
226
|
+
kind: "failed",
|
|
227
|
+
failureCode: result.failureCode,
|
|
228
|
+
modelId: result.modelId,
|
|
229
|
+
}
|
|
230
|
+
: {
|
|
231
|
+
kind: "failed",
|
|
232
|
+
failureCode: result.failureCode,
|
|
233
|
+
modelId: result.modelId,
|
|
234
|
+
diagnostics: result.diagnostics,
|
|
235
|
+
};
|
|
236
|
+
case "canceled":
|
|
215
237
|
return {
|
|
216
|
-
kind: "
|
|
217
|
-
reason: result.reason,
|
|
238
|
+
kind: "canceled",
|
|
218
239
|
modelId: result.modelId,
|
|
219
|
-
|
|
240
|
+
diagnostics: result.diagnostics,
|
|
220
241
|
};
|
|
221
|
-
case "canceled":
|
|
222
|
-
return { kind: "canceled", modelId: result.modelId, debug: result.debug };
|
|
223
242
|
case "timeout":
|
|
224
243
|
return {
|
|
225
244
|
kind: "timeout",
|
|
226
245
|
timeoutMs: result.timeoutMs,
|
|
227
|
-
partialOutput: result.partialOutput,
|
|
228
246
|
modelId: result.modelId,
|
|
229
|
-
|
|
247
|
+
diagnostics: result.diagnostics,
|
|
230
248
|
};
|
|
231
249
|
}
|
|
232
250
|
}
|