pi-orchestrator-config 4.22.4 → 4.24.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/CHANGELOG.md +29 -0
- package/README.md +17 -1
- package/extensions/orchestrator/ask-user.ts +94 -22
- package/extensions/orchestrator/subagent-tool.ts +6 -2
- package/extensions/orchestrator/utils.ts +1 -1
- package/extensions/shared/create-runtime-provider.ts +1 -1
- package/extensions/shared/ui-bridge.ts +5 -1
- package/package.json +7 -5
- package/prompts/pr-review.md +81 -4
- package/prompts/release.md +14 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [4.24.0] - 2026-11-03
|
|
4
|
+
|
|
5
|
+
- `/pr-review` asks how to decide after the findings table — `Post all`, `Skip all`, or
|
|
6
|
+
`Decide one by one` — instead of one bulk answer for everything.
|
|
7
|
+
- Deciding one by one gives each finding its own dialog (post, skip, skip with reason, more
|
|
8
|
+
context, stop), captures the skip reason at decision time, keeps author questions on their
|
|
9
|
+
Approve / Edit / Skip path, and stores a reason even for a bare skip.
|
|
10
|
+
- Every `ask_user` prompt offers "💬 Discuss with AI first", which returns a sentinel meaning
|
|
11
|
+
"talk about this decision first" rather than an answer; a caller that passes that row itself
|
|
12
|
+
gets one row, and `allowDiscuss: false` removes it.
|
|
13
|
+
- Docs: the `ask_user` reference covers `allowDiscuss` and the sentinel; the workflow page
|
|
14
|
+
documents the mode choice and the real turn cost.
|
|
15
|
+
|
|
16
|
+
## [4.23.0] - 2026-11-02
|
|
17
|
+
|
|
18
|
+
- Minimum pi version raised to 1.5.0 — the extension floor, the sidecar floor, and every manifest move together.
|
|
19
|
+
- Interactive dialogs (ask_user and the pidash bridge) now report "blocked on user input" while they wait, so terminals that track program status show pi as waiting instead of busy.
|
|
20
|
+
- This changelog ships with the package and pi reads it after `pi update --extensions`, so an update now tells you what changed.
|
|
21
|
+
- The `subagent` tool keeps its `agentScope` values as a real enum after pi 1.5.0 changed how `StringEnum` types itself, so the parameter stays validated and documented.
|
|
22
|
+
- Release workflow keeps this changelog current on every bump, and a changelog-only change now marks the package as affected.
|
|
23
|
+
- Docs: MCP OAuth client registration (`auto` / `dcr` / `cimd`), `PI_SELF_UPDATE_COMMAND` for wrapper installs, and the billed-vs-estimated cost contract.
|
|
24
|
+
|
|
25
|
+
## [4.22.4] - 2026-11-02
|
|
26
|
+
|
|
27
|
+
- Minimum pi version raised to 1.5.0 (extension and sidecar floors, manifests).
|
|
28
|
+
- Sidecar reports cost billed by the provider instead of only estimated cost.
|
|
29
|
+
- Status reporting for interactive dialogs blocked waiting on user input.
|
package/README.md
CHANGED
|
@@ -210,6 +210,22 @@ After updating, run `/reload` in pi or restart pi to pick up changes.
|
|
|
210
210
|
Refresh the global agent package after acpx-provider / cli-provider / settings
|
|
211
211
|
changes so `~/.pi` is not a mixed checkout (see issue #651).
|
|
212
212
|
|
|
213
|
+
### Custom update command
|
|
214
|
+
|
|
215
|
+
`pi update` installs a new release with a global npm install. When a wrapper or another
|
|
216
|
+
package manager provides pi — mise, pnpm, nix — point it at that manager's command instead:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
export PI_SELF_UPDATE_COMMAND="mise use -g npm:{packageName}@{version}"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`pi update` runs the command only when pi.dev reports a newer release, replacing
|
|
223
|
+
`{packageName}`, `{version}`, and `{installSpec}` (`{packageName}@{version}`) with that
|
|
224
|
+
release. The value is split on spaces; for an argument that contains spaces, set the global
|
|
225
|
+
`selfUpdateCommand` setting in `~/.pi/agent/settings.json` as an argv array such as
|
|
226
|
+
`["nix", "profile", "upgrade", "pi"]`. It can only be set in the agent-directory settings,
|
|
227
|
+
and `PI_SELF_UPDATE_COMMAND` takes precedence over it.
|
|
228
|
+
|
|
213
229
|
## Usage
|
|
214
230
|
|
|
215
231
|
### Automatic delegation
|
|
@@ -1076,7 +1092,7 @@ it also runs as a pre-commit hook when TypeScript files change.
|
|
|
1076
1092
|
|
|
1077
1093
|
## Prerequisites
|
|
1078
1094
|
|
|
1079
|
-
- [pi](https://github.com/badlogic/pi-mono) (minimum version: **1.
|
|
1095
|
+
- [pi](https://github.com/badlogic/pi-mono) (minimum version: **1.5.0**). Pidash uses Pi's `ui_prompt_start` and `ui_prompt_end` events to show prompt wait separately from agent work.
|
|
1080
1096
|
- `gh` CLI (for GitHub operations)
|
|
1081
1097
|
- `uv` (for Python execution)
|
|
1082
1098
|
- `myk-pi-tools` (optional, for `/pr-review` and `/release`)
|
|
@@ -14,6 +14,74 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-tui";
|
|
15
15
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Type } from "typebox";
|
|
17
|
+
import { createLogger } from "../shared/logger.js";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("ask-user");
|
|
20
|
+
|
|
21
|
+
/** Sentinel value returned when the user asks to discuss in plain chat first. */
|
|
22
|
+
export const DISCUSS_VALUE = "__discuss__";
|
|
23
|
+
export const DISCUSS_LABEL = "💬 Discuss with AI first";
|
|
24
|
+
const FREE_INPUT_VALUE = "__free_input__";
|
|
25
|
+
|
|
26
|
+
/** Emoji/punctuation-free, lowercased form so a caller's plain label still matches. */
|
|
27
|
+
const normalizeOption = (s: string): string =>
|
|
28
|
+
s.replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
|
|
29
|
+
|
|
30
|
+
/** A caller option that already is the discuss row: its plain label or the bare sentinel. */
|
|
31
|
+
const isDiscussRow = (option: string): boolean =>
|
|
32
|
+
option.trim().toLowerCase() === DISCUSS_VALUE ||
|
|
33
|
+
normalizeOption(option) === normalizeOption(DISCUSS_LABEL);
|
|
34
|
+
|
|
35
|
+
export function buildAskOptions(
|
|
36
|
+
options: string[] | undefined,
|
|
37
|
+
allowDiscuss: boolean,
|
|
38
|
+
): SelectItem[] {
|
|
39
|
+
// Free-input-only mode: no list, no discuss item.
|
|
40
|
+
if (!options || options.length === 0) return [];
|
|
41
|
+
|
|
42
|
+
let callerSuppliedDiscuss = false;
|
|
43
|
+
const items: SelectItem[] = [];
|
|
44
|
+
for (const opt of options) {
|
|
45
|
+
if (!isDiscussRow(opt)) {
|
|
46
|
+
items.push({ value: opt, label: opt });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Opted out: a caller's discuss row must disappear, not merely go unappended.
|
|
50
|
+
if (!allowDiscuss) continue;
|
|
51
|
+
// Caller's label stays for display; the sentinel makes selecting it pause.
|
|
52
|
+
callerSuppliedDiscuss = true;
|
|
53
|
+
items.push({ value: DISCUSS_VALUE, label: opt, description: "chat first" });
|
|
54
|
+
}
|
|
55
|
+
if (!allowDiscuss) {
|
|
56
|
+
log.debug("ask_user discuss option suppressed", { optionCount: options.length });
|
|
57
|
+
} else if (callerSuppliedDiscuss) {
|
|
58
|
+
log.debug("ask_user discuss option deduped", { optionCount: options.length });
|
|
59
|
+
} else {
|
|
60
|
+
items.push({ value: DISCUSS_VALUE, label: DISCUSS_LABEL, description: "chat first" });
|
|
61
|
+
log.debug("ask_user discuss option offered", { optionCount: options.length });
|
|
62
|
+
}
|
|
63
|
+
items.push({
|
|
64
|
+
value: FREE_INPUT_VALUE,
|
|
65
|
+
label: "✎ Other (type custom answer)",
|
|
66
|
+
description: "free-text",
|
|
67
|
+
});
|
|
68
|
+
return items;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Browser/discord renders options as buttons labelled with the raw string, so the
|
|
73
|
+
* discuss row travels as its label and comes back mapped to the sentinel. The
|
|
74
|
+
* free-input row is a TUI control, not an answer — it is never offered remotely.
|
|
75
|
+
*/
|
|
76
|
+
export function browserAskOptions(items: SelectItem[]): string[] {
|
|
77
|
+
return items
|
|
78
|
+
.filter((item) => item.value !== FREE_INPUT_VALUE)
|
|
79
|
+
.map((item) => (item.value === DISCUSS_VALUE ? DISCUSS_LABEL : item.value));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function answerToValue(answer: string): string {
|
|
83
|
+
return answer === DISCUSS_LABEL ? DISCUSS_VALUE : answer;
|
|
84
|
+
}
|
|
17
85
|
|
|
18
86
|
export function registerAskUser(
|
|
19
87
|
pi: ExtensionAPI,
|
|
@@ -29,6 +97,7 @@ export function registerAskUser(
|
|
|
29
97
|
"Use ask_user when you need user input during a workflow (approvals, selections, confirmations).",
|
|
30
98
|
"Do NOT ask users questions via plain text — always use this tool for structured choices.",
|
|
31
99
|
"Provide clear, concise options. Include a 'no' or 'cancel' option when appropriate.",
|
|
100
|
+
`If ask_user returns exactly "${DISCUSS_VALUE}", the user has NOT answered — they want to talk about this decision first. Open by addressing the decision itself: the trade-offs, the information you are missing, or the alternatives. Never answer with a status report about what you built or verified. Re-call ask_user with the SAME question and options only once the discussion resolves.`,
|
|
32
101
|
],
|
|
33
102
|
parameters: Type.Object({
|
|
34
103
|
question: Type.String({
|
|
@@ -40,6 +109,12 @@ export function registerAskUser(
|
|
|
40
109
|
"List of selectable options. If omitted, only free-text input is shown.",
|
|
41
110
|
}),
|
|
42
111
|
),
|
|
112
|
+
allowDiscuss: Type.Optional(
|
|
113
|
+
Type.Boolean({
|
|
114
|
+
description:
|
|
115
|
+
"Offer a 'Discuss with AI first' option that returns the __discuss__ sentinel without answering (default true).",
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
43
118
|
}),
|
|
44
119
|
|
|
45
120
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
@@ -55,14 +130,16 @@ export function registerAskUser(
|
|
|
55
130
|
|
|
56
131
|
terminalNotify("pi", "Action required");
|
|
57
132
|
|
|
133
|
+
const assembled = buildAskOptions(params.options, params.allowDiscuss !== false);
|
|
134
|
+
|
|
58
135
|
// Emit ask request for pidash browser clients
|
|
59
136
|
const askId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
60
137
|
pi.events.emit("pidash:ui-request", {
|
|
61
138
|
id: askId,
|
|
62
139
|
type: "extension_ui_request",
|
|
63
|
-
method:
|
|
140
|
+
method: assembled.length ? "select" : "input",
|
|
64
141
|
title: params.question,
|
|
65
|
-
options:
|
|
142
|
+
options: assembled.length ? browserAskOptions(assembled) : undefined,
|
|
66
143
|
});
|
|
67
144
|
|
|
68
145
|
// Track if resolved (from TUI or browser)
|
|
@@ -73,19 +150,21 @@ export function registerAskUser(
|
|
|
73
150
|
const resp = data as any;
|
|
74
151
|
if (resp.id === askId && externalDone) {
|
|
75
152
|
if (resp.cancelled) externalDone(null);
|
|
76
|
-
else if (resp.value)
|
|
77
|
-
|
|
153
|
+
else if (resp.value) {
|
|
154
|
+
const value = answerToValue(resp.value);
|
|
155
|
+
log.debug("ask_user browser answer received", { askId, isDiscuss: value === DISCUSS_VALUE });
|
|
156
|
+
externalDone(value);
|
|
157
|
+
} else if (resp.confirmed !== undefined) externalDone(resp.confirmed ? "Yes" : "No");
|
|
78
158
|
}
|
|
79
159
|
});
|
|
80
160
|
|
|
161
|
+
log.debug("ask_user prompt opened with blocked status", { askId, mode: assembled.length ? "select" : "input" });
|
|
81
162
|
const result = await ctx.ui.custom<string | null>(
|
|
82
163
|
(tui, theme, _kb, done) => {
|
|
83
164
|
// Allow browser to resolve this dialog
|
|
84
165
|
externalDone = done;
|
|
85
166
|
|
|
86
|
-
let mode: "select" | "input" =
|
|
87
|
-
? "select"
|
|
88
|
-
: "input";
|
|
167
|
+
let mode: "select" | "input" = assembled.length ? "select" : "input";
|
|
89
168
|
let resolved = false;
|
|
90
169
|
|
|
91
170
|
const resolve = (value: string | null) => {
|
|
@@ -137,7 +216,7 @@ export function registerAskUser(
|
|
|
137
216
|
sl.onSelect = (item: SelectItem) => {
|
|
138
217
|
if (item.value === "__no_match__") {
|
|
139
218
|
// Ignore — not a real option
|
|
140
|
-
} else if (item.value ===
|
|
219
|
+
} else if (item.value === FREE_INPUT_VALUE) {
|
|
141
220
|
mode = "input";
|
|
142
221
|
searchInput.setValue("");
|
|
143
222
|
tui.requestRender();
|
|
@@ -149,18 +228,8 @@ export function registerAskUser(
|
|
|
149
228
|
return sl;
|
|
150
229
|
};
|
|
151
230
|
|
|
152
|
-
if (
|
|
153
|
-
allItems =
|
|
154
|
-
...params.options.map((opt: string) => ({
|
|
155
|
-
value: opt,
|
|
156
|
-
label: opt,
|
|
157
|
-
})),
|
|
158
|
-
{
|
|
159
|
-
value: "__free_input__",
|
|
160
|
-
label: "✎ Other (type custom answer)",
|
|
161
|
-
description: "free-text",
|
|
162
|
-
},
|
|
163
|
-
];
|
|
231
|
+
if (assembled.length > 0) {
|
|
232
|
+
allItems = assembled;
|
|
164
233
|
selectList = makeSelectList(allItems);
|
|
165
234
|
}
|
|
166
235
|
|
|
@@ -172,7 +241,7 @@ export function registerAskUser(
|
|
|
172
241
|
if (selected) {
|
|
173
242
|
if (selected.value === "__no_match__") {
|
|
174
243
|
// Ignore
|
|
175
|
-
} else if (selected.value ===
|
|
244
|
+
} else if (selected.value === FREE_INPUT_VALUE) {
|
|
176
245
|
mode = "input";
|
|
177
246
|
searchInput.setValue("");
|
|
178
247
|
} else {
|
|
@@ -260,7 +329,7 @@ export function registerAskUser(
|
|
|
260
329
|
},
|
|
261
330
|
};
|
|
262
331
|
},
|
|
263
|
-
|
|
332
|
+
{ blocked: { kind: "question", message: params.question } },
|
|
264
333
|
);
|
|
265
334
|
|
|
266
335
|
unsubscribe();
|
|
@@ -284,6 +353,9 @@ export function registerAskUser(
|
|
|
284
353
|
renderResult(result, _options, theme) {
|
|
285
354
|
const text = result.content[0];
|
|
286
355
|
const value = text?.type === "text" ? text.text : "(no response)";
|
|
356
|
+
if (value === DISCUSS_VALUE) {
|
|
357
|
+
return new Text(theme.fg("dim", "💬 Discussion requested"), 0, 0);
|
|
358
|
+
}
|
|
287
359
|
const icon =
|
|
288
360
|
value === "User cancelled"
|
|
289
361
|
? theme.fg("warning", "✗")
|
|
@@ -14,7 +14,6 @@ const require = createRequire(import.meta.url);
|
|
|
14
14
|
|
|
15
15
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
16
16
|
import type { Message } from "@earendil-works/pi-ai";
|
|
17
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
18
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
18
|
import { createLogger } from "../shared/logger.js";
|
|
20
19
|
import { createBridgedUi } from "../shared/ui-bridge.js";
|
|
@@ -107,7 +106,12 @@ const ChainItem = Type.Object({
|
|
|
107
106
|
cwd: Type.String({ description: "Working directory" }),
|
|
108
107
|
estimatedSeconds: Type.Number({ description: "Estimated step duration in seconds" }),
|
|
109
108
|
});
|
|
110
|
-
|
|
109
|
+
// pi 1.5.0's StringEnum returns a pi-schema Schema, which TypeBox's Static cannot
|
|
110
|
+
// read — params.agentScope degraded to `unknown`. Unsafe keeps the JSON-Schema
|
|
111
|
+
// shape providers need while carrying the literal union in the static type.
|
|
112
|
+
const AgentScopeSchema = Type.Unsafe<AgentScope>({
|
|
113
|
+
type: "string",
|
|
114
|
+
enum: ["user", "project", "both"],
|
|
111
115
|
description: 'Agent directories to use. Default: "both".',
|
|
112
116
|
default: "both",
|
|
113
117
|
});
|
|
@@ -263,7 +263,7 @@ export function tryGetSystemPromptOptions(ctx: any): { contextFiles?: any[]; ski
|
|
|
263
263
|
* read from ~/.pi/agent/mcp.json by pi itself), and the built-in MCP/codemode/
|
|
264
264
|
* tool-search extensions become the ones we must not collide with.
|
|
265
265
|
*/
|
|
266
|
-
export const MIN_PI_VERSION = "1.
|
|
266
|
+
export const MIN_PI_VERSION = "1.5.0";
|
|
267
267
|
|
|
268
268
|
/** Get the installed pi version from its package.json. */
|
|
269
269
|
export function getPiVersion(): string | null {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Prefer this over legacy `pi.registerProvider(name, { streamSimple })` bags so
|
|
5
5
|
* providers get /login auth, fetchModels refresh, filterModels, and native
|
|
6
|
-
* ProviderStreams (requires pi >= 1.
|
|
6
|
+
* ProviderStreams (requires pi >= 1.5.0).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createLogger } from "./logger.js";
|
|
@@ -110,6 +110,10 @@ export function createBridgedUi(pi: ExtensionAPI): BridgedUi {
|
|
|
110
110
|
externalDone(resp.value);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
// Report "blocked on user input" (OSC 7501) while the dialog is visible.
|
|
114
|
+
const blocked = { kind: "question" as const, message: message ? `${title} — ${message}` : title };
|
|
115
|
+
log.debug("bridged prompt opened with blocked status", { id, method, kind: blocked.kind });
|
|
116
|
+
|
|
113
117
|
try {
|
|
114
118
|
const result = (await ctx.ui.custom((tui: any, theme: any, _kb: any, done: (v: any) => void) => {
|
|
115
119
|
let resolved = false;
|
|
@@ -215,7 +219,7 @@ export function createBridgedUi(pi: ExtensionAPI): BridgedUi {
|
|
|
215
219
|
tui.requestRender();
|
|
216
220
|
},
|
|
217
221
|
};
|
|
218
|
-
})) as T | undefined;
|
|
222
|
+
}, { blocked })) as T | undefined;
|
|
219
223
|
return result;
|
|
220
224
|
} finally {
|
|
221
225
|
unsubscribe?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-orchestrator-config",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.24.0",
|
|
4
4
|
"description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"extensions/pidiff/pidiff-ui"
|
|
16
16
|
],
|
|
17
17
|
"files": [
|
|
18
|
+
"CHANGELOG.md",
|
|
18
19
|
"extensions/acpx-provider/",
|
|
19
20
|
"extensions/cli-provider/",
|
|
20
21
|
"extensions/providers/",
|
|
@@ -76,10 +77,10 @@
|
|
|
76
77
|
"ws": "^8.0.0"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
|
-
"@earendil-works/pi-agent-core": "^1.
|
|
80
|
-
"@earendil-works/pi-ai": "^1.
|
|
81
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
82
|
-
"@earendil-works/pi-tui": "^1.
|
|
80
|
+
"@earendil-works/pi-agent-core": "^1.5.0",
|
|
81
|
+
"@earendil-works/pi-ai": "^1.5.0",
|
|
82
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
83
|
+
"@earendil-works/pi-tui": "^1.5.0",
|
|
83
84
|
"@types/node": "^22.20.1",
|
|
84
85
|
"jsdom": "^30.1.1",
|
|
85
86
|
"tsx": "^4.0.0"
|
|
@@ -100,6 +101,7 @@
|
|
|
100
101
|
"protobufjs": "7.6.5"
|
|
101
102
|
},
|
|
102
103
|
"pi": {
|
|
104
|
+
"changelogPath": "./CHANGELOG.md",
|
|
103
105
|
"extensions": [
|
|
104
106
|
"./extensions"
|
|
105
107
|
],
|
package/prompts/pr-review.md
CHANGED
|
@@ -44,6 +44,11 @@ If not found, prompt user: "myk-pi-tools is required. Install with: `uv tool ins
|
|
|
44
44
|
- `/pr-review 123` - Review PR #123 in current repo
|
|
45
45
|
- `/pr-review https://github.com/owner/repo/pull/123` - Review from URL
|
|
46
46
|
|
|
47
|
+
Phase 4 asks how you want to decide: `Post all`, `Skip all`, or `Decide one by one`.
|
|
48
|
+
Deciding one by one costs roughly one turn per finding (plus one per discussion round, and one
|
|
49
|
+
more when you skip with a reason or stop early) — choose it when findings need individual
|
|
50
|
+
judgment, not on a 20-finding wall.
|
|
51
|
+
|
|
47
52
|
## Task Plan
|
|
48
53
|
|
|
49
54
|
Before starting any work, create ALL tasks upfront using `TaskCreate`, then set their dependencies
|
|
@@ -522,6 +527,72 @@ This avoids ambiguity — user selections for new findings always refer to the `
|
|
|
522
527
|
- 'none' = Skip posting new findings (previous findings are still auto-posted)
|
|
523
528
|
- Specific numbers = Post only those new findings (numbers refer to the `[NEW]` list)
|
|
524
529
|
|
|
530
|
+
**One ask decides the mode.** After printing the table, call `ask_user` once with question
|
|
531
|
+
`{N} findings — how do you want to decide?` and options `Post all`, `Skip all`, `Decide one by one`.
|
|
532
|
+
Skip this question entirely when there are ZERO `[NEW]` and ZERO `[AUTHOR-QUESTION]` items —
|
|
533
|
+
go straight to Phase 5 with the auto-posted prior findings (see the zero-item rule below).
|
|
534
|
+
Typed answers (`all`, `none`, `3,7,12`) keep working as free text. `ask_user` appends the
|
|
535
|
+
`💬 Discuss with AI first` row itself — never pass it as one of your own options, or the user
|
|
536
|
+
sees it twice.
|
|
537
|
+
|
|
538
|
+
- `Post all` → every `[NEW]` finding is selected. Continue to the refine step in bulk mode.
|
|
539
|
+
- `Skip all` → every `[NEW]` finding is skipped. Collect one blanket reason in bulk mode below.
|
|
540
|
+
- `Decide one by one` → run the per-finding loop below.
|
|
541
|
+
- `💬 Discuss with AI first` (`__discuss__`) → not an answer. Talk about the decision itself —
|
|
542
|
+
trade-offs, what is missing, the alternatives — never a status report on what you just checked —
|
|
543
|
+
then re-ask the same question with the same options once it resolves.
|
|
544
|
+
|
|
545
|
+
**Per-finding loop (chosen by `Decide one by one`):**
|
|
546
|
+
|
|
547
|
+
Everything after this phase — the JSON, the post step, the replies — is unchanged.
|
|
548
|
+
|
|
549
|
+
1. Print the auto-post header block ONCE: `N prior finding(s) will be re-raised (auto-post).`
|
|
550
|
+
`[PREV-*]` findings are never part of the loop.
|
|
551
|
+
2. For each `[AUTHOR-QUESTION]` (Q1..) and then each `[NEW]` finding (1..N), in the stable
|
|
552
|
+
merged order.
|
|
553
|
+
|
|
554
|
+
**`[AUTHOR-QUESTION]` items keep their own flow** — never use the finding template or the
|
|
555
|
+
finding actions for them:
|
|
556
|
+
- Print the original comment (who asked, where, full text) and the AI-generated suggested
|
|
557
|
+
answer from Phase 1c.
|
|
558
|
+
- `ask_user` with question `Question i/N — <one-line summary>` and options `Approve`, `Edit`,
|
|
559
|
+
`Skip`. No `Show more context` (the question body is the context) and no `Skip + reason`
|
|
560
|
+
(the stored reason is `Author question — user chose not to respond`).
|
|
561
|
+
- `Approve` → the suggested answer is posted as-is in Phase 5.
|
|
562
|
+
- `Edit` → print `Answer for Q{i}:` as plain chat text and STOP the turn; the user's next
|
|
563
|
+
message is the revised answer, which Phase 5 posts instead of the suggestion.
|
|
564
|
+
- `Skip` → the question is not answered; it is stored with `skip_reason`
|
|
565
|
+
`Author question — user chose not to respond`.
|
|
566
|
+
|
|
567
|
+
**`[NEW]` findings:**
|
|
568
|
+
a. Print `Finding i/N — <severity> — <file>:<line>`, the source tag, the full description,
|
|
569
|
+
the suggestion block, and which reviewer(s) raised it after dedup.
|
|
570
|
+
b. Call `ask_user` with question `Finding i/N — <one-line summary>` and exactly these options:
|
|
571
|
+
`Post`, `Skip`, `Skip + reason`, `Show more context`, `Stop here`. Do NOT pass
|
|
572
|
+
`Discuss with AI first` — `ask_user` appends that row itself.
|
|
573
|
+
c. Act on the answer:
|
|
574
|
+
- `Post` → finding is selected for posting.
|
|
575
|
+
- `Skip` → finding is skipped. Phase 5b still requires a reason, so store the fixed
|
|
576
|
+
`skip_reason` `Skipped — no reason given`; do not ask the user for one.
|
|
577
|
+
- `Skip + reason` → re-call `ask_user` with question `Reason for finding i/N:` and no
|
|
578
|
+
options, so the answer is free text; use it as the reason and continue with the next
|
|
579
|
+
finding. If the user asks for a multi-line reason, print `Reason for finding i/N:` as
|
|
580
|
+
plain chat text and STOP the turn instead — their next message is the reason.
|
|
581
|
+
- `Show more context` → print the surrounding diff hunk and the affected call sites, then
|
|
582
|
+
re-ask the SAME finding (same index, same options).
|
|
583
|
+
- `Discuss with AI first` → `ask_user` returns exactly `__discuss__`. That is NOT an answer:
|
|
584
|
+
the user wants to talk about this finding. Discuss the finding itself — its trade-offs,
|
|
585
|
+
what you would need to know to judge it, the alternatives — and do not report on your own
|
|
586
|
+
workflow. Re-ask the SAME finding once the discussion resolves. `__discuss__` is a control
|
|
587
|
+
value — never post it, never store it, never write it to `.pi/data/review-guidelines.md`.
|
|
588
|
+
- `Stop here` → every remaining finding is skipped. Ask once for a blanket reason: use
|
|
589
|
+
`ask_user` with no options when one line is enough, otherwise print the prompt as plain
|
|
590
|
+
chat text and STOP the turn. If the user gives no reason, store the fixed
|
|
591
|
+
`skip_reason` `Skipped — no reason given` (Phase 5b rejects an empty one).
|
|
592
|
+
3. The reasons collected here feed the SAME refine → classify → `review-guidelines.md` steps
|
|
593
|
+
below. Only the timing differs: the reason is captured while the user is looking at that
|
|
594
|
+
finding instead of in a later bulk round.
|
|
595
|
+
|
|
525
596
|
If there are ZERO `[NEW]` findings AND zero `[AUTHOR-QUESTION]` findings, skip user
|
|
526
597
|
selection entirely — auto-post the previous findings and proceed directly to Phase 5
|
|
527
598
|
(the auto-post path still generates the comments JSON and posts them — see Phase 5).
|
|
@@ -532,10 +603,16 @@ user approval/edit/skip.
|
|
|
532
603
|
|
|
533
604
|
🚨 **Skip reason collection (MANDATORY — NEVER SKIP THIS STEP):**
|
|
534
605
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
606
|
+
In the per-finding loop the reasons were already captured as you went — go straight to
|
|
607
|
+
step 2 (AI refines each reason) and do not prompt again.
|
|
608
|
+
|
|
609
|
+
**Bulk mode only (Post all / Skip all / numbers) — HARD RULE: If the user skipped ANY finding
|
|
610
|
+
(said 'no' or 'Skip all', selected specific numbers that exclude some findings), you MUST ask
|
|
611
|
+
for skip reasons BEFORE proceeding to Phase 5.** Do NOT continue the workflow without collecting
|
|
612
|
+
skip reasons. Skipping this step is a HARD VIOLATION.
|
|
613
|
+
|
|
614
|
+
If the user declines to give any reason, still store one — Phase 5b rejects an empty
|
|
615
|
+
`skip_reason` — using the fixed text `Skipped — no reason given`.
|
|
539
616
|
|
|
540
617
|
After the user selects which findings to post, compute the skipped set (total `[NEW]`
|
|
541
618
|
findings minus user-selected findings). If the skipped set is non-empty:
|
package/prompts/release.md
CHANGED
|
@@ -128,7 +128,12 @@ root only).
|
|
|
128
128
|
stage ONLY the files in `updated[]` plus the refreshed lockfile
|
|
129
129
|
(`lockfile.path` when `lockfile.refreshed` is true — `refreshed: false`
|
|
130
130
|
is a warning: run `uv lock` yourself before committing); each entry in `packages[]`
|
|
131
|
-
carries its per-package `tag` (`<name>@<version>`).
|
|
131
|
+
carries its per-package `tag` (`<name>@<version>`). When
|
|
132
|
+
`pi-orchestrator-config` is among the bumped packages, also prepend its entry
|
|
133
|
+
to `CHANGELOG.md` — `## [<its new version>] - <YYYY-MM-DD>` plus its S3
|
|
134
|
+
changelog entries, newest first — and stage `CHANGELOG.md` with the bump
|
|
135
|
+
files, so the npm tarball carries the notes pi shows under Extension
|
|
136
|
+
Updates. Bump PR, merge,
|
|
132
137
|
and branch sync follow the standard Phase 5 delegation (git-expert /
|
|
133
138
|
github-expert), with the bump PR message `chore: bump version to
|
|
134
139
|
<name1>@<v1>[, <name2>@<v2>...]` (keep the `chore: bump version`
|
|
@@ -275,6 +280,14 @@ Parse the JSON output from bump-version. Only stage the files listed in the
|
|
|
275
280
|
`updated[]` array. If `skipped[]` is non-empty, inform the user which files were
|
|
276
281
|
skipped and why before proceeding.
|
|
277
282
|
|
|
283
|
+
When `pi-orchestrator-config` (the root `package.json`) was bumped, also write
|
|
284
|
+
its notes into `CHANGELOG.md`: prepend `## [<VERSION>] - <YYYY-MM-DD>` followed
|
|
285
|
+
by that package's changelog entries from Phase 3 (newest first, same bullet
|
|
286
|
+
style as the entries already there), and stage `CHANGELOG.md` with the version
|
|
287
|
+
files. The npm tarball ships the file and pi shows it under Extension Updates on
|
|
288
|
+
the next launch — leave it alone and users keep seeing the previous version's
|
|
289
|
+
notes.
|
|
290
|
+
|
|
278
291
|
**Delegate git and GitHub operations to specialist agents:**
|
|
279
292
|
|
|
280
293
|
1. **Delegate to `git-expert`:** Create branch, stage files, sync lockfile, commit, push:
|