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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|