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.
- 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 +1 -1
- 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 +7 -9
- 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 +49 -82
- 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 +5 -0
- package/.pi/harness/docs/practice-map.md +10 -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 -51
- package/.pi/lib/agents-policy.mjs +41 -28
- 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/package.json +3 -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/lsp/client.ts +62 -39
- package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
- package/.pi/lib/harness-lens/index.ts +241 -108
- 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 +4 -1
- 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 +2 -1
- 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-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 +288 -125
- package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
- package/.pi/scripts/run-tests.mjs +1 -0
- package/.pi/settings.example.json +1 -0
- package/.sentrux/rules.toml +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +25 -0
- package/README.md +13 -4
- package/package.json +5 -1
- package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
|
@@ -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
|
-
}
|
|
@@ -7,6 +7,10 @@ import { access, readFile, stat } from "node:fs/promises";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
import { validateAgainstHarnessSchema } from "./harness-schema-validate.js";
|
|
10
|
+
import {
|
|
11
|
+
TASK_CLARIFICATION_ARTIFACT,
|
|
12
|
+
validateTaskClarificationDoc,
|
|
13
|
+
} from "./plan-task-clarification.js";
|
|
10
14
|
|
|
11
15
|
export interface ArtifactGateResult {
|
|
12
16
|
ok: boolean;
|
|
@@ -19,12 +23,15 @@ const ARTIFACT_SCHEMA: Record<string, string> = {
|
|
|
19
23
|
"artifacts/implementation-research.yaml":
|
|
20
24
|
"plan-implementation-research-brief.schema.json",
|
|
21
25
|
"artifacts/stack.yaml": "plan-stack-brief.schema.json",
|
|
26
|
+
"artifacts/task-clarification.yaml": "plan-task-clarification.schema.json",
|
|
22
27
|
"artifacts/planning-context.yaml": "plan-planning-context.schema.json",
|
|
23
28
|
"artifacts/eval-verdict.yaml": "eval-verdict.schema.json",
|
|
24
29
|
"artifacts/adversary-report.yaml": "adversary-report.schema.json",
|
|
30
|
+
"artifacts/sentrux-repair-plan.yaml": "sentrux-repair-plan.schema.json",
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
const PREREQUISITE_ORDER: Record<string, string[]> = {
|
|
34
|
+
"artifacts/planning-context.yaml": [TASK_CLARIFICATION_ARTIFACT],
|
|
28
35
|
"artifacts/hypothesis.yaml": ["artifacts/decomposition.yaml"],
|
|
29
36
|
"artifacts/implementation-research.yaml": [
|
|
30
37
|
"artifacts/decomposition.yaml",
|
|
@@ -53,6 +60,63 @@ function artifactStatusBad(doc: Record<string, unknown>): string | null {
|
|
|
53
60
|
return null;
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
async function validatePlanningContextArtifact(
|
|
64
|
+
normalized: string,
|
|
65
|
+
doc: Record<string, unknown>,
|
|
66
|
+
): Promise<string[]> {
|
|
67
|
+
const errors: string[] = [];
|
|
68
|
+
if (normalized !== "artifacts/planning-context.yaml") return errors;
|
|
69
|
+
const statusErr = artifactStatusBad(doc);
|
|
70
|
+
if (statusErr) errors.push(`${normalized}: ${statusErr}`);
|
|
71
|
+
const coverage = doc.coverage as Record<string, unknown> | undefined;
|
|
72
|
+
if (!coverage || typeof coverage !== "object") return errors;
|
|
73
|
+
for (const lane of ["architecture", "structure"] as const) {
|
|
74
|
+
const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
|
|
75
|
+
const laneStatus = String(laneDoc?.status ?? "").toLowerCase();
|
|
76
|
+
if (laneStatus !== "ok" && laneStatus !== "partial") {
|
|
77
|
+
errors.push(
|
|
78
|
+
`${normalized}: coverage.${lane}.status must be ok or partial (got "${laneStatus || "missing"}")`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return errors;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function validateArtifactPrerequisites(
|
|
86
|
+
runRoot: string,
|
|
87
|
+
normalized: string,
|
|
88
|
+
prereqs: string[],
|
|
89
|
+
): Promise<string[]> {
|
|
90
|
+
const errors: string[] = [];
|
|
91
|
+
for (const prereq of prereqs) {
|
|
92
|
+
const prereqPath = join(runRoot, prereq);
|
|
93
|
+
if (!(await fileExists(prereqPath))) {
|
|
94
|
+
errors.push(`${normalized}: prerequisite missing (${prereq})`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (prereq !== TASK_CLARIFICATION_ARTIFACT) continue;
|
|
98
|
+
try {
|
|
99
|
+
const raw = await readFile(prereqPath, "utf-8");
|
|
100
|
+
const prereqDoc = parseYaml(raw) as Record<string, unknown>;
|
|
101
|
+
const clar = validateTaskClarificationDoc(prereqDoc, {
|
|
102
|
+
requireReady: true,
|
|
103
|
+
});
|
|
104
|
+
if (!clar.ok) {
|
|
105
|
+
errors.push(
|
|
106
|
+
...clar.errors.map(
|
|
107
|
+
(e) => `${normalized}: prerequisite ${prereq} — ${e}`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
errors.push(
|
|
113
|
+
`${normalized}: prerequisite ${prereq} invalid or unreadable`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return errors;
|
|
118
|
+
}
|
|
119
|
+
|
|
56
120
|
export async function validateHarnessArtifactFile(
|
|
57
121
|
runRoot: string,
|
|
58
122
|
relPath: string,
|
|
@@ -102,32 +166,22 @@ export async function validateHarnessArtifactFile(
|
|
|
102
166
|
}
|
|
103
167
|
}
|
|
104
168
|
|
|
105
|
-
if (doc && normalized ===
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
errors.push(`${normalized}: ${
|
|
109
|
-
}
|
|
110
|
-
const coverage = doc.coverage as Record<string, unknown> | undefined;
|
|
111
|
-
if (coverage && typeof coverage === "object") {
|
|
112
|
-
for (const lane of ["architecture", "structure"] as const) {
|
|
113
|
-
const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
|
|
114
|
-
const laneStatus = String(laneDoc?.status ?? "").toLowerCase();
|
|
115
|
-
if (laneStatus !== "ok" && laneStatus !== "partial") {
|
|
116
|
-
errors.push(
|
|
117
|
-
`${normalized}: coverage.${lane}.status must be ok or partial (got "${laneStatus || "missing"}")`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
169
|
+
if (doc && normalized === TASK_CLARIFICATION_ARTIFACT) {
|
|
170
|
+
const clar = validateTaskClarificationDoc(doc, { requireReady: true });
|
|
171
|
+
if (!clar.ok) {
|
|
172
|
+
errors.push(...clar.errors.map((e) => `${normalized}: ${e}`));
|
|
121
173
|
}
|
|
122
174
|
}
|
|
123
175
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!(await fileExists(join(runRoot, prereq)))) {
|
|
127
|
-
errors.push(`${normalized}: prerequisite missing (${prereq})`);
|
|
128
|
-
}
|
|
176
|
+
if (doc) {
|
|
177
|
+
errors.push(...(await validatePlanningContextArtifact(normalized, doc)));
|
|
129
178
|
}
|
|
130
179
|
|
|
180
|
+
const prereqs = PREREQUISITE_ORDER[normalized] ?? [];
|
|
181
|
+
errors.push(
|
|
182
|
+
...(await validateArtifactPrerequisites(runRoot, normalized, prereqs)),
|
|
183
|
+
);
|
|
184
|
+
|
|
131
185
|
return { ok: errors.length === 0, errors };
|
|
132
186
|
}
|
|
133
187
|
|