ultimate-pi 0.20.0 → 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.
Files changed (130) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +68 -2
  2. package/.agents/skills/harness-git-commit/SKILL.md +72 -0
  3. package/.agents/skills/harness-governor/SKILL.md +2 -2
  4. package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
  5. package/.agents/skills/harness-plan/SKILL.md +13 -11
  6. package/.agents/skills/harness-review/SKILL.md +1 -1
  7. package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
  8. package/.agents/skills/sentrux/SKILL.md +4 -2
  9. package/.agents/skills/wiki-save/SKILL.md +1 -1
  10. package/.pi/PACKAGING.md +6 -0
  11. package/.pi/SYSTEM.md +21 -3
  12. package/.pi/agents/harness/ls-lint-steward.md +49 -0
  13. package/.pi/agents/harness/planning/decompose.md +4 -4
  14. package/.pi/agents/harness/reviewing/evaluator.md +1 -1
  15. package/.pi/agents/harness/running/executor.md +1 -1
  16. package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
  17. package/.pi/agents/pi-pi/prompt-expert.md +17 -2
  18. package/.pi/auto-commit.json +9 -2
  19. package/.pi/extensions/debate-orchestrator.ts +3 -0
  20. package/.pi/extensions/harness-anchored-edit.ts +7 -9
  21. package/.pi/extensions/harness-ask-user.ts +13 -34
  22. package/.pi/extensions/harness-debate-tools.ts +43 -4
  23. package/.pi/extensions/harness-live-widget.ts +28 -19
  24. package/.pi/extensions/harness-run-context.ts +278 -115
  25. package/.pi/extensions/harness-web-tools.ts +598 -471
  26. package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
  27. package/.pi/extensions/observation-bus.ts +4 -0
  28. package/.pi/extensions/policy-gate.ts +270 -229
  29. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  30. package/.pi/extensions/soundboard.ts +48 -48
  31. package/.pi/harness/README.md +4 -0
  32. package/.pi/harness/agents.manifest.json +15 -7
  33. package/.pi/harness/agents.policy.yaml +49 -82
  34. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  35. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  36. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  37. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  38. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  39. package/.pi/harness/docs/adrs/README.md +5 -0
  40. package/.pi/harness/docs/practice-map.md +10 -5
  41. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  42. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  43. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  44. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  45. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  46. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  47. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  48. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  49. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  50. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  51. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  52. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  53. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  54. package/.pi/lib/agents-policy.d.mts +26 -51
  55. package/.pi/lib/agents-policy.mjs +41 -28
  56. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  57. package/.pi/lib/ask-user/constants.mjs +3 -0
  58. package/.pi/lib/ask-user/constants.ts +4 -0
  59. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  60. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  61. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  62. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  63. package/.pi/lib/ask-user/dialog.ts +2 -314
  64. package/.pi/lib/ask-user/fallback.ts +2 -78
  65. package/.pi/lib/ask-user/format.ts +85 -0
  66. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  67. package/.pi/lib/ask-user/index.ts +114 -0
  68. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  69. package/.pi/lib/ask-user/policy.mjs +43 -0
  70. package/.pi/lib/ask-user/policy.ts +104 -0
  71. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  72. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  73. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  74. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  75. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  76. package/.pi/lib/ask-user/render.ts +40 -9
  77. package/.pi/lib/ask-user/schema.ts +66 -13
  78. package/.pi/lib/ask-user/types.ts +60 -3
  79. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  80. package/.pi/lib/ask-user/validate.ts +53 -34
  81. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  82. package/.pi/lib/harness-artifact-gate.ts +75 -21
  83. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  84. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  85. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  86. package/.pi/lib/harness-lens/index.ts +241 -108
  87. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  88. package/.pi/lib/harness-repair-brief.ts +84 -25
  89. package/.pi/lib/harness-run-context.ts +42 -52
  90. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  91. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  92. package/.pi/lib/harness-slash-completions.ts +116 -0
  93. package/.pi/lib/harness-spawn-topology.ts +121 -87
  94. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  95. package/.pi/lib/harness-subagents-bridge.ts +4 -1
  96. package/.pi/lib/harness-ui-state.ts +95 -48
  97. package/.pi/lib/plan-approval/dialog.ts +5 -0
  98. package/.pi/lib/plan-approval/validate.ts +1 -1
  99. package/.pi/lib/plan-approval-readiness.ts +32 -0
  100. package/.pi/lib/plan-debate-gate.ts +154 -114
  101. package/.pi/lib/plan-task-clarification.ts +158 -0
  102. package/.pi/prompts/harness-auto.md +2 -2
  103. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  104. package/.pi/prompts/harness-plan.md +58 -8
  105. package/.pi/prompts/harness-review.md +40 -6
  106. package/.pi/prompts/harness-run.md +33 -11
  107. package/.pi/prompts/harness-setup.md +72 -3
  108. package/.pi/prompts/harness-steer.md +2 -1
  109. package/.pi/prompts/wiki-save.md +5 -4
  110. package/.pi/scripts/README.md +8 -0
  111. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  112. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  113. package/.pi/scripts/harness-cli-verify.sh +47 -0
  114. package/.pi/scripts/harness-git-churn.mjs +77 -0
  115. package/.pi/scripts/harness-git-commit.mjs +173 -0
  116. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  117. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  118. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  119. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  120. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  121. package/.pi/scripts/harness-verify.mjs +288 -125
  122. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  123. package/.pi/scripts/run-tests.mjs +1 -0
  124. package/.pi/settings.example.json +1 -0
  125. package/.sentrux/rules.toml +1 -1
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +25 -0
  128. package/README.md +13 -4
  129. package/package.json +5 -1
  130. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
@@ -1,314 +1,2 @@
1
- import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
- import {
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
- import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
- import type { DialogResult, ValidatedAskParams } from "./types.js";
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,10 @@
1
+ declare module "glimpseui" {
2
+ export function prompt(
3
+ html: string,
4
+ windowOptions?: {
5
+ width?: number;
6
+ height?: number;
7
+ title?: string;
8
+ },
9
+ ): Promise<Record<string, unknown> | null>;
10
+ }
@@ -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
+ }