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
@@ -0,0 +1,373 @@
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 {
10
+ detailFromFlatResult,
11
+ mergeQuestionnaireResults,
12
+ questionToFlatParams,
13
+ } from "../core/questionnaire.js";
14
+ import type {
15
+ AskResponse,
16
+ DialogResult,
17
+ ValidatedAskParams,
18
+ } from "../types.js";
19
+
20
+ type DisplayOption = {
21
+ title: string;
22
+ description?: string;
23
+ isFreeform?: boolean;
24
+ };
25
+
26
+ interface CustomAnswer {
27
+ response: AskResponse;
28
+ }
29
+
30
+ type ThemeLike = { fg(color: string, text: string): string };
31
+ type TuiLike = ConstructorParameters<typeof Editor>[0] & {
32
+ requestRender(): void;
33
+ };
34
+ type Done = (answer: CustomAnswer | null) => void;
35
+
36
+ function withTimeout<T>(
37
+ promise: Promise<T | null>,
38
+ ms: number | undefined,
39
+ ): Promise<T | null> {
40
+ if (!ms) return promise;
41
+ return Promise.race([
42
+ promise,
43
+ new Promise<null>((resolve) => {
44
+ setTimeout(() => resolve(null), ms);
45
+ }),
46
+ ]);
47
+ }
48
+
49
+ function displayOptionsFor(validated: ValidatedAskParams): DisplayOption[] {
50
+ const displayOptions: DisplayOption[] = [...validated.options];
51
+ if (validated.allowFreeform) {
52
+ displayOptions.push({ title: "Type something…", isFreeform: true });
53
+ }
54
+ return displayOptions;
55
+ }
56
+
57
+ async function runFreeformOnly(
58
+ ui: ExtensionUIContext,
59
+ question: string,
60
+ ): Promise<DialogResult> {
61
+ const text = await ui.input(question, "");
62
+ if (!text?.trim()) {
63
+ return { response: null, cancelled: true, ui_backend: "tui" };
64
+ }
65
+ return {
66
+ response: { kind: "freeform", text: text.trim() },
67
+ cancelled: false,
68
+ ui_backend: "tui",
69
+ };
70
+ }
71
+
72
+ function editorThemeFor(theme: ThemeLike): EditorTheme {
73
+ return {
74
+ borderColor: (s) => theme.fg("accent", s),
75
+ selectList: {
76
+ selectedPrefix: (t) => theme.fg("accent", t),
77
+ selectedText: (t) => theme.fg("accent", t),
78
+ description: (t) => theme.fg("muted", t),
79
+ scrollInfo: (t) => theme.fg("dim", t),
80
+ noMatch: (t) => theme.fg("warning", t),
81
+ },
82
+ };
83
+ }
84
+
85
+ class AskDialogController {
86
+ private optionIndex = 0;
87
+ private editMode = false;
88
+ private readonly selected = new Set<number>();
89
+ private cachedLines: string[] | undefined;
90
+ private readonly editor: Editor;
91
+
92
+ constructor(
93
+ private readonly validated: ValidatedAskParams,
94
+ private readonly displayOptions: DisplayOption[],
95
+ private readonly tui: TuiLike,
96
+ private readonly theme: ThemeLike,
97
+ private readonly done: Done,
98
+ ) {
99
+ this.editor = new Editor(tui, editorThemeFor(theme));
100
+ this.editor.onSubmit = (value) => this.submitFreeform(value);
101
+ }
102
+
103
+ invalidate(): void {
104
+ this.cachedLines = undefined;
105
+ }
106
+
107
+ handleInput(data: string): void {
108
+ if (this.editMode) {
109
+ this.handleEditInput(data);
110
+ return;
111
+ }
112
+ if (this.handleNavigationInput(data)) return;
113
+ if (this.validated.allowMultiple && matchesKey(data, Key.space)) {
114
+ this.toggleMultiSelect();
115
+ return;
116
+ }
117
+ if (matchesKey(data, Key.enter)) this.submitSelection();
118
+ if (matchesKey(data, Key.escape)) this.done(null);
119
+ }
120
+
121
+ render(width: number): string[] {
122
+ if (this.cachedLines) return this.cachedLines;
123
+ const lines: string[] = [];
124
+ const add = (s: string) => lines.push(truncateToWidth(s, width));
125
+ const useOverlay = this.validated.displayMode !== "inline";
126
+ this.renderHeader(lines, add, width, useOverlay);
127
+ this.renderOptions(add);
128
+ this.renderEditor(lines, add, width);
129
+ this.renderFooter(add, width, useOverlay);
130
+ this.cachedLines = lines;
131
+ return lines;
132
+ }
133
+
134
+ private refresh(): void {
135
+ this.invalidate();
136
+ this.tui.requestRender();
137
+ }
138
+
139
+ private submitFreeform(value: string): void {
140
+ const trimmed = value.trim();
141
+ if (trimmed) {
142
+ this.done({ response: { kind: "freeform", text: trimmed } });
143
+ return;
144
+ }
145
+ this.editMode = false;
146
+ this.editor.setText("");
147
+ this.refresh();
148
+ }
149
+
150
+ private submitSelection(): void {
151
+ if (this.validated.allowMultiple) {
152
+ const titles = [...this.selected]
153
+ .sort((a, b) => a - b)
154
+ .map((i) => this.displayOptions[i].title)
155
+ .filter((t) => t !== "Type something…");
156
+ if (titles.length) {
157
+ this.done({ response: { kind: "selection", selections: titles } });
158
+ }
159
+ return;
160
+ }
161
+ const opt = this.displayOptions[this.optionIndex];
162
+ if (opt.isFreeform) {
163
+ this.editMode = true;
164
+ this.refresh();
165
+ return;
166
+ }
167
+ this.done({ response: { kind: "selection", selections: [opt.title] } });
168
+ }
169
+
170
+ private handleEditInput(data: string): void {
171
+ if (matchesKey(data, Key.escape)) {
172
+ this.editMode = false;
173
+ this.editor.setText("");
174
+ } else {
175
+ this.editor.handleInput(data);
176
+ }
177
+ this.refresh();
178
+ }
179
+
180
+ private handleNavigationInput(data: string): boolean {
181
+ if (matchesKey(data, Key.up)) {
182
+ this.optionIndex = Math.max(0, this.optionIndex - 1);
183
+ this.refresh();
184
+ return true;
185
+ }
186
+ if (matchesKey(data, Key.down)) {
187
+ this.optionIndex = Math.min(
188
+ this.displayOptions.length - 1,
189
+ this.optionIndex + 1,
190
+ );
191
+ this.refresh();
192
+ return true;
193
+ }
194
+ return false;
195
+ }
196
+
197
+ private toggleMultiSelect(): void {
198
+ const opt = this.displayOptions[this.optionIndex];
199
+ if (opt.isFreeform) return;
200
+ if (this.selected.has(this.optionIndex)) {
201
+ this.selected.delete(this.optionIndex);
202
+ } else {
203
+ this.selected.add(this.optionIndex);
204
+ }
205
+ this.refresh();
206
+ }
207
+
208
+ private renderHeader(
209
+ lines: string[],
210
+ add: (s: string) => void,
211
+ width: number,
212
+ useOverlay: boolean,
213
+ ): void {
214
+ if (useOverlay) add(this.theme.fg("accent", "─".repeat(width)));
215
+ if (this.validated.context) {
216
+ for (const line of this.validated.context.split("\n")) {
217
+ add(this.theme.fg("muted", ` ${line}`));
218
+ }
219
+ lines.push("");
220
+ }
221
+ add(this.theme.fg("text", ` ${this.validated.question}`));
222
+ lines.push("");
223
+ }
224
+
225
+ private renderOptions(add: (s: string) => void): void {
226
+ for (let i = 0; i < this.displayOptions.length; i++) {
227
+ const opt = this.displayOptions[i];
228
+ const prefix = this.optionPrefix(i, opt.isFreeform === true);
229
+ const label = this.optionLabel(i, opt);
230
+ add(`${prefix}${label}`);
231
+ if (opt.description)
232
+ add(` ${this.theme.fg("muted", opt.description)}`);
233
+ }
234
+ }
235
+
236
+ private optionPrefix(index: number, isFreeform: boolean): string {
237
+ if (!this.validated.allowMultiple) {
238
+ return index === this.optionIndex ? this.theme.fg("accent", "> ") : " ";
239
+ }
240
+ if (isFreeform) return " ";
241
+ return this.selected.has(index) ? this.theme.fg("accent", "[x] ") : "[ ] ";
242
+ }
243
+
244
+ private optionLabel(index: number, opt: DisplayOption): string {
245
+ const raw = `${index + 1}. ${opt.title}`;
246
+ const focused = index === this.optionIndex;
247
+ if (opt.isFreeform && this.editMode && focused) {
248
+ return this.theme.fg("accent", `${raw} ✎`);
249
+ }
250
+ if (focused && !this.validated.allowMultiple) {
251
+ return this.theme.fg("accent", raw);
252
+ }
253
+ return this.theme.fg("text", raw);
254
+ }
255
+
256
+ private renderEditor(
257
+ lines: string[],
258
+ add: (s: string) => void,
259
+ width: number,
260
+ ): void {
261
+ if (!this.editMode) return;
262
+ lines.push("");
263
+ add(this.theme.fg("muted", " Your answer:"));
264
+ for (const line of this.editor.render(width - 2)) add(` ${line}`);
265
+ }
266
+
267
+ private renderFooter(
268
+ add: (s: string) => void,
269
+ width: number,
270
+ useOverlay: boolean,
271
+ ): void {
272
+ add("");
273
+ if (this.editMode) {
274
+ add(this.theme.fg("dim", " Enter to submit • Esc to go back"));
275
+ } else if (this.validated.allowMultiple) {
276
+ add(
277
+ this.theme.fg(
278
+ "dim",
279
+ " ↑↓ navigate • Space toggle • Enter confirm • Esc cancel",
280
+ ),
281
+ );
282
+ } else {
283
+ add(
284
+ this.theme.fg("dim", " ↑↓ navigate • Enter to select • Esc to cancel"),
285
+ );
286
+ }
287
+ if (useOverlay) add(this.theme.fg("accent", "─".repeat(width)));
288
+ }
289
+ }
290
+
291
+ async function runOptionDialog(
292
+ ui: ExtensionUIContext,
293
+ validated: ValidatedAskParams,
294
+ displayOptions: DisplayOption[],
295
+ ): Promise<CustomAnswer | null> {
296
+ return withTimeout(
297
+ ui.custom<CustomAnswer | null>((tui, theme, _kb, done) => {
298
+ const controller = new AskDialogController(
299
+ validated,
300
+ displayOptions,
301
+ tui as TuiLike,
302
+ theme,
303
+ done,
304
+ );
305
+ return {
306
+ render: (width: number) => controller.render(width),
307
+ invalidate: () => controller.invalidate(),
308
+ handleInput: (data: string) => controller.handleInput(data),
309
+ };
310
+ }),
311
+ validated.timeout,
312
+ );
313
+ }
314
+
315
+ async function runFlatTui(
316
+ ui: ExtensionUIContext,
317
+ validated: ValidatedAskParams,
318
+ ): Promise<DialogResult> {
319
+ const displayOptions = displayOptionsFor(validated);
320
+ if (displayOptions.length === 0) {
321
+ const r = await runFreeformOnly(ui, validated.question);
322
+ return { ...r, ui_backend: "tui" };
323
+ }
324
+ const result = await runOptionDialog(ui, validated, displayOptions);
325
+ if (!result) {
326
+ return { response: null, cancelled: true, ui_backend: "tui" };
327
+ }
328
+ return { response: result.response, cancelled: false, ui_backend: "tui" };
329
+ }
330
+
331
+ async function runQuestionnaireTui(
332
+ ui: ExtensionUIContext,
333
+ validated: ValidatedAskParams,
334
+ ): Promise<DialogResult> {
335
+ const total = validated.questions.length;
336
+ const details = [];
337
+ let last: DialogResult | undefined;
338
+
339
+ for (let i = 0; i < total; i++) {
340
+ const q = validated.questions[i];
341
+ const flat = questionToFlatParams(validated, q, i, total);
342
+ const step = await runFlatTui(ui, flat);
343
+ last = step;
344
+ if (step.cancelled) {
345
+ return { response: null, cancelled: true, ui_backend: "tui" };
346
+ }
347
+ const label = q.description ?? q.title;
348
+ const detail = detailFromFlatResult(label, step);
349
+ if (!detail) {
350
+ if (!validated.allowSkip) {
351
+ return { response: null, cancelled: true, ui_backend: "tui" };
352
+ }
353
+ continue;
354
+ }
355
+ details.push(detail);
356
+ }
357
+
358
+ return mergeQuestionnaireResults(details, last);
359
+ }
360
+
361
+ /** TUI presenter (overlay or inline). */
362
+ export async function runTuiPresenter(
363
+ ui: ExtensionUIContext,
364
+ validated: ValidatedAskParams,
365
+ ): Promise<DialogResult> {
366
+ if (validated.mode === "questionnaire") {
367
+ return runQuestionnaireTui(ui, validated);
368
+ }
369
+ return runFlatTui(ui, validated);
370
+ }
371
+
372
+ /** @deprecated Use runTuiPresenter */
373
+ export const runAskDialog = runTuiPresenter;
@@ -0,0 +1,13 @@
1
+ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
+ import type { DialogResult, ValidatedAskParams } from "../types.js";
3
+
4
+ export interface PresenterContext {
5
+ ui: ExtensionUIContext;
6
+ hasUI: boolean;
7
+ sessionName?: string;
8
+ }
9
+
10
+ export type AskUserPresenter = (
11
+ validated: ValidatedAskParams,
12
+ ctx: PresenterContext,
13
+ ) => Promise<DialogResult>;
@@ -16,16 +16,25 @@ export function renderAskCall(
16
16
  if (args.context) {
17
17
  text += `\n${theme.fg("dim", ` ${String(args.context)}`)}`;
18
18
  }
19
- const opts = Array.isArray(args.options) ? args.options : [];
20
- if (opts.length) {
21
- const labels = opts.map((o: unknown) =>
22
- typeof o === "string" ? o : ((o as { title?: string })?.title ?? "?"),
23
- );
24
- const numbered = labels.map((o, i) => `${i + 1}. ${o}`);
25
- if (args.allowFreeform !== false) {
26
- numbered.push(`${numbered.length + 1}. Type something…`);
19
+ const questions = Array.isArray(args.questions) ? args.questions : [];
20
+ if (questions.length > 0) {
21
+ text += `\n${theme.fg("dim", ` Questionnaire: ${questions.length} item(s)`)}`;
22
+ const first = questions[0] as { title?: string };
23
+ if (first?.title) {
24
+ text += `\n${theme.fg("dim", ` First: ${first.title}`)}`;
25
+ }
26
+ } else {
27
+ const opts = Array.isArray(args.options) ? args.options : [];
28
+ if (opts.length) {
29
+ const labels = opts.map((o: unknown) =>
30
+ typeof o === "string" ? o : ((o as { title?: string })?.title ?? "?"),
31
+ );
32
+ const numbered = labels.map((o, i) => `${i + 1}. ${o}`);
33
+ if (args.allowFreeform !== false) {
34
+ numbered.push(`${numbered.length + 1}. Type something…`);
35
+ }
36
+ text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
27
37
  }
28
- text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
29
38
  }
30
39
  return new Text(text, 0, 0);
31
40
  }
@@ -45,6 +54,19 @@ export function renderAskResult(
45
54
  return new Text(theme.fg("warning", "Cancelled"), 0, 0);
46
55
  }
47
56
 
57
+ if (details.ui_degraded) {
58
+ return new Text(
59
+ theme.fg("warning", "↓ TUI ") +
60
+ theme.fg("success", "✓ ") +
61
+ theme.fg(
62
+ "muted",
63
+ result.content[0]?.type === "text" ? result.content[0].text : "",
64
+ ),
65
+ 0,
66
+ 0,
67
+ );
68
+ }
69
+
48
70
  if (details.response.kind === "freeform") {
49
71
  return new Text(
50
72
  theme.fg("success", "✓ ") +
@@ -55,6 +77,15 @@ export function renderAskResult(
55
77
  );
56
78
  }
57
79
 
80
+ if (details.response.kind === "questionnaire") {
81
+ const count = details.response.questionnaireDetails.length;
82
+ return new Text(
83
+ theme.fg("success", "✓ ") + theme.fg("accent", `${count} answer(s)`),
84
+ 0,
85
+ 0,
86
+ );
87
+ }
88
+
58
89
  const sel = details.response.selections;
59
90
  const display =
60
91
  sel.length === 1 ? sel[0] : sel.map((s, i) => `${i + 1}. ${s}`).join(", ");
@@ -12,24 +12,62 @@ function StringEnum<T extends string[]>(
12
12
  });
13
13
  }
14
14
 
15
+ const OptionObjectSchema = Type.Object({
16
+ title: Type.String({ description: "Short option label" }),
17
+ description: Type.Optional(
18
+ Type.String({ description: "Optional explanation" }),
19
+ ),
20
+ recommended: Type.Optional(
21
+ Type.Boolean({ description: "Show a Recommended badge" }),
22
+ ),
23
+ });
24
+
15
25
  const OptionSchema = Type.Union([
16
26
  Type.String({ description: "Option label" }),
17
- Type.Object({
18
- title: Type.String({ description: "Short option label" }),
19
- description: Type.Optional(
20
- Type.String({ description: "Optional explanation" }),
21
- ),
22
- }),
27
+ OptionObjectSchema,
23
28
  ]);
24
29
 
30
+ const QuestionSchema = Type.Object({
31
+ title: Type.String({ description: "Short label for this sub-question" }),
32
+ description: Type.Optional(
33
+ Type.String({ description: "Full question text shown in the card" }),
34
+ ),
35
+ options: Type.Optional(
36
+ Type.Array(OptionSchema, {
37
+ description: "If omitted, renders as freeform textarea",
38
+ }),
39
+ ),
40
+ allowMultiple: Type.Optional(
41
+ Type.Boolean({
42
+ description: "Allow multiple selections for this sub-question",
43
+ default: false,
44
+ }),
45
+ ),
46
+ });
47
+
25
48
  export const AskUserParamsSchema = Type.Object({
26
49
  question: Type.String({ description: "The question to ask the user" }),
27
50
  context: Type.Optional(
28
- Type.String({ description: "Context shown above the question" }),
51
+ Type.String({
52
+ description:
53
+ "Additional context (markdown or HTML panel when contextFormat is set)",
54
+ }),
55
+ ),
56
+ contextFormat: Type.Optional(
57
+ StringEnum(["markdown", "html"] as const, {
58
+ description: "How to render context in rich UI (default markdown)",
59
+ }),
29
60
  ),
30
61
  options: Type.Optional(
31
62
  Type.Array(OptionSchema, {
32
- description: "Multiple-choice options (min 2 if provided)",
63
+ description:
64
+ "Flat mode: multiple-choice options (min 2 if provided). Ignored when questions is set.",
65
+ }),
66
+ ),
67
+ questions: Type.Optional(
68
+ Type.Array(QuestionSchema, {
69
+ description:
70
+ "Questionnaire mode: batch independent forks in one dialog (max 8)",
33
71
  }),
34
72
  ),
35
73
  allowMultiple: Type.Optional(
@@ -40,18 +78,31 @@ export const AskUserParamsSchema = Type.Object({
40
78
  ),
41
79
  allowFreeform: Type.Optional(
42
80
  Type.Boolean({
43
- description: 'Append "Type something…" freeform row',
81
+ description: 'Allow custom answer (TUI: "Type something…" row)',
44
82
  default: true,
45
83
  }),
46
84
  ),
85
+ allowComment: Type.Optional(
86
+ Type.Boolean({
87
+ description: "Collect optional comment after selection",
88
+ default: false,
89
+ }),
90
+ ),
91
+ allowSkip: Type.Optional(
92
+ Type.Boolean({
93
+ description: "Questionnaire only: allow submit with unanswered items",
94
+ default: false,
95
+ }),
96
+ ),
47
97
  displayMode: Type.Optional(
48
98
  StringEnum(["overlay", "inline"] as const, {
49
- description: "overlay = modal (default), inline = in transcript flow",
99
+ description:
100
+ "overlay = modal (default); inline = transcript flow (forces TUI)",
50
101
  }),
51
102
  ),
52
103
  timeout: Type.Optional(
53
104
  Type.Number({
54
- description: "Auto-cancel after N milliseconds",
105
+ description: "Auto-cancel after N milliseconds (TUI/headless only)",
55
106
  minimum: 1,
56
107
  }),
57
108
  ),
@@ -62,8 +113,10 @@ export const PROMPT_SNIPPET =
62
113
 
63
114
  export const PROMPT_GUIDELINES = [
64
115
  "Use ask_user when requirements are ambiguous, conflicting, or high-impact — never guess on harness forks (Firecrawl mode, .env creation, scope, risk, merge policy).",
65
- "Prefer one focused question per call with 2–4 clear options and short descriptions for trade-offs.",
116
+ "Prefer one focused flat question with 2–4 options, or use questions[] when 2–4 independent dimensions must be decided in one clarification round (max 8 sub-questions).",
117
+ "Use context (markdown) for evidence bullets; keep under ~500 chars unless the fork is architectural.",
118
+ "Never use ask_user for final plan approval — use approve_plan.",
66
119
  "If the user cancels (Esc), stop and report needs_clarification; do not proceed with assumptions.",
67
- "Do not stack redundant ask_user calls — combine related choices when phase 2 batch mode exists.",
68
120
  "Independent evaluator/adversary agents must not call ask_user; emit human_required and let the orchestrator ask.",
121
+ "After Phase 0 ask_user, merge answers into task-clarification.yaml via applyAskUserToTaskClarification (harness-decisions skill).",
69
122
  ];
@@ -1,26 +1,70 @@
1
1
  export interface NormalizedOption {
2
2
  title: string;
3
3
  description?: string;
4
+ recommended?: boolean;
4
5
  }
5
6
 
7
+ export interface NormalizedQuestion {
8
+ title: string;
9
+ description?: string;
10
+ options: NormalizedOption[];
11
+ allowMultiple: boolean;
12
+ }
13
+
14
+ export type QuestionnaireDetail = {
15
+ question: string;
16
+ answer: string;
17
+ kind: "selection" | "freeform";
18
+ comment?: string;
19
+ };
20
+
6
21
  export type AskResponse =
7
- | { kind: "selection"; selections: string[] }
8
- | { kind: "freeform"; text: string };
22
+ | {
23
+ kind: "selection";
24
+ selections: string[];
25
+ comment?: string;
26
+ additionalComments?: string;
27
+ }
28
+ | { kind: "freeform"; text: string; additionalComments?: string }
29
+ | {
30
+ kind: "questionnaire";
31
+ questionnaireDetails: QuestionnaireDetail[];
32
+ additionalComments?: string;
33
+ };
34
+
35
+ export type UiBackend = "tui" | "glimpse" | "headless";
9
36
 
10
37
  export interface AskToolDetails {
11
38
  question: string;
12
39
  context?: string;
40
+ contextFormat?: "markdown" | "html";
13
41
  options: string[];
14
42
  response: AskResponse | null;
15
43
  cancelled: boolean;
44
+ ui_backend: UiBackend;
45
+ ui_degraded?: boolean;
46
+ non_interactive_blocked?: boolean;
16
47
  }
17
48
 
18
49
  export interface AskUserParams {
19
50
  question: string;
20
51
  context?: string;
21
- options?: Array<string | { title: string; description?: string }>;
52
+ contextFormat?: "markdown" | "html";
53
+ options?: Array<
54
+ string | { title: string; description?: string; recommended?: boolean }
55
+ >;
56
+ questions?: Array<{
57
+ title: string;
58
+ description?: string;
59
+ options?: Array<
60
+ string | { title: string; description?: string; recommended?: boolean }
61
+ >;
62
+ allowMultiple?: boolean;
63
+ }>;
22
64
  allowMultiple?: boolean;
23
65
  allowFreeform?: boolean;
66
+ allowComment?: boolean;
67
+ allowSkip?: boolean;
24
68
  displayMode?: "overlay" | "inline";
25
69
  timeout?: number;
26
70
  }
@@ -28,9 +72,15 @@ export interface AskUserParams {
28
72
  export interface ValidatedAskParams {
29
73
  question: string;
30
74
  context?: string;
75
+ contextFormat: "markdown" | "html";
76
+ /** Flat single/multi-select mode */
31
77
  options: NormalizedOption[];
78
+ questions: NormalizedQuestion[];
79
+ mode: "flat" | "questionnaire";
32
80
  allowMultiple: boolean;
33
81
  allowFreeform: boolean;
82
+ allowComment: boolean;
83
+ allowSkip: boolean;
34
84
  displayMode: "overlay" | "inline";
35
85
  timeout?: number;
36
86
  }
@@ -38,4 +88,11 @@ export interface ValidatedAskParams {
38
88
  export interface DialogResult {
39
89
  response: AskResponse | null;
40
90
  cancelled: boolean;
91
+ ui_backend: UiBackend;
92
+ ui_degraded?: boolean;
93
+ }
94
+
95
+ export interface RunAskUserResult {
96
+ content: { type: "text"; text: string }[];
97
+ details: AskToolDetails;
41
98
  }