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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** @sync .pi/lib/ask-user/policy.ts — test / node entrypoint */
|
|
2
|
+
|
|
3
|
+
export const PLAN_APPROVE_OPTION =
|
|
4
|
+
/^(approve(d)?(\s+plan)?|yes,?\s+proceed|looks\s+good)$/i;
|
|
5
|
+
export const PLAN_CANCEL_OPTION =
|
|
6
|
+
/^(cancel(led)?|revise|request\s+changes|needs?\s+clarification)$/i;
|
|
7
|
+
|
|
8
|
+
const PLAN_APPROVAL_PHRASE = /plan|approve/i;
|
|
9
|
+
|
|
10
|
+
function optionTitlesFromParams(input) {
|
|
11
|
+
const titles = [];
|
|
12
|
+
for (const o of input.options ?? []) {
|
|
13
|
+
if (typeof o === "string") titles.push(o.trim());
|
|
14
|
+
else if (o && typeof o === "object" && "title" in o) {
|
|
15
|
+
titles.push(String(o.title ?? "").trim());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const q of input.questions ?? []) {
|
|
19
|
+
if (q && typeof q === "object" && "title" in q) {
|
|
20
|
+
titles.push(String(q.title ?? "").trim());
|
|
21
|
+
}
|
|
22
|
+
if (q && typeof q === "object" && "options" in q) {
|
|
23
|
+
for (const o of q.options ?? []) {
|
|
24
|
+
if (typeof o === "string") titles.push(o.trim());
|
|
25
|
+
else if (o && typeof o === "object" && "title" in o) {
|
|
26
|
+
titles.push(String(o.title ?? "").trim());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return titles.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isPlanApprovalAskUser(input) {
|
|
35
|
+
const q = String(input.question ?? "").trim();
|
|
36
|
+
const titles = optionTitlesFromParams(input);
|
|
37
|
+
const hasPlanOptions =
|
|
38
|
+
titles.some(
|
|
39
|
+
(t) => PLAN_APPROVE_OPTION.test(t) || PLAN_CANCEL_OPTION.test(t),
|
|
40
|
+
) || PLAN_APPROVE_OPTION.test(q);
|
|
41
|
+
if (!hasPlanOptions) return false;
|
|
42
|
+
return PLAN_APPROVAL_PHRASE.test(q);
|
|
43
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { AskUserParams } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Match plan-approval option labels — keep in sync with plan-approval/types. */
|
|
4
|
+
export const PLAN_APPROVE_OPTION =
|
|
5
|
+
/^(approve(d)?(\s+plan)?|yes,?\s+proceed|looks\s+good)$/i;
|
|
6
|
+
export const PLAN_CANCEL_OPTION =
|
|
7
|
+
/^(cancel(led)?|revise|request\s+changes|needs?\s+clarification)$/i;
|
|
8
|
+
|
|
9
|
+
const PLAN_APPROVAL_PHRASE = /plan|approve/i;
|
|
10
|
+
|
|
11
|
+
function optionTitlesFromParams(input: {
|
|
12
|
+
options?: unknown[];
|
|
13
|
+
questions?: unknown[];
|
|
14
|
+
}): string[] {
|
|
15
|
+
const titles: string[] = [];
|
|
16
|
+
for (const o of input.options ?? []) {
|
|
17
|
+
if (typeof o === "string") titles.push(o.trim());
|
|
18
|
+
else if (o && typeof o === "object" && "title" in o) {
|
|
19
|
+
titles.push(String((o as { title?: string }).title ?? "").trim());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const q of input.questions ?? []) {
|
|
23
|
+
if (q && typeof q === "object" && "title" in q) {
|
|
24
|
+
titles.push(String((q as { title?: string }).title ?? "").trim());
|
|
25
|
+
}
|
|
26
|
+
if (q && typeof q === "object" && "options" in q) {
|
|
27
|
+
const qOpts = (q as { options?: unknown[] }).options ?? [];
|
|
28
|
+
for (const o of qOpts) {
|
|
29
|
+
if (typeof o === "string") titles.push(o.trim());
|
|
30
|
+
else if (o && typeof o === "object" && "title" in o) {
|
|
31
|
+
titles.push(String((o as { title?: string }).title ?? "").trim());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return titles.filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Detect ask_user calls that duplicate plan approval (must use approve_plan). */
|
|
40
|
+
export function isPlanApprovalAskUser(input: {
|
|
41
|
+
question?: string;
|
|
42
|
+
options?: unknown[];
|
|
43
|
+
questions?: unknown[];
|
|
44
|
+
}): boolean {
|
|
45
|
+
const q = String(input.question ?? "").trim();
|
|
46
|
+
const titles = optionTitlesFromParams(input);
|
|
47
|
+
const hasPlanOptions =
|
|
48
|
+
titles.some(
|
|
49
|
+
(t) => PLAN_APPROVE_OPTION.test(t) || PLAN_CANCEL_OPTION.test(t),
|
|
50
|
+
) || PLAN_APPROVE_OPTION.test(q);
|
|
51
|
+
if (!hasPlanOptions) return false;
|
|
52
|
+
return PLAN_APPROVAL_PHRASE.test(q);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True when harness setup/CI forbids interactive prompts. */
|
|
56
|
+
export function isHarnessNonInteractive(): boolean {
|
|
57
|
+
return (
|
|
58
|
+
process.env.HARNESS_NON_INTERACTIVE === "1" ||
|
|
59
|
+
process.argv.some((a) => a.includes("non-interactive"))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assertSubagentCannotAskUser(agentType: string | undefined): {
|
|
64
|
+
blocked: boolean;
|
|
65
|
+
reason?: string;
|
|
66
|
+
} {
|
|
67
|
+
if (!agentType) return { blocked: false };
|
|
68
|
+
if (agentType.startsWith("harness/reviewing/")) {
|
|
69
|
+
return {
|
|
70
|
+
blocked: true,
|
|
71
|
+
reason: `Tool "ask_user" is not available for ${agentType} (orchestrator-only).`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (agentType.startsWith("harness/planning/")) {
|
|
75
|
+
return {
|
|
76
|
+
blocked: true,
|
|
77
|
+
reason: `Tool "ask_user" is not available for ${agentType} (orchestrator-only).`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (agentType === "harness/running/executor") {
|
|
81
|
+
return {
|
|
82
|
+
blocked: true,
|
|
83
|
+
reason: `Tool "ask_user" is not available for ${agentType} (orchestrator-only).`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { blocked: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function nonInteractiveAskUserResult(question: string): {
|
|
90
|
+
text: string;
|
|
91
|
+
details: Partial<import("./types.js").AskToolDetails>;
|
|
92
|
+
} {
|
|
93
|
+
return {
|
|
94
|
+
text: "ask_user blocked in non-interactive mode — set needs_clarification; do not guess defaults.",
|
|
95
|
+
details: {
|
|
96
|
+
question,
|
|
97
|
+
options: [],
|
|
98
|
+
response: null,
|
|
99
|
+
cancelled: true,
|
|
100
|
+
ui_backend: "headless",
|
|
101
|
+
non_interactive_blocked: true,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { parseGlimpseRawResult } from "../contracts/glimpse-parse.js";
|
|
5
|
+
import { buildGlimpsePayload } from "../contracts/glimpse-payload-build.js";
|
|
6
|
+
import type { DialogResult, ValidatedAskParams } from "../types.js";
|
|
7
|
+
import type { PresenterContext } from "./types.js";
|
|
8
|
+
|
|
9
|
+
function glimpsePackageJson(): string {
|
|
10
|
+
const roots = [process.env.UP_PKG, process.cwd()].filter((r): r is string =>
|
|
11
|
+
Boolean(r),
|
|
12
|
+
);
|
|
13
|
+
for (const root of roots) {
|
|
14
|
+
const pkgJson = join(
|
|
15
|
+
root,
|
|
16
|
+
".pi/npm/node_modules/@alexleekt/pi-ask-user-glimpse/package.json",
|
|
17
|
+
);
|
|
18
|
+
try {
|
|
19
|
+
return createRequire(pkgJson).resolve(
|
|
20
|
+
"@alexleekt/pi-ask-user-glimpse/package.json",
|
|
21
|
+
);
|
|
22
|
+
} catch {
|
|
23
|
+
// try next root
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error(
|
|
27
|
+
"@alexleekt/pi-ask-user-glimpse not installed — run npm install in .pi/npm",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const require = createRequire(glimpsePackageJson());
|
|
32
|
+
|
|
33
|
+
let warnedUnavailable = false;
|
|
34
|
+
|
|
35
|
+
function resolveWebviewHtml(): string {
|
|
36
|
+
const pkgRoot = dirname(
|
|
37
|
+
require.resolve("@alexleekt/pi-ask-user-glimpse/package.json"),
|
|
38
|
+
);
|
|
39
|
+
const distPath = join(pkgRoot, "dist", "index.html");
|
|
40
|
+
const html = readFileSync(distPath, "utf-8");
|
|
41
|
+
if (!html.includes("/*ASK_USER_PAYLOAD*/")) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"@alexleekt/pi-ask-user-glimpse dist/index.html missing ASK_USER_PAYLOAD placeholder",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return html;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isGlimpseAvailable(): boolean {
|
|
50
|
+
try {
|
|
51
|
+
resolveWebviewHtml();
|
|
52
|
+
require.resolve("glimpseui");
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function summarizeTitle(question: string, maxWords = 3): string {
|
|
60
|
+
const words = question.trim().split(/\s+/).slice(0, maxWords);
|
|
61
|
+
const title = words.join(" ");
|
|
62
|
+
return title.length < question.length ? `${title}…` : title;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runGlimpsePresenter(
|
|
66
|
+
validated: ValidatedAskParams,
|
|
67
|
+
ctx: PresenterContext,
|
|
68
|
+
): Promise<DialogResult> {
|
|
69
|
+
const payload = buildGlimpsePayload(validated, ctx.sessionName);
|
|
70
|
+
const baseHtml = resolveWebviewHtml();
|
|
71
|
+
const html = baseHtml.replace(
|
|
72
|
+
"/*ASK_USER_PAYLOAD*/",
|
|
73
|
+
JSON.stringify(payload)
|
|
74
|
+
.replace(/</g, "\\u003c")
|
|
75
|
+
.replace(/>/g, "\\u003e")
|
|
76
|
+
.replace(/&/g, "\\u0026"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const sessionName = ctx.sessionName;
|
|
80
|
+
const questionTitle = summarizeTitle(validated.question);
|
|
81
|
+
const title = sessionName
|
|
82
|
+
? `Pi · ${sessionName} · ${questionTitle}`
|
|
83
|
+
: `Pi · ${questionTitle}`;
|
|
84
|
+
|
|
85
|
+
const windowOptions = {
|
|
86
|
+
width: 1200,
|
|
87
|
+
height: 900,
|
|
88
|
+
title: title.length > 60 ? `${title.slice(0, 57)}…` : title,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const { prompt } = await import("glimpseui");
|
|
93
|
+
const raw = (await prompt(html, windowOptions)) as Record<
|
|
94
|
+
string,
|
|
95
|
+
unknown
|
|
96
|
+
> | null;
|
|
97
|
+
|
|
98
|
+
const cancelled = raw === null || raw?.__cancelled === true;
|
|
99
|
+
const response = parseGlimpseRawResult(raw, cancelled);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
response,
|
|
103
|
+
cancelled,
|
|
104
|
+
ui_backend: "glimpse",
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (!warnedUnavailable) {
|
|
108
|
+
warnedUnavailable = true;
|
|
109
|
+
console.warn(
|
|
110
|
+
"[harness-ask-user] Glimpse unavailable:",
|
|
111
|
+
err instanceof Error ? err.message : err,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Probe glimpse without opening a dialog. */
|
|
119
|
+
export function glimpseHealthCheck(): { ok: boolean; error?: string } {
|
|
120
|
+
try {
|
|
121
|
+
resolveWebviewHtml();
|
|
122
|
+
require.resolve("glimpseui");
|
|
123
|
+
return { ok: true };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: e instanceof Error ? e.message : String(e),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { FREEFORM_OPTION_TITLE } from "../constants.js";
|
|
3
|
+
import {
|
|
4
|
+
detailFromFlatResult,
|
|
5
|
+
mergeQuestionnaireResults,
|
|
6
|
+
questionToFlatParams,
|
|
7
|
+
} from "../core/questionnaire.js";
|
|
8
|
+
import type { DialogResult, ValidatedAskParams } from "../types.js";
|
|
9
|
+
|
|
10
|
+
async function runFlatHeadless(
|
|
11
|
+
ui: ExtensionUIContext,
|
|
12
|
+
validated: ValidatedAskParams,
|
|
13
|
+
): Promise<DialogResult> {
|
|
14
|
+
const { question, context, options, allowMultiple, allowFreeform } =
|
|
15
|
+
validated;
|
|
16
|
+
|
|
17
|
+
const title = context ? `${context}\n\n${question}` : question;
|
|
18
|
+
const labels = options.map((o) => o.title);
|
|
19
|
+
|
|
20
|
+
if (labels.length === 0) {
|
|
21
|
+
if (!allowFreeform) {
|
|
22
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
23
|
+
}
|
|
24
|
+
const text = await ui.input(title, "");
|
|
25
|
+
if (!text?.trim()) {
|
|
26
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
response: { kind: "freeform", text: text.trim() },
|
|
30
|
+
cancelled: false,
|
|
31
|
+
ui_backend: "headless",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (allowMultiple) {
|
|
36
|
+
const selections: string[] = [];
|
|
37
|
+
const remaining = [...labels];
|
|
38
|
+
while (remaining.length > 0) {
|
|
39
|
+
const pick = await ui.select(
|
|
40
|
+
selections.length === 0
|
|
41
|
+
? title
|
|
42
|
+
: `${title}\n(selected: ${selections.join(", ")})`,
|
|
43
|
+
[...remaining, "(done selecting)"],
|
|
44
|
+
);
|
|
45
|
+
if (!pick) {
|
|
46
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
47
|
+
}
|
|
48
|
+
if (pick === "(done selecting)") {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
selections.push(pick);
|
|
52
|
+
const idx = remaining.indexOf(pick);
|
|
53
|
+
if (idx >= 0) remaining.splice(idx, 1);
|
|
54
|
+
}
|
|
55
|
+
if (selections.length === 0) {
|
|
56
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
response: { kind: "selection", selections },
|
|
60
|
+
cancelled: false,
|
|
61
|
+
ui_backend: "headless",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const choices = allowFreeform ? [...labels, FREEFORM_OPTION_TITLE] : labels;
|
|
66
|
+
const picked = await ui.select(title, choices);
|
|
67
|
+
if (!picked) {
|
|
68
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (picked === FREEFORM_OPTION_TITLE) {
|
|
72
|
+
const text = await ui.input(question, "");
|
|
73
|
+
if (!text?.trim()) {
|
|
74
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
response: { kind: "freeform", text: text.trim() },
|
|
78
|
+
cancelled: false,
|
|
79
|
+
ui_backend: "headless",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
response: { kind: "selection", selections: [picked] },
|
|
85
|
+
cancelled: false,
|
|
86
|
+
ui_backend: "headless",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runQuestionnaireHeadless(
|
|
91
|
+
ui: ExtensionUIContext,
|
|
92
|
+
validated: ValidatedAskParams,
|
|
93
|
+
): Promise<DialogResult> {
|
|
94
|
+
const total = validated.questions.length;
|
|
95
|
+
const details = [];
|
|
96
|
+
let last: DialogResult | undefined;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < total; i++) {
|
|
99
|
+
const q = validated.questions[i];
|
|
100
|
+
const flat = questionToFlatParams(validated, q, i, total);
|
|
101
|
+
const step = await runFlatHeadless(ui, flat);
|
|
102
|
+
last = step;
|
|
103
|
+
if (step.cancelled) {
|
|
104
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
105
|
+
}
|
|
106
|
+
const label = q.description ?? q.title;
|
|
107
|
+
const detail = detailFromFlatResult(label, step);
|
|
108
|
+
if (!detail) {
|
|
109
|
+
if (!validated.allowSkip) {
|
|
110
|
+
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
details.push(detail);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return mergeQuestionnaireResults(details, last);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function runHeadlessPresenter(
|
|
121
|
+
ui: ExtensionUIContext,
|
|
122
|
+
validated: ValidatedAskParams,
|
|
123
|
+
): Promise<DialogResult> {
|
|
124
|
+
if (validated.mode === "questionnaire") {
|
|
125
|
+
return runQuestionnaireHeadless(ui, validated);
|
|
126
|
+
}
|
|
127
|
+
return runFlatHeadless(ui, validated);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @deprecated Use runHeadlessPresenter */
|
|
131
|
+
export const runAskFallback = runHeadlessPresenter;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DialogResult, UiBackend, ValidatedAskParams } from "../types.js";
|
|
2
|
+
import { isGlimpseAvailable, runGlimpsePresenter } from "./glimpse.js";
|
|
3
|
+
import { runHeadlessPresenter } from "./headless.js";
|
|
4
|
+
import { runTuiPresenter } from "./tui.js";
|
|
5
|
+
import type { PresenterContext } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export type PresenterChoice = UiBackend;
|
|
8
|
+
|
|
9
|
+
export function resolvePresenterChoice(
|
|
10
|
+
validated: ValidatedAskParams,
|
|
11
|
+
hasUI: boolean,
|
|
12
|
+
): PresenterChoice {
|
|
13
|
+
if (validated.displayMode === "inline") return "tui";
|
|
14
|
+
|
|
15
|
+
const forced = process.env.HARNESS_ASK_USER_UI?.toLowerCase();
|
|
16
|
+
if (forced === "tui") return "tui";
|
|
17
|
+
if (forced === "glimpse") return "glimpse";
|
|
18
|
+
if (forced === "headless") return "headless";
|
|
19
|
+
|
|
20
|
+
if (hasUI && isGlimpseAvailable()) return "glimpse";
|
|
21
|
+
if (hasUI) return "tui";
|
|
22
|
+
return "headless";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function runPresenter(
|
|
26
|
+
choice: PresenterChoice,
|
|
27
|
+
validated: ValidatedAskParams,
|
|
28
|
+
ctx: PresenterContext,
|
|
29
|
+
): Promise<DialogResult> {
|
|
30
|
+
switch (choice) {
|
|
31
|
+
case "glimpse":
|
|
32
|
+
return runGlimpsePresenter(validated, ctx);
|
|
33
|
+
case "headless":
|
|
34
|
+
return runHeadlessPresenter(ctx.ui, validated);
|
|
35
|
+
default:
|
|
36
|
+
return runTuiPresenter(ctx.ui, validated);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run ask_user UI with glimpse→tui degradation on failure.
|
|
42
|
+
*/
|
|
43
|
+
export async function presentAskUser(
|
|
44
|
+
validated: ValidatedAskParams,
|
|
45
|
+
ctx: PresenterContext,
|
|
46
|
+
): Promise<DialogResult> {
|
|
47
|
+
let choice = resolvePresenterChoice(validated, ctx.hasUI);
|
|
48
|
+
|
|
49
|
+
if (choice === "glimpse") {
|
|
50
|
+
try {
|
|
51
|
+
return await runPresenter("glimpse", validated, ctx);
|
|
52
|
+
} catch {
|
|
53
|
+
choice = "tui";
|
|
54
|
+
const outcome = await runPresenter("tui", validated, ctx);
|
|
55
|
+
return { ...outcome, ui_degraded: true };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return runPresenter(choice, validated, ctx);
|
|
60
|
+
}
|