ultimate-pi 0.19.1 → 0.22.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/.agents/skills/harness-decisions/SKILL.md +68 -2
- package/.agents/skills/harness-git-commit/SKILL.md +72 -0
- package/.agents/skills/harness-governor/SKILL.md +2 -2
- package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
- package/.agents/skills/harness-plan/SKILL.md +13 -11
- package/.agents/skills/harness-review/SKILL.md +1 -1
- package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
- package/.agents/skills/sentrux/SKILL.md +4 -2
- package/.agents/skills/wiki-save/SKILL.md +1 -1
- package/.pi/PACKAGING.md +6 -0
- package/.pi/SYSTEM.md +21 -3
- package/.pi/agents/harness/ls-lint-steward.md +49 -0
- package/.pi/agents/harness/planning/decompose.md +4 -4
- package/.pi/agents/harness/reviewing/evaluator.md +1 -1
- package/.pi/agents/harness/running/executor.md +43 -2
- package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
- package/.pi/agents/pi-pi/prompt-expert.md +17 -2
- package/.pi/auto-commit.json +9 -2
- package/.pi/extensions/debate-orchestrator.ts +3 -0
- package/.pi/extensions/harness-anchored-edit.ts +139 -0
- package/.pi/extensions/harness-ask-user.ts +13 -34
- package/.pi/extensions/harness-debate-tools.ts +43 -4
- package/.pi/extensions/harness-live-widget.ts +28 -19
- package/.pi/extensions/harness-run-context.ts +278 -115
- package/.pi/extensions/harness-web-tools.ts +598 -471
- package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
- package/.pi/extensions/observation-bus.ts +4 -0
- package/.pi/extensions/policy-gate.ts +270 -229
- package/.pi/extensions/sentrux-rules-sync.ts +2 -0
- package/.pi/extensions/soundboard.ts +48 -48
- package/.pi/harness/README.md +4 -0
- package/.pi/harness/agents.manifest.json +15 -7
- package/.pi/harness/agents.policy.yaml +47 -81
- package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
- package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
- package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
- package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
- package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
- package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
- package/.pi/harness/docs/adrs/README.md +7 -0
- package/.pi/harness/docs/practice-map.md +21 -5
- package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
- package/.pi/harness/evolution/self-healing-rules.json +16 -0
- package/.pi/harness/ls-lint/naming.manifest.json +128 -0
- package/.pi/harness/sentrux/architecture.manifest.json +1 -1
- package/.pi/harness/specs/auto-commit.schema.json +63 -0
- package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
- package/.pi/harness/specs/naming-manifest.schema.json +54 -0
- package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
- package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
- package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
- package/.pi/harness/specs/sentrux-report.schema.json +119 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
- package/.pi/lib/agents-policy.d.mts +26 -47
- package/.pi/lib/agents-policy.mjs +84 -29
- package/.pi/lib/agents-policy.ts +1 -0
- package/.pi/lib/agt/build-evaluation-context.ts +136 -64
- package/.pi/lib/ask-user/constants.mjs +3 -0
- package/.pi/lib/ask-user/constants.ts +4 -0
- package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
- package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
- package/.pi/lib/ask-user/dialog.ts +2 -314
- package/.pi/lib/ask-user/fallback.ts +2 -78
- package/.pi/lib/ask-user/format.ts +85 -0
- package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
- package/.pi/lib/ask-user/index.ts +114 -0
- package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
- package/.pi/lib/ask-user/policy.mjs +43 -0
- package/.pi/lib/ask-user/policy.ts +104 -0
- package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
- package/.pi/lib/ask-user/presenters/headless.ts +131 -0
- package/.pi/lib/ask-user/presenters/select.ts +60 -0
- package/.pi/lib/ask-user/presenters/tui.ts +373 -0
- package/.pi/lib/ask-user/presenters/types.ts +13 -0
- package/.pi/lib/ask-user/render.ts +40 -9
- package/.pi/lib/ask-user/schema.ts +66 -13
- package/.pi/lib/ask-user/types.ts +60 -3
- package/.pi/lib/ask-user/validate-core.mjs +193 -7
- package/.pi/lib/ask-user/validate.ts +53 -34
- package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
- package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
- package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
- package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
- package/.pi/lib/harness-anchored-edit/index.ts +9 -0
- package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
- package/.pi/lib/harness-anchored-edit/package.json +3 -0
- package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
- package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
- package/.pi/lib/harness-anchored-edit/types.ts +19 -0
- package/.pi/lib/harness-artifact-gate.ts +75 -21
- package/.pi/lib/harness-auto-commit-config.mjs +321 -0
- package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
- package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
- package/.pi/lib/harness-lens/index.ts +246 -96
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
- package/.pi/lib/harness-repair-brief.ts +84 -25
- package/.pi/lib/harness-run-context.ts +42 -52
- package/.pi/lib/harness-sentrux-parse.mjs +272 -0
- package/.pi/lib/harness-sentrux-root.mjs +78 -0
- package/.pi/lib/harness-slash-completions.ts +116 -0
- package/.pi/lib/harness-spawn-topology.ts +121 -87
- package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
- package/.pi/lib/harness-subagents-bridge.ts +11 -6
- package/.pi/lib/harness-ui-state.ts +95 -48
- package/.pi/lib/plan-approval/dialog.ts +5 -0
- package/.pi/lib/plan-approval/validate.ts +1 -1
- package/.pi/lib/plan-approval-readiness.ts +32 -0
- package/.pi/lib/plan-debate-gate.ts +154 -114
- package/.pi/lib/plan-task-clarification.ts +158 -0
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-ls-lint-steward.md +43 -0
- package/.pi/prompts/harness-plan.md +58 -8
- package/.pi/prompts/harness-review.md +40 -6
- package/.pi/prompts/harness-run.md +33 -11
- package/.pi/prompts/harness-setup.md +72 -3
- package/.pi/prompts/harness-steer.md +3 -2
- package/.pi/prompts/wiki-save.md +5 -4
- package/.pi/scripts/README.md +8 -0
- package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
- package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
- package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
- package/.pi/scripts/harness-cli-verify.sh +47 -0
- package/.pi/scripts/harness-git-churn.mjs +77 -0
- package/.pi/scripts/harness-git-commit.mjs +173 -0
- package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
- package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
- package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
- package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
- package/.pi/scripts/harness-sentrux-report.mjs +256 -0
- package/.pi/scripts/harness-verify.mjs +347 -117
- package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
- package/.pi/scripts/run-tests.mjs +65 -0
- package/.pi/settings.example.json +1 -0
- package/.sentrux/rules.toml +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +31 -0
- package/README.md +13 -4
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +8 -3
- package/vendor/pi-subagents/src/agents.ts +5 -0
- package/vendor/pi-subagents/src/subagents.ts +22 -3
- package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
- package/.pi/scripts/release.sh +0 -338
|
@@ -1,314 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Editor,
|
|
4
|
-
type EditorTheme,
|
|
5
|
-
Key,
|
|
6
|
-
matchesKey,
|
|
7
|
-
truncateToWidth,
|
|
8
|
-
} from "@earendil-works/pi-tui";
|
|
9
|
-
import type { AskResponse, DialogResult, ValidatedAskParams } from "./types.js";
|
|
10
|
-
|
|
11
|
-
type DisplayOption = {
|
|
12
|
-
title: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
isFreeform?: boolean;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
interface CustomAnswer {
|
|
18
|
-
response: AskResponse;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type ThemeLike = { fg(color: string, text: string): string };
|
|
22
|
-
type TuiLike = ConstructorParameters<typeof Editor>[0] & {
|
|
23
|
-
requestRender(): void;
|
|
24
|
-
};
|
|
25
|
-
type Done = (answer: CustomAnswer | null) => void;
|
|
26
|
-
|
|
27
|
-
function withTimeout<T>(
|
|
28
|
-
promise: Promise<T | null>,
|
|
29
|
-
ms: number | undefined,
|
|
30
|
-
): Promise<T | null> {
|
|
31
|
-
if (!ms) return promise;
|
|
32
|
-
return Promise.race([
|
|
33
|
-
promise,
|
|
34
|
-
new Promise<null>((resolve) => {
|
|
35
|
-
setTimeout(() => resolve(null), ms);
|
|
36
|
-
}),
|
|
37
|
-
]);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function displayOptionsFor(validated: ValidatedAskParams): DisplayOption[] {
|
|
41
|
-
const displayOptions: DisplayOption[] = [...validated.options];
|
|
42
|
-
if (validated.allowFreeform) {
|
|
43
|
-
displayOptions.push({ title: "Type something…", isFreeform: true });
|
|
44
|
-
}
|
|
45
|
-
return displayOptions;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function runFreeformOnly(
|
|
49
|
-
ui: ExtensionUIContext,
|
|
50
|
-
question: string,
|
|
51
|
-
): Promise<DialogResult> {
|
|
52
|
-
const text = await ui.input(question, "");
|
|
53
|
-
if (!text?.trim()) return { response: null, cancelled: true };
|
|
54
|
-
return {
|
|
55
|
-
response: { kind: "freeform", text: text.trim() },
|
|
56
|
-
cancelled: false,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function editorThemeFor(theme: ThemeLike): EditorTheme {
|
|
61
|
-
return {
|
|
62
|
-
borderColor: (s) => theme.fg("accent", s),
|
|
63
|
-
selectList: {
|
|
64
|
-
selectedPrefix: (t) => theme.fg("accent", t),
|
|
65
|
-
selectedText: (t) => theme.fg("accent", t),
|
|
66
|
-
description: (t) => theme.fg("muted", t),
|
|
67
|
-
scrollInfo: (t) => theme.fg("dim", t),
|
|
68
|
-
noMatch: (t) => theme.fg("warning", t),
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
class AskDialogController {
|
|
74
|
-
private optionIndex = 0;
|
|
75
|
-
private editMode = false;
|
|
76
|
-
private readonly selected = new Set<number>();
|
|
77
|
-
private cachedLines: string[] | undefined;
|
|
78
|
-
private readonly editor: Editor;
|
|
79
|
-
|
|
80
|
-
constructor(
|
|
81
|
-
private readonly validated: ValidatedAskParams,
|
|
82
|
-
private readonly displayOptions: DisplayOption[],
|
|
83
|
-
private readonly tui: TuiLike,
|
|
84
|
-
private readonly theme: ThemeLike,
|
|
85
|
-
private readonly done: Done,
|
|
86
|
-
) {
|
|
87
|
-
this.editor = new Editor(tui, editorThemeFor(theme));
|
|
88
|
-
this.editor.onSubmit = (value) => this.submitFreeform(value);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
invalidate(): void {
|
|
92
|
-
this.cachedLines = undefined;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
handleInput(data: string): void {
|
|
96
|
-
if (this.editMode) {
|
|
97
|
-
this.handleEditInput(data);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (this.handleNavigationInput(data)) return;
|
|
101
|
-
if (this.validated.allowMultiple && matchesKey(data, Key.space)) {
|
|
102
|
-
this.toggleMultiSelect();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (matchesKey(data, Key.enter)) this.submitSelection();
|
|
106
|
-
if (matchesKey(data, Key.escape)) this.done(null);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
render(width: number): string[] {
|
|
110
|
-
if (this.cachedLines) return this.cachedLines;
|
|
111
|
-
const lines: string[] = [];
|
|
112
|
-
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
113
|
-
const useOverlay = this.validated.displayMode !== "inline";
|
|
114
|
-
this.renderHeader(lines, add, width, useOverlay);
|
|
115
|
-
this.renderOptions(add);
|
|
116
|
-
this.renderEditor(lines, add, width);
|
|
117
|
-
this.renderFooter(add, width, useOverlay);
|
|
118
|
-
this.cachedLines = lines;
|
|
119
|
-
return lines;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private refresh(): void {
|
|
123
|
-
this.invalidate();
|
|
124
|
-
this.tui.requestRender();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private submitFreeform(value: string): void {
|
|
128
|
-
const trimmed = value.trim();
|
|
129
|
-
if (trimmed) {
|
|
130
|
-
this.done({ response: { kind: "freeform", text: trimmed } });
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
this.editMode = false;
|
|
134
|
-
this.editor.setText("");
|
|
135
|
-
this.refresh();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private submitSelection(): void {
|
|
139
|
-
if (this.validated.allowMultiple) {
|
|
140
|
-
const titles = [...this.selected]
|
|
141
|
-
.sort((a, b) => a - b)
|
|
142
|
-
.map((i) => this.displayOptions[i].title)
|
|
143
|
-
.filter((t) => t !== "Type something…");
|
|
144
|
-
if (titles.length) {
|
|
145
|
-
this.done({ response: { kind: "selection", selections: titles } });
|
|
146
|
-
}
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const opt = this.displayOptions[this.optionIndex];
|
|
150
|
-
if (opt.isFreeform) {
|
|
151
|
-
this.editMode = true;
|
|
152
|
-
this.refresh();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
this.done({ response: { kind: "selection", selections: [opt.title] } });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private handleEditInput(data: string): void {
|
|
159
|
-
if (matchesKey(data, Key.escape)) {
|
|
160
|
-
this.editMode = false;
|
|
161
|
-
this.editor.setText("");
|
|
162
|
-
} else {
|
|
163
|
-
this.editor.handleInput(data);
|
|
164
|
-
}
|
|
165
|
-
this.refresh();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private handleNavigationInput(data: string): boolean {
|
|
169
|
-
if (matchesKey(data, Key.up)) {
|
|
170
|
-
this.optionIndex = Math.max(0, this.optionIndex - 1);
|
|
171
|
-
this.refresh();
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
if (matchesKey(data, Key.down)) {
|
|
175
|
-
this.optionIndex = Math.min(
|
|
176
|
-
this.displayOptions.length - 1,
|
|
177
|
-
this.optionIndex + 1,
|
|
178
|
-
);
|
|
179
|
-
this.refresh();
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private toggleMultiSelect(): void {
|
|
186
|
-
const opt = this.displayOptions[this.optionIndex];
|
|
187
|
-
if (opt.isFreeform) return;
|
|
188
|
-
if (this.selected.has(this.optionIndex)) {
|
|
189
|
-
this.selected.delete(this.optionIndex);
|
|
190
|
-
} else {
|
|
191
|
-
this.selected.add(this.optionIndex);
|
|
192
|
-
}
|
|
193
|
-
this.refresh();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private renderHeader(
|
|
197
|
-
lines: string[],
|
|
198
|
-
add: (s: string) => void,
|
|
199
|
-
width: number,
|
|
200
|
-
useOverlay: boolean,
|
|
201
|
-
): void {
|
|
202
|
-
if (useOverlay) add(this.theme.fg("accent", "─".repeat(width)));
|
|
203
|
-
if (this.validated.context) {
|
|
204
|
-
for (const line of this.validated.context.split("\n")) {
|
|
205
|
-
add(this.theme.fg("muted", ` ${line}`));
|
|
206
|
-
}
|
|
207
|
-
lines.push("");
|
|
208
|
-
}
|
|
209
|
-
add(this.theme.fg("text", ` ${this.validated.question}`));
|
|
210
|
-
lines.push("");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private renderOptions(add: (s: string) => void): void {
|
|
214
|
-
for (let i = 0; i < this.displayOptions.length; i++) {
|
|
215
|
-
const opt = this.displayOptions[i];
|
|
216
|
-
const prefix = this.optionPrefix(i, opt.isFreeform === true);
|
|
217
|
-
const label = this.optionLabel(i, opt);
|
|
218
|
-
add(`${prefix}${label}`);
|
|
219
|
-
if (opt.description)
|
|
220
|
-
add(` ${this.theme.fg("muted", opt.description)}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private optionPrefix(index: number, isFreeform: boolean): string {
|
|
225
|
-
if (!this.validated.allowMultiple) {
|
|
226
|
-
return index === this.optionIndex ? this.theme.fg("accent", "> ") : " ";
|
|
227
|
-
}
|
|
228
|
-
if (isFreeform) return " ";
|
|
229
|
-
return this.selected.has(index) ? this.theme.fg("accent", "[x] ") : "[ ] ";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private optionLabel(index: number, opt: DisplayOption): string {
|
|
233
|
-
const raw = `${index + 1}. ${opt.title}`;
|
|
234
|
-
const focused = index === this.optionIndex;
|
|
235
|
-
if (opt.isFreeform && this.editMode && focused) {
|
|
236
|
-
return this.theme.fg("accent", `${raw} ✎`);
|
|
237
|
-
}
|
|
238
|
-
if (focused && !this.validated.allowMultiple) {
|
|
239
|
-
return this.theme.fg("accent", raw);
|
|
240
|
-
}
|
|
241
|
-
return this.theme.fg("text", raw);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private renderEditor(
|
|
245
|
-
lines: string[],
|
|
246
|
-
add: (s: string) => void,
|
|
247
|
-
width: number,
|
|
248
|
-
): void {
|
|
249
|
-
if (!this.editMode) return;
|
|
250
|
-
lines.push("");
|
|
251
|
-
add(this.theme.fg("muted", " Your answer:"));
|
|
252
|
-
for (const line of this.editor.render(width - 2)) add(` ${line}`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private renderFooter(
|
|
256
|
-
add: (s: string) => void,
|
|
257
|
-
width: number,
|
|
258
|
-
useOverlay: boolean,
|
|
259
|
-
): void {
|
|
260
|
-
add("");
|
|
261
|
-
if (this.editMode) {
|
|
262
|
-
add(this.theme.fg("dim", " Enter to submit • Esc to go back"));
|
|
263
|
-
} else if (this.validated.allowMultiple) {
|
|
264
|
-
add(
|
|
265
|
-
this.theme.fg(
|
|
266
|
-
"dim",
|
|
267
|
-
" ↑↓ navigate • Space toggle • Enter confirm • Esc cancel",
|
|
268
|
-
),
|
|
269
|
-
);
|
|
270
|
-
} else {
|
|
271
|
-
add(
|
|
272
|
-
this.theme.fg("dim", " ↑↓ navigate • Enter to select • Esc to cancel"),
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
if (useOverlay) add(this.theme.fg("accent", "─".repeat(width)));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async function runOptionDialog(
|
|
280
|
-
ui: ExtensionUIContext,
|
|
281
|
-
validated: ValidatedAskParams,
|
|
282
|
-
displayOptions: DisplayOption[],
|
|
283
|
-
): Promise<CustomAnswer | null> {
|
|
284
|
-
return withTimeout(
|
|
285
|
-
ui.custom<CustomAnswer | null>((tui, theme, _kb, done) => {
|
|
286
|
-
const controller = new AskDialogController(
|
|
287
|
-
validated,
|
|
288
|
-
displayOptions,
|
|
289
|
-
tui as TuiLike,
|
|
290
|
-
theme,
|
|
291
|
-
done,
|
|
292
|
-
);
|
|
293
|
-
return {
|
|
294
|
-
render: (width: number) => controller.render(width),
|
|
295
|
-
invalidate: () => controller.invalidate(),
|
|
296
|
-
handleInput: (data: string) => controller.handleInput(data),
|
|
297
|
-
};
|
|
298
|
-
}),
|
|
299
|
-
validated.timeout,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export async function runAskDialog(
|
|
304
|
-
ui: ExtensionUIContext,
|
|
305
|
-
validated: ValidatedAskParams,
|
|
306
|
-
): Promise<DialogResult> {
|
|
307
|
-
const displayOptions = displayOptionsFor(validated);
|
|
308
|
-
if (displayOptions.length === 0) {
|
|
309
|
-
return runFreeformOnly(ui, validated.question);
|
|
310
|
-
}
|
|
311
|
-
const result = await runOptionDialog(ui, validated, displayOptions);
|
|
312
|
-
if (!result) return { response: null, cancelled: true };
|
|
313
|
-
return { response: result.response, cancelled: false };
|
|
314
|
-
}
|
|
1
|
+
/** @deprecated Import from ./presenters/tui.js */
|
|
2
|
+
export { runAskDialog, runTuiPresenter } from "./presenters/tui.js";
|
|
@@ -1,78 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export async function runAskFallback(
|
|
5
|
-
ui: ExtensionUIContext,
|
|
6
|
-
validated: ValidatedAskParams,
|
|
7
|
-
): Promise<DialogResult> {
|
|
8
|
-
const { question, context, options, allowMultiple, allowFreeform } =
|
|
9
|
-
validated;
|
|
10
|
-
|
|
11
|
-
const title = context ? `${context}\n\n${question}` : question;
|
|
12
|
-
const labels = options.map((o) => o.title);
|
|
13
|
-
|
|
14
|
-
if (labels.length === 0) {
|
|
15
|
-
if (!allowFreeform) {
|
|
16
|
-
return { response: null, cancelled: true };
|
|
17
|
-
}
|
|
18
|
-
const text = await ui.input(title, "");
|
|
19
|
-
if (!text?.trim()) {
|
|
20
|
-
return { response: null, cancelled: true };
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
response: { kind: "freeform", text: text.trim() },
|
|
24
|
-
cancelled: false,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (allowMultiple) {
|
|
29
|
-
const selections: string[] = [];
|
|
30
|
-
const remaining = [...labels];
|
|
31
|
-
while (remaining.length > 0) {
|
|
32
|
-
const pick = await ui.select(
|
|
33
|
-
selections.length === 0
|
|
34
|
-
? title
|
|
35
|
-
: `${title}\n(selected: ${selections.join(", ")})`,
|
|
36
|
-
[...remaining, "(done selecting)"],
|
|
37
|
-
);
|
|
38
|
-
if (!pick) {
|
|
39
|
-
return { response: null, cancelled: true };
|
|
40
|
-
}
|
|
41
|
-
if (pick === "(done selecting)") {
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
selections.push(pick);
|
|
45
|
-
const idx = remaining.indexOf(pick);
|
|
46
|
-
if (idx >= 0) remaining.splice(idx, 1);
|
|
47
|
-
}
|
|
48
|
-
if (selections.length === 0) {
|
|
49
|
-
return { response: null, cancelled: true };
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
response: { kind: "selection", selections },
|
|
53
|
-
cancelled: false,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const choices = allowFreeform ? [...labels, "Type something…"] : labels;
|
|
58
|
-
const picked = await ui.select(title, choices);
|
|
59
|
-
if (!picked) {
|
|
60
|
-
return { response: null, cancelled: true };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (picked === "Type something…") {
|
|
64
|
-
const text = await ui.input(question, "");
|
|
65
|
-
if (!text?.trim()) {
|
|
66
|
-
return { response: null, cancelled: true };
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
response: { kind: "freeform", text: text.trim() },
|
|
70
|
-
cancelled: false,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
response: { kind: "selection", selections: [picked] },
|
|
76
|
-
cancelled: false,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
1
|
+
/** @deprecated Import from ./presenters/headless.js */
|
|
2
|
+
export { runAskFallback, runHeadlessPresenter } from "./presenters/headless.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AskResponse,
|
|
3
|
+
AskToolDetails,
|
|
4
|
+
UiBackend,
|
|
5
|
+
ValidatedAskParams,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
|
|
8
|
+
export function formatResultText(
|
|
9
|
+
response: AskResponse | null,
|
|
10
|
+
cancelled: boolean,
|
|
11
|
+
opts?: { ui_degraded?: boolean },
|
|
12
|
+
): string {
|
|
13
|
+
if (cancelled || !response) {
|
|
14
|
+
return "User cancelled (no answer)";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (opts?.ui_degraded) {
|
|
18
|
+
return `Rich ask UI unavailable; using terminal prompt.\n\n${formatResponseBody(response)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return formatResponseBody(response);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatResponseBody(response: AskResponse): string {
|
|
25
|
+
if (response.kind === "freeform") {
|
|
26
|
+
const base = `User wrote: ${response.text}`;
|
|
27
|
+
return appendComments(base, response.additionalComments);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (response.kind === "questionnaire") {
|
|
31
|
+
const lines = response.questionnaireDetails.map((d) => {
|
|
32
|
+
let line = `- ${d.question}: ${d.answer}`;
|
|
33
|
+
if (d.comment) line += ` (comment: ${d.comment})`;
|
|
34
|
+
return line;
|
|
35
|
+
});
|
|
36
|
+
const body = `User answered questionnaire:\n${lines.join("\n")}`;
|
|
37
|
+
return appendComments(body, response.additionalComments);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sel = response.selections;
|
|
41
|
+
let base: string;
|
|
42
|
+
if (sel.length === 1) {
|
|
43
|
+
base = `User selected: ${sel[0]}`;
|
|
44
|
+
} else {
|
|
45
|
+
base = `User selected: ${sel.join(", ")}`;
|
|
46
|
+
}
|
|
47
|
+
const withComment = response.comment
|
|
48
|
+
? `${base}\nComment: ${response.comment}`
|
|
49
|
+
: base;
|
|
50
|
+
return appendComments(withComment, response.additionalComments);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function appendComments(body: string, additional?: string): string {
|
|
54
|
+
if (!additional?.trim()) return body;
|
|
55
|
+
return `${body}\nAdditional comments: ${additional.trim()}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function optionTitlesForDetails(
|
|
59
|
+
validated: ValidatedAskParams,
|
|
60
|
+
): string[] {
|
|
61
|
+
if (validated.mode === "questionnaire") {
|
|
62
|
+
return validated.questions.map((q) => q.title);
|
|
63
|
+
}
|
|
64
|
+
return validated.options.map((o) => o.title);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toToolDetails(
|
|
68
|
+
validated: ValidatedAskParams,
|
|
69
|
+
response: AskResponse | null,
|
|
70
|
+
cancelled: boolean,
|
|
71
|
+
ui_backend: UiBackend,
|
|
72
|
+
opts?: { ui_degraded?: boolean; non_interactive_blocked?: boolean },
|
|
73
|
+
): AskToolDetails {
|
|
74
|
+
return {
|
|
75
|
+
question: validated.question,
|
|
76
|
+
context: validated.context,
|
|
77
|
+
contextFormat: validated.contextFormat,
|
|
78
|
+
options: optionTitlesForDetails(validated),
|
|
79
|
+
response,
|
|
80
|
+
cancelled,
|
|
81
|
+
ui_backend,
|
|
82
|
+
ui_degraded: opts?.ui_degraded,
|
|
83
|
+
non_interactive_blocked: opts?.non_interactive_blocked,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatResultText, toToolDetails } from "./format.js";
|
|
3
|
+
import {
|
|
4
|
+
isHarnessNonInteractive,
|
|
5
|
+
isPlanApprovalAskUser,
|
|
6
|
+
nonInteractiveAskUserResult,
|
|
7
|
+
} from "./policy.js";
|
|
8
|
+
import { presentAskUser } from "./presenters/select.js";
|
|
9
|
+
import type { AskUserParams, RunAskUserResult } from "./types.js";
|
|
10
|
+
import { validateAskParams } from "./validate.js";
|
|
11
|
+
|
|
12
|
+
export { buildGlimpsePayload } from "./contracts/glimpse-payload-build.js";
|
|
13
|
+
export { formatResultText, toToolDetails } from "./format.js";
|
|
14
|
+
export { applyAskUserToTaskClarification } from "./merge-task-clarification.js";
|
|
15
|
+
export {
|
|
16
|
+
assertSubagentCannotAskUser,
|
|
17
|
+
isHarnessNonInteractive,
|
|
18
|
+
isPlanApprovalAskUser,
|
|
19
|
+
nonInteractiveAskUserResult,
|
|
20
|
+
PLAN_APPROVE_OPTION,
|
|
21
|
+
PLAN_CANCEL_OPTION,
|
|
22
|
+
} from "./policy.js";
|
|
23
|
+
export {
|
|
24
|
+
glimpseHealthCheck,
|
|
25
|
+
isGlimpseAvailable,
|
|
26
|
+
} from "./presenters/glimpse.js";
|
|
27
|
+
export {
|
|
28
|
+
AskUserParamsSchema,
|
|
29
|
+
PROMPT_GUIDELINES,
|
|
30
|
+
PROMPT_SNIPPET,
|
|
31
|
+
} from "./schema.js";
|
|
32
|
+
export type {
|
|
33
|
+
AskResponse,
|
|
34
|
+
AskToolDetails,
|
|
35
|
+
AskUserParams,
|
|
36
|
+
DialogResult,
|
|
37
|
+
ValidatedAskParams,
|
|
38
|
+
} from "./types.js";
|
|
39
|
+
export { normalizeOption, validateAskParams } from "./validate.js";
|
|
40
|
+
|
|
41
|
+
export interface RunAskUserContext {
|
|
42
|
+
ui: ExtensionUIContext;
|
|
43
|
+
hasUI: boolean;
|
|
44
|
+
sessionName?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runAskUser(
|
|
48
|
+
params: AskUserParams,
|
|
49
|
+
ctx: RunAskUserContext,
|
|
50
|
+
): Promise<
|
|
51
|
+
| RunAskUserResult
|
|
52
|
+
| { error: string; details: Partial<import("./types.js").AskToolDetails> }
|
|
53
|
+
> {
|
|
54
|
+
if (isPlanApprovalAskUser(params)) {
|
|
55
|
+
return {
|
|
56
|
+
error:
|
|
57
|
+
"ask_user must not be used for plan approval — call approve_plan with the PlanPacket.",
|
|
58
|
+
details: {
|
|
59
|
+
question: params.question ?? "",
|
|
60
|
+
options: [],
|
|
61
|
+
response: null,
|
|
62
|
+
cancelled: true,
|
|
63
|
+
ui_backend: "headless",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isHarnessNonInteractive()) {
|
|
69
|
+
const blocked = nonInteractiveAskUserResult(params.question ?? "");
|
|
70
|
+
return {
|
|
71
|
+
error: blocked.text,
|
|
72
|
+
details: blocked.details as import("./types.js").AskToolDetails,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const validated = validateAskParams(params);
|
|
77
|
+
if (typeof validated === "string") {
|
|
78
|
+
return {
|
|
79
|
+
error: validated,
|
|
80
|
+
details: {
|
|
81
|
+
question: params.question ?? "",
|
|
82
|
+
options: [],
|
|
83
|
+
response: null,
|
|
84
|
+
cancelled: true,
|
|
85
|
+
ui_backend: "headless",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const outcome = await presentAskUser(validated, {
|
|
91
|
+
ui: ctx.ui,
|
|
92
|
+
hasUI: ctx.hasUI,
|
|
93
|
+
sessionName: ctx.sessionName,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const details = toToolDetails(
|
|
97
|
+
validated,
|
|
98
|
+
outcome.response,
|
|
99
|
+
outcome.cancelled,
|
|
100
|
+
outcome.ui_backend,
|
|
101
|
+
{
|
|
102
|
+
ui_degraded: outcome.ui_degraded,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const text = formatResultText(outcome.response, outcome.cancelled, {
|
|
107
|
+
ui_degraded: outcome.ui_degraded,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text }],
|
|
112
|
+
details,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AskToolDetails } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type TaskClarificationDoc = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
const TITLE_ALIASES: Record<string, string[]> = {
|
|
6
|
+
success_definition: [
|
|
7
|
+
"done",
|
|
8
|
+
"success",
|
|
9
|
+
"success criteria",
|
|
10
|
+
"what does done look like",
|
|
11
|
+
],
|
|
12
|
+
risk_level: ["risk", "risk level"],
|
|
13
|
+
in_scope: ["scope", "in scope", "what is in scope"],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function normalizeKey(s: string): string {
|
|
17
|
+
return s
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchField(title: string): string | null {
|
|
24
|
+
const n = normalizeKey(title);
|
|
25
|
+
for (const [field, aliases] of Object.entries(TITLE_ALIASES)) {
|
|
26
|
+
if (aliases.some((a) => n.includes(normalizeKey(a)))) return field;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseRiskLevel(answer: string): "low" | "med" | "high" | null {
|
|
32
|
+
const n = answer.toLowerCase();
|
|
33
|
+
if (/\blow\b/.test(n)) return "low";
|
|
34
|
+
if (/\bhigh\b/.test(n)) return "high";
|
|
35
|
+
if (/\bmed/.test(n) || /\bmedium\b/.test(n)) return "med";
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Merge ask_user questionnaire or flat answers into a task-clarification draft.
|
|
41
|
+
*/
|
|
42
|
+
export function applyAskUserToTaskClarification(
|
|
43
|
+
doc: TaskClarificationDoc,
|
|
44
|
+
details: AskToolDetails,
|
|
45
|
+
): TaskClarificationDoc {
|
|
46
|
+
const next = { ...doc };
|
|
47
|
+
const assumptions = Array.isArray(next.assumptions)
|
|
48
|
+
? [...(next.assumptions as string[])]
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
if (!details.response || details.cancelled) {
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const applyPair = (question: string, answer: string) => {
|
|
56
|
+
const field = matchField(question);
|
|
57
|
+
if (field === "success_definition") {
|
|
58
|
+
next.success_definition = answer;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (field === "risk_level") {
|
|
62
|
+
const risk = parseRiskLevel(answer);
|
|
63
|
+
if (risk) next.risk_level = risk;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (field === "in_scope") {
|
|
67
|
+
const items = answer
|
|
68
|
+
.split(/[,;\n]/)
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
if (items.length) next.in_scope = items;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
assumptions.push(`${question}: ${answer}`);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const r = details.response;
|
|
78
|
+
if (r.kind === "questionnaire") {
|
|
79
|
+
for (const d of r.questionnaireDetails) {
|
|
80
|
+
applyPair(d.question, d.answer);
|
|
81
|
+
}
|
|
82
|
+
} else if (r.kind === "freeform") {
|
|
83
|
+
applyPair(details.question, r.text);
|
|
84
|
+
} else if (r.kind === "selection") {
|
|
85
|
+
applyPair(details.question, r.selections.join(", "));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (assumptions.length) next.assumptions = assumptions;
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(next.unresolved_questions)) {
|
|
91
|
+
next.unresolved_questions = [];
|
|
92
|
+
}
|
|
93
|
+
if (next.status === "draft" || next.status === "needs_user") {
|
|
94
|
+
next.status = "needs_user";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return next;
|
|
98
|
+
}
|