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.
Files changed (147) 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 +43 -2
  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 +139 -0
  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 +47 -81
  34. package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
  35. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  36. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  37. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  38. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  39. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  40. package/.pi/harness/docs/adrs/README.md +7 -0
  41. package/.pi/harness/docs/practice-map.md +21 -5
  42. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  43. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  44. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  45. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  46. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  47. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  48. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  49. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  50. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  51. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  52. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  53. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  54. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  55. package/.pi/lib/agents-policy.d.mts +26 -47
  56. package/.pi/lib/agents-policy.mjs +84 -29
  57. package/.pi/lib/agents-policy.ts +1 -0
  58. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  59. package/.pi/lib/ask-user/constants.mjs +3 -0
  60. package/.pi/lib/ask-user/constants.ts +4 -0
  61. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  62. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  63. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  64. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  65. package/.pi/lib/ask-user/dialog.ts +2 -314
  66. package/.pi/lib/ask-user/fallback.ts +2 -78
  67. package/.pi/lib/ask-user/format.ts +85 -0
  68. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  69. package/.pi/lib/ask-user/index.ts +114 -0
  70. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  71. package/.pi/lib/ask-user/policy.mjs +43 -0
  72. package/.pi/lib/ask-user/policy.ts +104 -0
  73. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  74. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  75. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  76. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  77. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  78. package/.pi/lib/ask-user/render.ts +40 -9
  79. package/.pi/lib/ask-user/schema.ts +66 -13
  80. package/.pi/lib/ask-user/types.ts +60 -3
  81. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  82. package/.pi/lib/ask-user/validate.ts +53 -34
  83. package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
  84. package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
  85. package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
  86. package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
  87. package/.pi/lib/harness-anchored-edit/index.ts +9 -0
  88. package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
  89. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  90. package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
  91. package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
  92. package/.pi/lib/harness-anchored-edit/types.ts +19 -0
  93. package/.pi/lib/harness-artifact-gate.ts +75 -21
  94. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  95. package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
  96. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  97. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  98. package/.pi/lib/harness-lens/index.ts +246 -96
  99. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  100. package/.pi/lib/harness-repair-brief.ts +84 -25
  101. package/.pi/lib/harness-run-context.ts +42 -52
  102. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  103. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  104. package/.pi/lib/harness-slash-completions.ts +116 -0
  105. package/.pi/lib/harness-spawn-topology.ts +121 -87
  106. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  107. package/.pi/lib/harness-subagents-bridge.ts +11 -6
  108. package/.pi/lib/harness-ui-state.ts +95 -48
  109. package/.pi/lib/plan-approval/dialog.ts +5 -0
  110. package/.pi/lib/plan-approval/validate.ts +1 -1
  111. package/.pi/lib/plan-approval-readiness.ts +32 -0
  112. package/.pi/lib/plan-debate-gate.ts +154 -114
  113. package/.pi/lib/plan-task-clarification.ts +158 -0
  114. package/.pi/prompts/harness-auto.md +2 -2
  115. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  116. package/.pi/prompts/harness-plan.md +58 -8
  117. package/.pi/prompts/harness-review.md +40 -6
  118. package/.pi/prompts/harness-run.md +33 -11
  119. package/.pi/prompts/harness-setup.md +72 -3
  120. package/.pi/prompts/harness-steer.md +3 -2
  121. package/.pi/prompts/wiki-save.md +5 -4
  122. package/.pi/scripts/README.md +8 -0
  123. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  124. package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
  125. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  126. package/.pi/scripts/harness-cli-verify.sh +47 -0
  127. package/.pi/scripts/harness-git-churn.mjs +77 -0
  128. package/.pi/scripts/harness-git-commit.mjs +173 -0
  129. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  130. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  131. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  132. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  133. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  134. package/.pi/scripts/harness-verify.mjs +347 -117
  135. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  136. package/.pi/scripts/run-tests.mjs +65 -0
  137. package/.pi/settings.example.json +1 -0
  138. package/.sentrux/rules.toml +1 -1
  139. package/AGENTS.md +1 -0
  140. package/CHANGELOG.md +31 -0
  141. package/README.md +13 -4
  142. package/THIRD_PARTY_NOTICES.md +7 -0
  143. package/package.json +8 -3
  144. package/vendor/pi-subagents/src/agents.ts +5 -0
  145. package/vendor/pi-subagents/src/subagents.ts +22 -3
  146. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
  147. package/.pi/scripts/release.sh +0 -338
@@ -1,7 +1,12 @@
1
- /** Keep in sync with validate.ts — test entrypoint for node --test */
1
+ /** Keep in sync with validate.ts + format.ts — test entrypoint for node --test */
2
+
3
+ import { createRequire } from "node:module";
4
+ import { MAX_QUESTIONNAIRE_QUESTIONS } from "./constants.mjs";
5
+
6
+ const require = createRequire(import.meta.url);
2
7
 
3
8
  /**
4
- * @param {string | { title: string, description?: string }} raw
9
+ * @param {string | { title: string, description?: string, recommended?: boolean }} raw
5
10
  */
6
11
  export function normalizeOption(raw) {
7
12
  if (typeof raw === "string") {
@@ -10,6 +15,26 @@ export function normalizeOption(raw) {
10
15
  return {
11
16
  title: raw.title.trim(),
12
17
  description: raw.description?.trim() || undefined,
18
+ recommended: raw.recommended === true ? true : undefined,
19
+ };
20
+ }
21
+
22
+ function normalizeQuestion(raw) {
23
+ const title = raw.title?.trim();
24
+ if (!title) return "ask_user: each questions[] item requires title";
25
+
26
+ const options = (raw.options ?? [])
27
+ .map(normalizeOption)
28
+ .filter((o) => o.title);
29
+ if (options.length > 0 && options.length < 2) {
30
+ return `ask_user: question "${title}" needs at least 2 options or omit options for freeform`;
31
+ }
32
+
33
+ return {
34
+ title,
35
+ description: raw.description?.trim() || undefined,
36
+ options,
37
+ allowMultiple: raw.allowMultiple === true,
13
38
  };
14
39
  }
15
40
 
@@ -20,6 +45,22 @@ export function validateAskParams(params) {
20
45
  return "ask_user: question is required";
21
46
  }
22
47
 
48
+ const rawQuestions = params.questions ?? [];
49
+ if (rawQuestions.length > MAX_QUESTIONNAIRE_QUESTIONS) {
50
+ return `ask_user: at most ${MAX_QUESTIONNAIRE_QUESTIONS} questions in questionnaire mode`;
51
+ }
52
+
53
+ if (rawQuestions.length > 0 && (params.options?.length ?? 0) > 0) {
54
+ return "ask_user: use either options or questions[], not both";
55
+ }
56
+
57
+ const questions = [];
58
+ for (const q of rawQuestions) {
59
+ const normalized = normalizeQuestion(q);
60
+ if (typeof normalized === "string") return normalized;
61
+ questions.push(normalized);
62
+ }
63
+
23
64
  const options = (params.options ?? [])
24
65
  .map(normalizeOption)
25
66
  .filter((o) => o.title);
@@ -28,7 +69,9 @@ export function validateAskParams(params) {
28
69
  }
29
70
 
30
71
  const allowFreeform = params.allowFreeform !== false;
31
- if (options.length === 0 && !allowFreeform) {
72
+ const mode = questions.length > 0 ? "questionnaire" : "flat";
73
+
74
+ if (mode === "flat" && options.length === 0 && !allowFreeform) {
32
75
  return "ask_user: options required when allowFreeform is false";
33
76
  }
34
77
 
@@ -42,9 +85,15 @@ export function validateAskParams(params) {
42
85
  return {
43
86
  question,
44
87
  context: params.context?.trim() || undefined,
88
+ contextFormat:
89
+ params.contextFormat === "html" ? "html" : "markdown",
45
90
  options,
91
+ questions,
92
+ mode,
46
93
  allowMultiple: params.allowMultiple === true,
47
94
  allowFreeform,
95
+ allowComment: params.allowComment === true,
96
+ allowSkip: params.allowSkip === true,
48
97
  displayMode,
49
98
  timeout:
50
99
  typeof params.timeout === "number" && params.timeout > 0
@@ -53,14 +102,30 @@ export function validateAskParams(params) {
53
102
  };
54
103
  }
55
104
 
56
- /** @param {import('./types.js').AskResponse | null} response @param {boolean} cancelled */
57
- export function formatResultText(response, cancelled) {
105
+ /** @param {import('./types.js').AskResponse | null} response @param {boolean} cancelled @param {{ ui_degraded?: boolean }} [opts] */
106
+ export function formatResultText(response, cancelled, opts) {
58
107
  if (cancelled || !response) {
59
108
  return "User cancelled (no answer)";
60
109
  }
110
+ if (opts?.ui_degraded) {
111
+ return `Rich ask UI unavailable; using terminal prompt.\n\n${formatResponseBody(response)}`;
112
+ }
113
+ return formatResponseBody(response);
114
+ }
115
+
116
+ /** @param {import('./types.js').AskResponse} response */
117
+ function formatResponseBody(response) {
61
118
  if (response.kind === "freeform") {
62
119
  return `User wrote: ${response.text}`;
63
120
  }
121
+ if (response.kind === "questionnaire") {
122
+ const lines = response.questionnaireDetails.map((d) => {
123
+ let line = `- ${d.question}: ${d.answer}`;
124
+ if (d.comment) line += ` (comment: ${d.comment})`;
125
+ return line;
126
+ });
127
+ return `User answered questionnaire:\n${lines.join("\n")}`;
128
+ }
64
129
  if (response.selections.length === 1) {
65
130
  return `User selected: ${response.selections[0]}`;
66
131
  }
@@ -68,12 +133,133 @@ export function formatResultText(response, cancelled) {
68
133
  }
69
134
 
70
135
  /** @param {import('./types.js').ValidatedAskParams} validated */
71
- export function toToolDetails(validated, response, cancelled) {
136
+ export function toToolDetails(validated, response, cancelled, ui_backend = "headless", opts = {}) {
137
+ const options =
138
+ validated.mode === "questionnaire"
139
+ ? validated.questions.map((q) => q.title)
140
+ : validated.options.map((o) => o.title);
72
141
  return {
73
142
  question: validated.question,
74
143
  context: validated.context,
75
- options: validated.options.map((o) => o.title),
144
+ contextFormat: validated.contextFormat,
145
+ options,
76
146
  response,
77
147
  cancelled,
148
+ ui_backend,
149
+ ui_degraded: opts.ui_degraded,
150
+ };
151
+ }
152
+
153
+ export { isPlanApprovalAskUser } from "./policy.mjs";
154
+
155
+ /** @param {import('./types.js').ValidatedAskParams} validated */
156
+ export function buildGlimpsePayload(validated, sessionName) {
157
+ const hasQuestions = validated.mode === "questionnaire";
158
+ const hasOptions = validated.options.length > 0;
159
+ let payloadType;
160
+ if (hasQuestions) payloadType = "questionnaire";
161
+ else if (!hasOptions) payloadType = "freeform";
162
+ else if (validated.allowMultiple) payloadType = "multi-select";
163
+ else payloadType = "single-select";
164
+
165
+ let question = validated.question;
166
+ let context = validated.context;
167
+ if (!context && validated.question.length > 120) {
168
+ const match = validated.question.match(/^(.+?[.?!])(\s+|$)/);
169
+ if (match && match[0].length < validated.question.length) {
170
+ question = match[1].trim();
171
+ context = validated.question.slice(match[0].length).trim();
172
+ }
173
+ }
174
+
175
+ return {
176
+ type: payloadType,
177
+ question,
178
+ context,
179
+ contextFormat: validated.contextFormat,
180
+ options: validated.options.map((o) => ({
181
+ title: o.title,
182
+ description: o.description,
183
+ recommended: o.recommended,
184
+ })),
185
+ questions: validated.questions.map((q) => ({
186
+ title: q.title,
187
+ description: q.description,
188
+ allowMultiple: q.allowMultiple,
189
+ options: q.options.map((o) => ({
190
+ title: o.title,
191
+ description: o.description,
192
+ recommended: o.recommended,
193
+ })),
194
+ })),
195
+ allowMultiple: validated.allowMultiple,
196
+ allowFreeform: validated.allowFreeform,
197
+ allowComment: validated.allowComment,
198
+ allowSkip: validated.allowSkip,
199
+ sessionName,
200
+ };
201
+ }
202
+
203
+ function isGlimpseAvailable() {
204
+ try {
205
+ require.resolve("@alexleekt/pi-ask-user-glimpse/package.json");
206
+ require.resolve("glimpseui");
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ /** @param {import('./types.js').ValidatedAskParams} validated @param {boolean} hasUI */
214
+ export function resolvePresenterChoice(validated, hasUI) {
215
+ if (validated.displayMode === "inline") return "tui";
216
+
217
+ const forced = process.env.HARNESS_ASK_USER_UI?.toLowerCase();
218
+ if (forced === "tui") return "tui";
219
+ if (forced === "glimpse") return "glimpse";
220
+ if (forced === "headless") return "headless";
221
+ if (hasUI && isGlimpseAvailable()) return "glimpse";
222
+ if (hasUI) return "tui";
223
+ return "headless";
224
+ }
225
+
226
+ /** @param {Record<string, unknown>} doc @param {import('./types.js').AskToolDetails} details */
227
+ export function applyAskUserToTaskClarification(doc, details) {
228
+ const next = { ...doc };
229
+ const assumptions = Array.isArray(next.assumptions) ? [...next.assumptions] : [];
230
+ if (!details.response || details.cancelled) return next;
231
+
232
+ const applyPair = (question, answer) => {
233
+ const n = question.toLowerCase();
234
+ if (n.includes("done") || n.includes("success")) {
235
+ next.success_definition = answer;
236
+ return;
237
+ }
238
+ if (n.includes("risk")) {
239
+ const a = answer.toLowerCase();
240
+ if (/\blow\b/.test(a)) next.risk_level = "low";
241
+ else if (/\bhigh\b/.test(a)) next.risk_level = "high";
242
+ else if (/\bmed/.test(a) || /\bmedium\b/.test(a)) next.risk_level = "med";
243
+ return;
244
+ }
245
+ if (n.includes("scope") || n.includes("in scope")) {
246
+ const items = answer.split(/[,;\n]/).map((s) => s.trim()).filter(Boolean);
247
+ if (items.length) next.in_scope = items;
248
+ return;
249
+ }
250
+ assumptions.push(`${question}: ${answer}`);
78
251
  };
252
+
253
+ const r = details.response;
254
+ if (r.kind === "questionnaire") {
255
+ for (const d of r.questionnaireDetails) applyPair(d.question, d.answer);
256
+ } else if (r.kind === "freeform") {
257
+ applyPair(details.question, r.text);
258
+ } else if (r.kind === "selection") {
259
+ applyPair(details.question, r.selections.join(", "));
260
+ }
261
+
262
+ if (assumptions.length) next.assumptions = assumptions;
263
+ if (Array.isArray(next.unresolved_questions)) next.unresolved_questions = [];
264
+ return next;
79
265
  }
@@ -1,15 +1,15 @@
1
+ import { MAX_QUESTIONNAIRE_QUESTIONS } from "./constants.js";
1
2
  import type {
2
- AskResponse,
3
- AskToolDetails,
4
3
  AskUserParams,
5
4
  NormalizedOption,
5
+ NormalizedQuestion,
6
6
  ValidatedAskParams,
7
7
  } from "./types.js";
8
8
 
9
9
  export type { ValidatedAskParams };
10
10
 
11
11
  export function normalizeOption(
12
- raw: string | { title: string; description?: string },
12
+ raw: string | { title: string; description?: string; recommended?: boolean },
13
13
  ): NormalizedOption {
14
14
  if (typeof raw === "string") {
15
15
  return { title: raw.trim() };
@@ -17,6 +17,28 @@ export function normalizeOption(
17
17
  return {
18
18
  title: raw.title.trim(),
19
19
  description: raw.description?.trim() || undefined,
20
+ recommended: raw.recommended === true ? true : undefined,
21
+ };
22
+ }
23
+
24
+ function normalizeQuestion(
25
+ raw: NonNullable<AskUserParams["questions"]>[number],
26
+ ): NormalizedQuestion | string {
27
+ const title = raw.title?.trim();
28
+ if (!title) return "ask_user: each questions[] item requires title";
29
+
30
+ const options = (raw.options ?? [])
31
+ .map(normalizeOption)
32
+ .filter((o) => o.title);
33
+ if (options.length > 0 && options.length < 2) {
34
+ return `ask_user: question "${title}" needs at least 2 options or omit options for freeform`;
35
+ }
36
+
37
+ return {
38
+ title,
39
+ description: raw.description?.trim() || undefined,
40
+ options,
41
+ allowMultiple: raw.allowMultiple === true,
20
42
  };
21
43
  }
22
44
 
@@ -28,6 +50,22 @@ export function validateAskParams(
28
50
  return "ask_user: question is required";
29
51
  }
30
52
 
53
+ const rawQuestions = params.questions ?? [];
54
+ if (rawQuestions.length > MAX_QUESTIONNAIRE_QUESTIONS) {
55
+ return `ask_user: at most ${MAX_QUESTIONNAIRE_QUESTIONS} questions in questionnaire mode`;
56
+ }
57
+
58
+ if (rawQuestions.length > 0 && (params.options?.length ?? 0) > 0) {
59
+ return "ask_user: use either options or questions[], not both";
60
+ }
61
+
62
+ const questions: NormalizedQuestion[] = [];
63
+ for (const q of rawQuestions) {
64
+ const normalized = normalizeQuestion(q);
65
+ if (typeof normalized === "string") return normalized;
66
+ questions.push(normalized);
67
+ }
68
+
31
69
  const options = (params.options ?? [])
32
70
  .map(normalizeOption)
33
71
  .filter((o) => o.title);
@@ -36,10 +74,16 @@ export function validateAskParams(
36
74
  }
37
75
 
38
76
  const allowFreeform = params.allowFreeform !== false;
39
- if (options.length === 0 && !allowFreeform) {
77
+ const mode = questions.length > 0 ? "questionnaire" : "flat";
78
+
79
+ if (mode === "flat" && options.length === 0 && !allowFreeform) {
40
80
  return "ask_user: options required when allowFreeform is false";
41
81
  }
42
82
 
83
+ if (mode === "questionnaire" && questions.length === 0) {
84
+ return "ask_user: questions[] must not be empty";
85
+ }
86
+
43
87
  const displayMode =
44
88
  process.env.HARNESS_ASK_USER_DISPLAY_MODE === "inline"
45
89
  ? "inline"
@@ -50,9 +94,14 @@ export function validateAskParams(
50
94
  return {
51
95
  question,
52
96
  context: params.context?.trim() || undefined,
97
+ contextFormat: params.contextFormat === "html" ? "html" : "markdown",
53
98
  options,
99
+ questions,
100
+ mode,
54
101
  allowMultiple: params.allowMultiple === true,
55
102
  allowFreeform,
103
+ allowComment: params.allowComment === true,
104
+ allowSkip: params.allowSkip === true,
56
105
  displayMode,
57
106
  timeout:
58
107
  typeof params.timeout === "number" && params.timeout > 0
@@ -60,33 +109,3 @@ export function validateAskParams(
60
109
  : undefined,
61
110
  };
62
111
  }
63
-
64
- export function formatResultText(
65
- response: AskResponse | null,
66
- cancelled: boolean,
67
- ): string {
68
- if (cancelled || !response) {
69
- return "User cancelled (no answer)";
70
- }
71
- if (response.kind === "freeform") {
72
- return `User wrote: ${response.text}`;
73
- }
74
- if (response.selections.length === 1) {
75
- return `User selected: ${response.selections[0]}`;
76
- }
77
- return `User selected: ${response.selections.join(", ")}`;
78
- }
79
-
80
- export function toToolDetails(
81
- validated: ValidatedAskParams,
82
- response: AskResponse | null,
83
- cancelled: boolean,
84
- ): AskToolDetails {
85
- return {
86
- question: validated.question,
87
- context: validated.context,
88
- options: validated.options.map((o) => o.title),
89
- response,
90
- cancelled,
91
- };
92
- }