ultimate-pi 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +68 -2
  2. package/.agents/skills/harness-git-commit/SKILL.md +72 -0
  3. package/.agents/skills/harness-governor/SKILL.md +2 -2
  4. package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
  5. package/.agents/skills/harness-plan/SKILL.md +13 -11
  6. package/.agents/skills/harness-review/SKILL.md +1 -1
  7. package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
  8. package/.agents/skills/sentrux/SKILL.md +4 -2
  9. package/.agents/skills/wiki-save/SKILL.md +1 -1
  10. package/.pi/PACKAGING.md +6 -0
  11. package/.pi/SYSTEM.md +21 -3
  12. package/.pi/agents/harness/ls-lint-steward.md +49 -0
  13. package/.pi/agents/harness/planning/decompose.md +4 -4
  14. package/.pi/agents/harness/reviewing/evaluator.md +1 -1
  15. package/.pi/agents/harness/running/executor.md +1 -1
  16. package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
  17. package/.pi/agents/pi-pi/prompt-expert.md +17 -2
  18. package/.pi/auto-commit.json +9 -2
  19. package/.pi/extensions/debate-orchestrator.ts +3 -0
  20. package/.pi/extensions/harness-anchored-edit.ts +7 -9
  21. package/.pi/extensions/harness-ask-user.ts +13 -34
  22. package/.pi/extensions/harness-debate-tools.ts +43 -4
  23. package/.pi/extensions/harness-live-widget.ts +28 -19
  24. package/.pi/extensions/harness-run-context.ts +278 -115
  25. package/.pi/extensions/harness-web-tools.ts +598 -471
  26. package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
  27. package/.pi/extensions/observation-bus.ts +4 -0
  28. package/.pi/extensions/policy-gate.ts +270 -229
  29. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  30. package/.pi/extensions/soundboard.ts +48 -48
  31. package/.pi/harness/README.md +4 -0
  32. package/.pi/harness/agents.manifest.json +15 -7
  33. package/.pi/harness/agents.policy.yaml +49 -82
  34. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  35. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  36. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  37. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  38. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  39. package/.pi/harness/docs/adrs/README.md +5 -0
  40. package/.pi/harness/docs/practice-map.md +10 -5
  41. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  42. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  43. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  44. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  45. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  46. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  47. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  48. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  49. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  50. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  51. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  52. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  53. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  54. package/.pi/lib/agents-policy.d.mts +26 -51
  55. package/.pi/lib/agents-policy.mjs +41 -28
  56. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  57. package/.pi/lib/ask-user/constants.mjs +3 -0
  58. package/.pi/lib/ask-user/constants.ts +4 -0
  59. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  60. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  61. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  62. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  63. package/.pi/lib/ask-user/dialog.ts +2 -314
  64. package/.pi/lib/ask-user/fallback.ts +2 -78
  65. package/.pi/lib/ask-user/format.ts +85 -0
  66. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  67. package/.pi/lib/ask-user/index.ts +114 -0
  68. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  69. package/.pi/lib/ask-user/policy.mjs +43 -0
  70. package/.pi/lib/ask-user/policy.ts +104 -0
  71. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  72. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  73. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  74. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  75. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  76. package/.pi/lib/ask-user/render.ts +40 -9
  77. package/.pi/lib/ask-user/schema.ts +66 -13
  78. package/.pi/lib/ask-user/types.ts +60 -3
  79. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  80. package/.pi/lib/ask-user/validate.ts +53 -34
  81. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  82. package/.pi/lib/harness-artifact-gate.ts +75 -21
  83. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  84. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  85. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  86. package/.pi/lib/harness-lens/index.ts +241 -108
  87. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  88. package/.pi/lib/harness-repair-brief.ts +84 -25
  89. package/.pi/lib/harness-run-context.ts +42 -52
  90. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  91. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  92. package/.pi/lib/harness-slash-completions.ts +116 -0
  93. package/.pi/lib/harness-spawn-topology.ts +121 -87
  94. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  95. package/.pi/lib/harness-subagents-bridge.ts +4 -1
  96. package/.pi/lib/harness-ui-state.ts +95 -48
  97. package/.pi/lib/plan-approval/dialog.ts +5 -0
  98. package/.pi/lib/plan-approval/validate.ts +1 -1
  99. package/.pi/lib/plan-approval-readiness.ts +32 -0
  100. package/.pi/lib/plan-debate-gate.ts +154 -114
  101. package/.pi/lib/plan-task-clarification.ts +158 -0
  102. package/.pi/prompts/harness-auto.md +2 -2
  103. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  104. package/.pi/prompts/harness-plan.md +58 -8
  105. package/.pi/prompts/harness-review.md +40 -6
  106. package/.pi/prompts/harness-run.md +33 -11
  107. package/.pi/prompts/harness-setup.md +72 -3
  108. package/.pi/prompts/harness-steer.md +2 -1
  109. package/.pi/prompts/wiki-save.md +5 -4
  110. package/.pi/scripts/README.md +8 -0
  111. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  112. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  113. package/.pi/scripts/harness-cli-verify.sh +47 -0
  114. package/.pi/scripts/harness-git-churn.mjs +77 -0
  115. package/.pi/scripts/harness-git-commit.mjs +173 -0
  116. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  117. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  118. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  119. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  120. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  121. package/.pi/scripts/harness-verify.mjs +288 -125
  122. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  123. package/.pi/scripts/run-tests.mjs +1 -0
  124. package/.pi/settings.example.json +1 -0
  125. package/.sentrux/rules.toml +1 -1
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +25 -0
  128. package/README.md +13 -4
  129. package/package.json +5 -1
  130. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
@@ -125,6 +125,106 @@ function bashPlanningJsonDenied(command: string, agentType: string): boolean {
125
125
  return PLANNING_ARTIFACT_JSON_WRITE.test(command);
126
126
  }
127
127
 
128
+ function isReadOnlyAgentKind(agentKind: string): boolean {
129
+ return (
130
+ agentKind === "planner" ||
131
+ agentKind === "evaluator" ||
132
+ agentKind === "adversary" ||
133
+ agentKind === "tie_breaker" ||
134
+ agentKind === "trace" ||
135
+ agentKind === "incident" ||
136
+ agentKind === "meta"
137
+ );
138
+ }
139
+
140
+ async function resolvePlanMutation(args: {
141
+ sessionActive: boolean;
142
+ toolName: string;
143
+ toolInput: Record<string, unknown>;
144
+ phase: HarnessPhase;
145
+ runCtx: HarnessRunContext | null;
146
+ projectRoot: string;
147
+ aborted: boolean;
148
+ entries: unknown[];
149
+ sessionId: string;
150
+ }): Promise<{ allowed: boolean; reason?: string }> {
151
+ const mutatingTool = args.toolName === "write" || args.toolName === "edit";
152
+ if (!(args.sessionActive && mutatingTool)) return { allowed: true };
153
+ return isPlanPhaseAllowedMutation(
154
+ args.toolName,
155
+ args.toolInput,
156
+ args.phase,
157
+ args.runCtx,
158
+ args.projectRoot,
159
+ {
160
+ aborted: args.aborted,
161
+ entries: args.entries,
162
+ ownerSessionId: args.runCtx?.owner_pi_session_id,
163
+ currentSessionId: args.sessionId,
164
+ },
165
+ );
166
+ }
167
+
168
+ function resolveContextMode(args: {
169
+ sessionActive: boolean;
170
+ toolName: string;
171
+ toolInput: Record<string, unknown>;
172
+ phase: HarnessPhase;
173
+ aborted: boolean;
174
+ budgetBypass: boolean;
175
+ agentKind: string;
176
+ }): { blocked: boolean; reason?: string } {
177
+ if (!args.sessionActive) return { blocked: false, reason: "" };
178
+ return evaluateContextModeMutation(
179
+ args.toolName,
180
+ args.toolInput,
181
+ args.phase,
182
+ {
183
+ aborted: args.aborted,
184
+ budgetBypass: args.budgetBypass,
185
+ readOnlyAgent: isReadOnlyAgentKind(args.agentKind),
186
+ },
187
+ );
188
+ }
189
+
190
+ function shouldBlockEvalPlanPacketWrite(args: {
191
+ sessionActive: boolean;
192
+ runCtx: HarnessRunContext | null;
193
+ phase: HarnessPhase;
194
+ toolName: string;
195
+ toolInput: Record<string, unknown>;
196
+ }): boolean {
197
+ if (!args.sessionActive || !args.runCtx?.plan_packet_path) return false;
198
+ if (args.phase !== "evaluate" && args.phase !== "adversary") return false;
199
+ if (args.toolName !== "write" && args.toolName !== "edit") return false;
200
+ const target = String(args.toolInput.path ?? args.toolInput.filePath ?? "");
201
+ return target.includes("plan-packet.yaml");
202
+ }
203
+
204
+ function resolveMutatingBashFlags(args: {
205
+ sessionActive: boolean;
206
+ bashCommand: string;
207
+ aborted: boolean;
208
+ phase: HarnessPhase;
209
+ toolName: string;
210
+ }): { mutatingBashPhaseBlock: boolean; abortMutatingBlock: boolean } {
211
+ const isMutating = Boolean(
212
+ args.bashCommand && isMutatingBash(args.bashCommand),
213
+ );
214
+ return {
215
+ mutatingBashPhaseBlock:
216
+ args.sessionActive &&
217
+ isMutating &&
218
+ !args.aborted &&
219
+ args.phase !== "execute" &&
220
+ args.phase !== "merge",
221
+ abortMutatingBlock:
222
+ args.sessionActive &&
223
+ args.aborted &&
224
+ (isMutating || args.toolName === "write" || args.toolName === "edit"),
225
+ };
226
+ }
227
+
128
228
  function harnessSessionActive(entries: unknown[]): boolean {
129
229
  return isHarnessAutoSession(entries);
130
230
  }
@@ -142,35 +242,27 @@ export async function buildHarnessAgtEvaluationContext(
142
242
  input.toolName === "bash" ? String(input.toolInput.command ?? "") : "";
143
243
  const sessionActive = harnessSessionActive(input.entries);
144
244
 
145
- const MUTATING_FILE_TOOLS = new Set(["write", "edit"]);
146
- const planMutation =
147
- sessionActive && MUTATING_FILE_TOOLS.has(input.toolName)
148
- ? await isPlanPhaseAllowedMutation(
149
- input.toolName,
150
- input.toolInput,
151
- phase,
152
- runCtx,
153
- input.projectRoot,
154
- {
155
- aborted: input.policyState.aborted,
156
- entries: input.entries,
157
- ownerSessionId: runCtx?.owner_pi_session_id,
158
- currentSessionId: input.sessionId,
159
- },
160
- )
161
- : { allowed: true };
245
+ const planMutation = await resolvePlanMutation({
246
+ sessionActive,
247
+ toolName: input.toolName,
248
+ toolInput: input.toolInput,
249
+ phase,
250
+ runCtx,
251
+ projectRoot: input.projectRoot,
252
+ aborted: input.policyState.aborted,
253
+ entries: input.entries,
254
+ sessionId: input.sessionId,
255
+ });
162
256
 
163
- const ctxMode = sessionActive
164
- ? evaluateContextModeMutation(input.toolName, input.toolInput, phase, {
165
- aborted: input.policyState.aborted,
166
- budgetBypass: input.policyState.budgetBypass,
167
- readOnlyAgent:
168
- agentKind === "planner" ||
169
- agentKind === "evaluator" ||
170
- agentKind === "adversary" ||
171
- agentKind === "tie_breaker",
172
- })
173
- : { blocked: false, reason: "" };
257
+ const ctxMode = resolveContextMode({
258
+ sessionActive,
259
+ toolName: input.toolName,
260
+ toolInput: input.toolInput,
261
+ phase,
262
+ aborted: input.policyState.aborted,
263
+ budgetBypass: input.policyState.budgetBypass,
264
+ agentKind,
265
+ });
174
266
 
175
267
  const spawnDecision = evaluateSubagentToolCall(input.toolName, agentId);
176
268
 
@@ -184,40 +276,27 @@ export async function buildHarnessAgtEvaluationContext(
184
276
  isParentOrchestrator,
185
277
  });
186
278
 
187
- let evalPlanPacketBlock = false;
188
- if (
189
- sessionActive &&
190
- runCtx?.plan_packet_path &&
191
- (phase === "evaluate" || phase === "adversary") &&
192
- (input.toolName === "write" || input.toolName === "edit")
193
- ) {
194
- const target = String(
195
- input.toolInput.path ?? input.toolInput.filePath ?? "",
196
- );
197
- if (target.includes("plan-packet.yaml")) {
198
- evalPlanPacketBlock = true;
199
- }
200
- }
279
+ const evalPlanPacketBlock = shouldBlockEvalPlanPacketWrite({
280
+ sessionActive,
281
+ runCtx,
282
+ phase,
283
+ toolName: input.toolName,
284
+ toolInput: input.toolInput,
285
+ });
201
286
 
202
287
  const writePath =
203
288
  input.toolName === "write" || input.toolName === "edit"
204
289
  ? extractWritePathFromToolInput(input.toolInput)
205
290
  : null;
206
291
 
207
- const mutatingBashPhaseBlock =
208
- sessionActive &&
209
- Boolean(bashCommand && isMutatingBash(bashCommand)) &&
210
- !input.policyState.aborted &&
211
- phase !== "execute" &&
212
- phase !== "merge";
213
-
214
- const abortMutatingBlock =
215
- sessionActive &&
216
- input.policyState.aborted &&
217
- ((bashCommand && isMutatingBash(bashCommand)) ||
218
- input.toolName === "write" ||
219
- input.toolName === "edit");
220
-
292
+ const { mutatingBashPhaseBlock, abortMutatingBlock } =
293
+ resolveMutatingBashFlags({
294
+ sessionActive,
295
+ bashCommand,
296
+ aborted: input.policyState.aborted,
297
+ phase,
298
+ toolName: input.toolName,
299
+ });
221
300
  return {
222
301
  tool_name: input.toolName,
223
302
  harness_phase: phase,
@@ -249,14 +328,7 @@ export async function buildHarnessAgtEvaluationContext(
249
328
  eval_plan_packet_write_block: evalPlanPacketBlock,
250
329
  is_submit_tool: input.toolName.startsWith("submit_"),
251
330
  is_planning_agent: isHarnessPlanningAgent(agentId),
252
- is_read_only_kind:
253
- agentKind === "planner" ||
254
- agentKind === "evaluator" ||
255
- agentKind === "adversary" ||
256
- agentKind === "tie_breaker" ||
257
- agentKind === "trace" ||
258
- agentKind === "incident" ||
259
- agentKind === "meta",
331
+ is_read_only_kind: isReadOnlyAgentKind(agentKind),
260
332
  is_executor_kind: agentKind === "executor",
261
333
  trust_score: Number(process.env.HARNESS_TRUST_SCORE ?? "1"),
262
334
  delegation_ceiling: Number(process.env.HARNESS_DELEGATION_CEILING ?? "1"),
@@ -0,0 +1,3 @@
1
+ /** @sync .pi/lib/ask-user/constants.ts */
2
+ export const FREEFORM_OPTION_TITLE = "Type something…";
3
+ export const MAX_QUESTIONNAIRE_QUESTIONS = 8;
@@ -0,0 +1,4 @@
1
+ /** Freeform row label in TUI/headless presenters (agents: "custom answer", not this literal). */
2
+ export const FREEFORM_OPTION_TITLE = "Type something…";
3
+
4
+ export const MAX_QUESTIONNAIRE_QUESTIONS = 8;
@@ -0,0 +1,56 @@
1
+ import type { AskResponse, QuestionnaireDetail } from "../types.js";
2
+
3
+ export function parseGlimpseRawResult(
4
+ raw: Record<string, unknown> | null,
5
+ cancelled: boolean,
6
+ ): AskResponse | null {
7
+ if (cancelled || !raw) return null;
8
+
9
+ const kind = raw.kind;
10
+ if (kind === "freeform") {
11
+ return {
12
+ kind: "freeform",
13
+ text: String(raw.text ?? "").trim(),
14
+ additionalComments: pickString(raw.additionalComments),
15
+ };
16
+ }
17
+
18
+ if (kind === "questionnaire") {
19
+ const questionnaireDetails: QuestionnaireDetail[] = Array.isArray(
20
+ raw.questionnaireDetails,
21
+ )
22
+ ? raw.questionnaireDetails.map((d: unknown) => {
23
+ const entry = d as Record<string, unknown>;
24
+ return {
25
+ question: String(entry.question ?? ""),
26
+ answer: String(entry.answer ?? ""),
27
+ kind: entry.kind === "freeform" ? "freeform" : "selection",
28
+ comment: pickString(entry.comment),
29
+ };
30
+ })
31
+ : [];
32
+
33
+ return {
34
+ kind: "questionnaire",
35
+ questionnaireDetails,
36
+ additionalComments: pickString(raw.additionalComments),
37
+ };
38
+ }
39
+
40
+ const selections = Array.isArray(raw.selections)
41
+ ? raw.selections.map(String)
42
+ : raw.selection
43
+ ? [String(raw.selection)]
44
+ : [];
45
+
46
+ return {
47
+ kind: "selection",
48
+ selections,
49
+ comment: pickString(raw.comment),
50
+ additionalComments: pickString(raw.additionalComments),
51
+ };
52
+ }
53
+
54
+ function pickString(raw: unknown): string | undefined {
55
+ return raw ? String(raw) : undefined;
56
+ }
@@ -0,0 +1,58 @@
1
+ import type { ValidatedAskParams } from "../types.js";
2
+ import type { GlimpseAskUserPayload } from "./glimpse-payload.js";
3
+
4
+ export function buildGlimpsePayload(
5
+ validated: ValidatedAskParams,
6
+ sessionName?: string,
7
+ ): GlimpseAskUserPayload {
8
+ const hasQuestions = validated.mode === "questionnaire";
9
+ const hasOptions = validated.options.length > 0;
10
+
11
+ let payloadType: GlimpseAskUserPayload["type"];
12
+ if (hasQuestions) {
13
+ payloadType = "questionnaire";
14
+ } else if (!hasOptions) {
15
+ payloadType = "freeform";
16
+ } else if (validated.allowMultiple) {
17
+ payloadType = "multi-select";
18
+ } else {
19
+ payloadType = "single-select";
20
+ }
21
+
22
+ let question = validated.question;
23
+ let context = validated.context;
24
+ if (!context && validated.question.length > 120) {
25
+ const match = validated.question.match(/^(.+?[.?!])(\s+|$)/);
26
+ if (match && match[0].length < validated.question.length) {
27
+ question = match[1].trim();
28
+ context = validated.question.slice(match[0].length).trim();
29
+ }
30
+ }
31
+
32
+ return {
33
+ type: payloadType,
34
+ question,
35
+ context,
36
+ contextFormat: validated.contextFormat,
37
+ options: validated.options.map((o) => ({
38
+ title: o.title,
39
+ description: o.description,
40
+ recommended: o.recommended,
41
+ })),
42
+ questions: validated.questions.map((q) => ({
43
+ title: q.title,
44
+ description: q.description,
45
+ allowMultiple: q.allowMultiple,
46
+ options: q.options.map((o) => ({
47
+ title: o.title,
48
+ description: o.description,
49
+ recommended: o.recommended,
50
+ })),
51
+ })),
52
+ allowMultiple: validated.allowMultiple,
53
+ allowFreeform: validated.allowFreeform,
54
+ allowComment: validated.allowComment,
55
+ allowSkip: validated.allowSkip,
56
+ sessionName,
57
+ };
58
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Synced with @alexleekt/pi-ask-user-glimpse@0.5.1 shared/ask-user.ts — harness-owned payload contract.
3
+ */
4
+
5
+ export interface GlimpseQuestionOption {
6
+ title: string;
7
+ description?: string;
8
+ recommended?: boolean;
9
+ }
10
+
11
+ export interface GlimpseQuestion {
12
+ title: string;
13
+ description?: string;
14
+ options?: GlimpseQuestionOption[];
15
+ allowMultiple?: boolean;
16
+ }
17
+
18
+ export type GlimpsePayloadType =
19
+ | "single-select"
20
+ | "multi-select"
21
+ | "questionnaire"
22
+ | "freeform";
23
+
24
+ export interface GlimpseAskUserPayload {
25
+ type: GlimpsePayloadType;
26
+ question: string;
27
+ context?: string;
28
+ contextFormat?: "markdown" | "html";
29
+ options: GlimpseQuestionOption[];
30
+ questions?: GlimpseQuestion[];
31
+ allowMultiple: boolean;
32
+ allowFreeform: boolean;
33
+ allowComment: boolean;
34
+ allowSkip?: boolean;
35
+ sessionName?: string;
36
+ }
37
+
38
+ export const GLIMPSE_FREEFORM_OPTION_TITLE = "My answer isn't listed above";
@@ -0,0 +1,74 @@
1
+ import type {
2
+ DialogResult,
3
+ NormalizedQuestion,
4
+ QuestionnaireDetail,
5
+ ValidatedAskParams,
6
+ } from "../types.js";
7
+
8
+ /** Build flat ask params for one questionnaire card (TUI/headless sequential). */
9
+ export function questionToFlatParams(
10
+ parent: ValidatedAskParams,
11
+ q: NormalizedQuestion,
12
+ index: number,
13
+ total: number,
14
+ ): ValidatedAskParams {
15
+ const header =
16
+ total > 1 ? `[${index + 1}/${total}] ${parent.question}` : parent.question;
17
+ return {
18
+ ...parent,
19
+ mode: "flat",
20
+ question: q.title,
21
+ context: [header, q.description].filter(Boolean).join("\n\n") || undefined,
22
+ options: q.options,
23
+ questions: [],
24
+ allowMultiple: q.allowMultiple,
25
+ allowFreeform: q.options.length === 0 ? true : parent.allowFreeform,
26
+ };
27
+ }
28
+
29
+ export function mergeQuestionnaireResults(
30
+ details: QuestionnaireDetail[],
31
+ last?: DialogResult,
32
+ ): DialogResult {
33
+ const additionalComments =
34
+ last?.response?.kind === "freeform"
35
+ ? last.response.text
36
+ : last?.response && "additionalComments" in last.response
37
+ ? last.response.additionalComments
38
+ : undefined;
39
+
40
+ return {
41
+ response: {
42
+ kind: "questionnaire",
43
+ questionnaireDetails: details,
44
+ additionalComments,
45
+ },
46
+ cancelled: false,
47
+ ui_backend: last?.ui_backend ?? "tui",
48
+ ui_degraded: last?.ui_degraded,
49
+ };
50
+ }
51
+
52
+ export function detailFromFlatResult(
53
+ questionLabel: string,
54
+ result: DialogResult,
55
+ ): QuestionnaireDetail | null {
56
+ if (result.cancelled || !result.response) return null;
57
+ const r = result.response;
58
+ if (r.kind === "freeform") {
59
+ return {
60
+ question: questionLabel,
61
+ answer: r.text,
62
+ kind: "freeform",
63
+ };
64
+ }
65
+ if (r.kind === "selection") {
66
+ return {
67
+ question: questionLabel,
68
+ answer: r.selections.join(", "),
69
+ kind: "selection",
70
+ comment: r.comment,
71
+ };
72
+ }
73
+ return null;
74
+ }