@hobin/developer 0.1.5 → 0.1.6

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/extensions/tui.ts CHANGED
@@ -1,8 +1,9 @@
1
- import {
2
- type ExtensionCommandContext,
3
- type KeybindingsManager,
4
- type Theme,
5
- type ThemeColor,
1
+ import type {
2
+ ExtensionCommandContext,
3
+ ExtensionContext,
4
+ KeybindingsManager,
5
+ Theme,
6
+ ThemeColor,
6
7
  } from "@earendil-works/pi-coding-agent";
7
8
  import {
8
9
  Container,
@@ -17,6 +18,8 @@ import {
17
18
 
18
19
  import {
19
20
  protocolState,
21
+ type ChoiceResponseField,
22
+ type ChoiceResponseOption,
20
23
  type DeveloperMode,
21
24
  type DeveloperState,
22
25
  type JudgmentStatus,
@@ -24,7 +27,9 @@ import {
24
27
  type ProtocolState,
25
28
  } from "./state.ts";
26
29
 
27
- export type DeveloperAction = "status" | "questions" | DeveloperMode;
30
+ export type DeveloperAction =
31
+ | { kind: "command"; value: "status" | "questions" | DeveloperMode }
32
+ | { kind: "question"; questionId: string };
28
33
 
29
34
  function modeName(mode: DeveloperMode): string {
30
35
  if (mode === "on") return "adaptive";
@@ -38,7 +43,8 @@ function protocolColor(value: ProtocolState): ThemeColor {
38
43
  value === "needs-answer" ||
39
44
  value === "needs-routing" ||
40
45
  value === "needs-verification"
41
- ) return "warning";
46
+ )
47
+ return "warning";
42
48
  if (value === "needs-judgment") return "accent";
43
49
  return "dim";
44
50
  }
@@ -87,13 +93,7 @@ export function developerActionItems(state: DeveloperState): SelectItem[] {
87
93
  description: `${modeName(state.mode)} · ${currentProtocol} · ${state.pendingQuestions.length} open`,
88
94
  },
89
95
  ];
90
- if (state.pendingQuestions.length > 0) {
91
- items.push({
92
- value: "questions",
93
- label: "Revisit an open question",
94
- description: `Choose from ${state.pendingQuestions.length} unresolved question(s) and prepare the editor`,
95
- });
96
- }
96
+ items.push(...pendingQuestionItems(state.pendingQuestions));
97
97
  items.push(
98
98
  {
99
99
  value: "on",
@@ -127,9 +127,13 @@ function resolutionRequestLabel(owner: PendingQuestion["resolutionOwner"]): stri
127
127
  return "Evidence or investigation request for Pi:";
128
128
  }
129
129
 
130
+ const MAX_IMMEDIATE_REQUEST_BYTES = 16_000;
131
+ const MAX_IMMEDIATE_REQUEST_LINES = 1_000;
130
132
  const ENABLE_MOUSE_SCROLL = "\u001b[?1000h\u001b[?1006h";
131
133
  const DISABLE_MOUSE_SCROLL = "\u001b[?1006l\u001b[?1000l";
132
134
 
135
+ export type ImmediateQuestionDisposition = { kind: "answer"; request: string } | { kind: "defer" };
136
+
133
137
  interface WritableTerminal {
134
138
  write(data: string): void;
135
139
  }
@@ -175,13 +179,19 @@ interface SelectDialogOptions {
175
179
  selectedDescriptionMaxLines: number;
176
180
  }
177
181
 
178
- function boundedWrappedLines(content: string, width: number, maxLines: number, ellipsis: string): string[] {
182
+ function boundedWrappedLines(
183
+ content: string,
184
+ width: number,
185
+ maxLines: number,
186
+ ellipsis: string,
187
+ ): string[] {
179
188
  const contentWidth = Math.max(1, width);
180
189
  const wrapped = wrapTextWithAnsi(content, contentWidth);
181
190
  const visible = wrapped.slice(0, Math.max(1, maxLines));
182
191
  if (wrapped.length > visible.length && visible.length > 0) {
183
192
  const last = visible.length - 1;
184
- visible[last] = truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + ellipsis;
193
+ visible[last] =
194
+ truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + ellipsis;
185
195
  }
186
196
  return visible;
187
197
  }
@@ -237,7 +247,10 @@ class WrappedSelectList {
237
247
  const lines: string[] = [];
238
248
  const startIndex = Math.max(
239
249
  0,
240
- Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.items.length - this.maxVisible),
250
+ Math.min(
251
+ this.selectedIndex - Math.floor(this.maxVisible / 2),
252
+ this.items.length - this.maxVisible,
253
+ ),
241
254
  );
242
255
  const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
243
256
 
@@ -273,7 +286,11 @@ class WrappedSelectList {
273
286
  lines.push(
274
287
  this.theme.fg(
275
288
  "dim",
276
- truncateToWidth(` (${this.selectedIndex + 1}/${this.items.length})`, Math.max(1, width - 2), ""),
289
+ truncateToWidth(
290
+ ` (${this.selectedIndex + 1}/${this.items.length})`,
291
+ Math.max(1, width - 2),
292
+ "",
293
+ ),
277
294
  ),
278
295
  );
279
296
  }
@@ -287,9 +304,11 @@ class WrappedSelectList {
287
304
  if (wheelDirection !== undefined) {
288
305
  this.moveSelection(wheelDirection * 3);
289
306
  } else if (this.keybindings.matches(data, "tui.select.up")) {
290
- this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
307
+ this.selectedIndex =
308
+ this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
291
309
  } else if (this.keybindings.matches(data, "tui.select.down")) {
292
- this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
310
+ this.selectedIndex =
311
+ this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
293
312
  } else if (this.keybindings.matches(data, "tui.select.pageUp")) {
294
313
  this.moveSelection(-Math.max(1, this.maxVisible - 1));
295
314
  } else if (this.keybindings.matches(data, "tui.select.pageDown")) {
@@ -314,7 +333,7 @@ class WrappedSelectList {
314
333
  }
315
334
 
316
335
  async function showSelectDialog(
317
- ctx: ExtensionCommandContext,
336
+ ctx: ExtensionContext,
318
337
  options: SelectDialogOptions,
319
338
  ): Promise<string | undefined> {
320
339
  let mouseTerminal: WritableTerminal | undefined;
@@ -329,7 +348,9 @@ async function showSelectDialog(
329
348
  const updateText = () => {
330
349
  title.setText(theme.fg("accent", theme.bold(`◆ ${options.title}`)));
331
350
  subtitle.setText(theme.fg("muted", options.subtitle));
332
- hint.setText(theme.fg("dim", "wheel/↑↓ · PgUp/PgDn · Home/End · enter select · esc cancel"));
351
+ hint.setText(
352
+ theme.fg("dim", "wheel/↑↓ · PgUp/PgDn · Home/End · enter select · esc cancel"),
353
+ );
333
354
  };
334
355
  updateText();
335
356
 
@@ -396,8 +417,18 @@ export async function showDeveloperActionSelector(
396
417
  selectedLabelMaxLines: 3,
397
418
  selectedDescriptionMaxLines: 3,
398
419
  });
399
- if (result === "status" || result === "questions" || result === "on" || result === "strict" || result === "off") {
400
- return result;
420
+ if (!result) return undefined;
421
+ if (
422
+ result === "status" ||
423
+ result === "questions" ||
424
+ result === "on" ||
425
+ result === "strict" ||
426
+ result === "off"
427
+ ) {
428
+ return { kind: "command", value: result };
429
+ }
430
+ if (state.pendingQuestions.some((question) => question.id === result)) {
431
+ return { kind: "question", questionId: result };
401
432
  }
402
433
  return undefined;
403
434
  }
@@ -409,7 +440,8 @@ export function showPendingQuestionSelector(
409
440
  if (questions.length === 0) return Promise.resolve(undefined);
410
441
  return showSelectDialog(ctx, {
411
442
  title: "Resolve an open Developer question",
412
- subtitle: "Enter opens an answer/evidence editor; the question closes after a resolved or not-applicable judgment",
443
+ subtitle:
444
+ "Enter opens an answer/evidence editor; the question closes after a resolved or not-applicable judgment",
413
445
  items: pendingQuestionItems(questions),
414
446
  width: "92%",
415
447
  minWidth: 88,
@@ -419,6 +451,240 @@ export function showPendingQuestionSelector(
419
451
  });
420
452
  }
421
453
 
454
+ interface ChoiceResponseAnswer {
455
+ option: ChoiceResponseOption;
456
+ detail?: string;
457
+ }
458
+
459
+ function responseWithinLimits(
460
+ ctx: ExtensionContext,
461
+ request: string,
462
+ label = "Question response",
463
+ ): boolean {
464
+ const requestBytes = new TextEncoder().encode(request).byteLength;
465
+ const requestLines = request.split(/\r?\n/).length;
466
+ if (requestBytes <= MAX_IMMEDIATE_REQUEST_BYTES && requestLines <= MAX_IMMEDIATE_REQUEST_LINES) {
467
+ return true;
468
+ }
469
+ ctx.ui.notify(
470
+ `${label} is too large (${requestBytes} bytes, ${requestLines} lines); shorten it before submitting.`,
471
+ "warning",
472
+ );
473
+ return false;
474
+ }
475
+
476
+ function choiceDetailInitial(field: ChoiceResponseField, option: ChoiceResponseOption): string {
477
+ return [
478
+ `Decision: ${field.prompt}`,
479
+ `Selected: ${option.value} — ${option.label}`,
480
+ "",
481
+ option.detailPrompt,
482
+ "",
483
+ "Required detail:",
484
+ ].join("\n");
485
+ }
486
+
487
+ async function editChoiceDetail(
488
+ ctx: ExtensionContext,
489
+ field: ChoiceResponseField,
490
+ option: ChoiceResponseOption,
491
+ ): Promise<string | undefined> {
492
+ const initial = choiceDetailInitial(field, option);
493
+ while (true) {
494
+ const response = await ctx.ui.editor(`Add detail for ${field.id} · ${option.value}`, initial);
495
+ if (response === undefined) return undefined;
496
+ const detail = (
497
+ response.startsWith(initial) ? response.slice(initial.length) : response
498
+ ).trim();
499
+ if (!detail) {
500
+ ctx.ui.notify("This choice requires a non-empty detail.", "warning");
501
+ continue;
502
+ }
503
+ if (!responseWithinLimits(ctx, detail, "Choice detail")) continue;
504
+ return detail;
505
+ }
506
+ }
507
+
508
+ function structuredResolutionRequest(
509
+ question: PendingQuestion,
510
+ answers: ReadonlyArray<ChoiceResponseAnswer | undefined>,
511
+ ): string {
512
+ const fields = question.responseSpec?.fields ?? [];
513
+ const lines = [questionResolutionPrompt(question), "", "Structured answer:"];
514
+ for (const [index, field] of fields.entries()) {
515
+ const answer = answers[index];
516
+ if (!answer) continue;
517
+ lines.push(`- ${field.id}: ${answer.option.value} — ${answer.option.label}`);
518
+ if (answer.detail) lines.push(` Detail: ${answer.detail}`);
519
+ }
520
+ return lines.join("\n");
521
+ }
522
+
523
+ type ChoiceFieldAction =
524
+ | { kind: "answer"; answer: ChoiceResponseAnswer }
525
+ | { kind: "cancel" }
526
+ | { kind: "retry" };
527
+
528
+ type ChoiceReviewAction =
529
+ | { kind: "submit" }
530
+ | { kind: "edit"; fieldIndex: number }
531
+ | { kind: "back" };
532
+
533
+ async function selectChoiceAnswer(
534
+ ctx: ExtensionContext,
535
+ field: ChoiceResponseField,
536
+ fieldIndex: number,
537
+ fieldCount: number,
538
+ ): Promise<ChoiceFieldAction> {
539
+ const selectedValue = await showSelectDialog(ctx, {
540
+ title: `Decision ${fieldIndex + 1}/${fieldCount} · ${field.id}`,
541
+ subtitle: field.description ?? field.prompt,
542
+ items: field.options.map((option) => ({
543
+ value: option.value,
544
+ label: `${option.value} · ${option.label}`,
545
+ description: option.description ?? field.prompt,
546
+ })),
547
+ width: "92%",
548
+ minWidth: 88,
549
+ maxVisible: Math.min(field.options.length, 12),
550
+ selectedLabelMaxLines: 3,
551
+ selectedDescriptionMaxLines: 4,
552
+ });
553
+ if (!selectedValue) return { kind: "cancel" };
554
+ const option = field.options.find((candidate) => candidate.value === selectedValue);
555
+ if (!option) return { kind: "retry" };
556
+ const detail = option.detailPrompt ? await editChoiceDetail(ctx, field, option) : undefined;
557
+ if (option.detailPrompt && detail === undefined) return { kind: "retry" };
558
+ return { kind: "answer", answer: { option, detail } };
559
+ }
560
+
561
+ async function reviewChoiceAnswers(
562
+ ctx: ExtensionContext,
563
+ question: PendingQuestion,
564
+ fields: ChoiceResponseField[],
565
+ answers: ReadonlyArray<ChoiceResponseAnswer | undefined>,
566
+ ): Promise<ChoiceReviewAction> {
567
+ const action = await showSelectDialog(ctx, {
568
+ title: "Review structured answer",
569
+ subtitle: question.question,
570
+ items: [
571
+ {
572
+ value: "submit",
573
+ label: "Submit answers",
574
+ description: "Send these decisions to Pi; the question remains open until judgment",
575
+ },
576
+ ...fields.map((field, index) => {
577
+ const answer = answers[index];
578
+ return {
579
+ value: `edit:${index}`,
580
+ label: `${field.id} · ${answer?.option.value ?? "missing"} — ${answer?.option.label ?? "Missing answer"}`,
581
+ description: answer?.detail ? `Detail: ${answer.detail}` : "Enter to change this answer",
582
+ };
583
+ }),
584
+ ],
585
+ width: "92%",
586
+ minWidth: 88,
587
+ maxVisible: Math.min(fields.length + 1, 12),
588
+ selectedLabelMaxLines: 3,
589
+ selectedDescriptionMaxLines: 4,
590
+ });
591
+ if (action === "submit") return { kind: "submit" };
592
+ if (!action?.startsWith("edit:")) return { kind: "back" };
593
+ const fieldIndex = Number(action.slice("edit:".length));
594
+ if (!Number.isInteger(fieldIndex) || fieldIndex < 0 || fieldIndex >= fields.length) {
595
+ return { kind: "back" };
596
+ }
597
+ return { kind: "edit", fieldIndex };
598
+ }
599
+
600
+ function previousChoiceField(
601
+ fieldIndex: number,
602
+ fieldCount: number,
603
+ returnToReview: boolean,
604
+ ): number | undefined {
605
+ if (returnToReview) return fieldCount;
606
+ return fieldIndex === 0 ? undefined : fieldIndex - 1;
607
+ }
608
+
609
+ async function collectChoiceResponse(
610
+ ctx: ExtensionContext,
611
+ question: PendingQuestion,
612
+ ): Promise<string | undefined> {
613
+ const fields = question.responseSpec?.fields;
614
+ if (!fields || fields.length === 0) return undefined;
615
+ const answers: Array<ChoiceResponseAnswer | undefined> = [];
616
+ let fieldIndex = 0;
617
+ let returnToReview = false;
618
+
619
+ while (true) {
620
+ if (fieldIndex < fields.length) {
621
+ const field = fields[fieldIndex];
622
+ if (!field) return undefined;
623
+ const action = await selectChoiceAnswer(ctx, field, fieldIndex, fields.length);
624
+ if (action.kind === "retry") continue;
625
+ if (action.kind === "cancel") {
626
+ fieldIndex = previousChoiceField(fieldIndex, fields.length, returnToReview) ?? -1;
627
+ }
628
+ if (fieldIndex < 0) return undefined;
629
+ if (action.kind === "cancel") continue;
630
+ answers[fieldIndex] = action.answer;
631
+ fieldIndex = returnToReview ? fields.length : fieldIndex + 1;
632
+ continue;
633
+ }
634
+
635
+ const action = await reviewChoiceAnswers(ctx, question, fields, answers);
636
+ if (action.kind === "submit") return structuredResolutionRequest(question, answers);
637
+ if (action.kind === "edit") {
638
+ fieldIndex = action.fieldIndex;
639
+ returnToReview = true;
640
+ continue;
641
+ }
642
+ returnToReview = false;
643
+ fieldIndex = fields.length - 1;
644
+ }
645
+ }
646
+
647
+ export async function promptImmediateUserQuestion(
648
+ ctx: ExtensionContext,
649
+ question: PendingQuestion,
650
+ ): Promise<ImmediateQuestionDisposition> {
651
+ while (true) {
652
+ const action = await showSelectDialog(ctx, {
653
+ title: "Developer needs a decision",
654
+ subtitle: question.question,
655
+ items: [
656
+ {
657
+ value: "answer",
658
+ label: "Answer now",
659
+ description:
660
+ "Review the full context and provide the decision required before direct work",
661
+ },
662
+ {
663
+ value: "defer",
664
+ label: "Leave open",
665
+ description: "Keep this question in Developer and answer it later",
666
+ },
667
+ ],
668
+ width: "92%",
669
+ minWidth: 88,
670
+ maxVisible: 2,
671
+ selectedLabelMaxLines: 3,
672
+ selectedDescriptionMaxLines: 3,
673
+ });
674
+ if (action !== "answer") return { kind: "defer" };
675
+
676
+ const request = question.responseSpec
677
+ ? await collectChoiceResponse(ctx, question)
678
+ : await ctx.ui.editor(
679
+ "Answer the new Developer question",
680
+ questionResolutionPrompt(question),
681
+ );
682
+ if (request === undefined) continue;
683
+ if (!responseWithinLimits(ctx, request)) continue;
684
+ return { kind: "answer", request };
685
+ }
686
+ }
687
+
422
688
  export class DeveloperWidget {
423
689
  private readonly state: DeveloperState;
424
690
  private readonly theme: Theme;
@@ -450,10 +716,14 @@ export class DeveloperWidget {
450
716
  );
451
717
  }
452
718
  if (this.state.pendingQuestions.length > 3) {
453
- lines.push(this.theme.fg("dim", ` +${this.state.pendingQuestions.length - 3} more open questions`));
719
+ lines.push(
720
+ this.theme.fg("dim", ` +${this.state.pendingQuestions.length - 3} more open questions`),
721
+ );
454
722
  }
455
723
  if (this.state.implementationFramingRequired) {
456
- lines.push(this.theme.fg("warning", "→ next · sketch feature shape or signal structural movement"));
724
+ lines.push(
725
+ this.theme.fg("warning", "◇ gate · frame implementation before direct (sketch or signal)"),
726
+ );
457
727
  }
458
728
  if (this.state.rerouteRequired) {
459
729
  lines.push(this.theme.fg("warning", "→ next · reroute from the latest direct landing"));
@@ -527,8 +797,14 @@ export class DeveloperStatusPanel {
527
797
  const innerWidth = Math.max(1, panelWidth - 2);
528
798
  const rows: string[] = [];
529
799
  const border = (text: string) => this.theme.fg("borderAccent", text);
530
- const row = (content = "") => `${border("│")}${truncateToWidth(content, innerWidth, "…", true)}${border("│")}`;
531
- const addWrapped = (label: string, value: string, color: ThemeColor = "muted", maxLines = 2) => {
800
+ const row = (content = "") =>
801
+ `${border("│")}${truncateToWidth(content, innerWidth, "", true)}${border("│")}`;
802
+ const addWrapped = (
803
+ label: string,
804
+ value: string,
805
+ color: ThemeColor = "muted",
806
+ maxLines = 2,
807
+ ) => {
532
808
  const labelText = `${label} ·`;
533
809
  const plainPrefix = ` ${labelText} `;
534
810
  const styledPrefix = ` ${this.theme.fg("dim", labelText)} `;
@@ -538,13 +814,15 @@ export class DeveloperStatusPanel {
538
814
  if (wrapped.length > visible.length && visible.length > 0) {
539
815
  const last = visible.length - 1;
540
816
  visible[last] =
541
- truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + this.theme.fg("dim", "…");
817
+ truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") +
818
+ this.theme.fg("dim", "…");
542
819
  }
543
820
  rows.push(row(styledPrefix + (visible[0] ?? "")));
544
821
  const hangingIndent = " ".repeat(visibleWidth(plainPrefix));
545
822
  for (const line of visible.slice(1)) rows.push(row(hangingIndent + line));
546
823
  };
547
- const section = (title: string) => rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
824
+ const section = (title: string) =>
825
+ rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
548
826
 
549
827
  rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
550
828
  rows.push(row(` ${this.theme.fg("accent", this.theme.bold("◆ Developer status"))}`));
@@ -558,7 +836,8 @@ export class DeveloperStatusPanel {
558
836
  `protocol ${this.theme.fg(protocolColor(currentProtocol), currentProtocol)}` +
559
837
  this.theme.fg("dim", " · ") +
560
838
  `target ${this.theme.fg("muted", state.activeRoute?.owner ?? "none")}`;
561
- for (const line of wrapTextWithAnsi(summary, Math.max(1, innerWidth - 2))) rows.push(row(` ${line}`));
839
+ for (const line of wrapTextWithAnsi(summary, Math.max(1, innerWidth - 2)))
840
+ rows.push(row(` ${line}`));
562
841
  rows.push(row());
563
842
 
564
843
  section("Active route");
@@ -587,7 +866,12 @@ export class DeveloperStatusPanel {
587
866
  addWrapped("resolves when", question.resolutionCriteria, "dim", 2);
588
867
  }
589
868
  if (state.pendingQuestions.length > 4) {
590
- addWrapped("more", `${state.pendingQuestions.length - 4} additional open questions`, "dim", 1);
869
+ addWrapped(
870
+ "more",
871
+ `${state.pendingQuestions.length - 4} additional open questions`,
872
+ "dim",
873
+ 1,
874
+ );
591
875
  }
592
876
  }
593
877
 
@@ -598,7 +882,9 @@ export class DeveloperStatusPanel {
598
882
  } else {
599
883
  const recentJudgments = state.judgmentHistory.slice(-10).toReversed();
600
884
  for (const judgment of recentJudgments) {
601
- const route = state.routeHistory.find((candidate) => candidate.routeId === judgment.routeId);
885
+ const route = state.routeHistory.find(
886
+ (candidate) => candidate.routeId === judgment.routeId,
887
+ );
602
888
  addWrapped(
603
889
  `${judgment.owner} ${judgment.status}`,
604
890
  `${judgment.question} → ${judgmentSummary(judgment.result)}`,
@@ -610,11 +896,21 @@ export class DeveloperStatusPanel {
610
896
  addWrapped(`considered ${alternative.owner}`, alternative.reason, "dim", 2);
611
897
  }
612
898
  for (const update of judgment.questionUpdates ?? []) {
613
- addWrapped(`question ${update.status}`, `${update.questionId} → ${update.result}`, "accent", 2);
899
+ addWrapped(
900
+ `question ${update.status}`,
901
+ `${update.questionId} → ${update.result}`,
902
+ "accent",
903
+ 2,
904
+ );
614
905
  }
615
906
  }
616
907
  if (state.judgmentHistory.length > recentJudgments.length) {
617
- addWrapped("earlier", `${state.judgmentHistory.length - recentJudgments.length} earlier judgments`, "dim", 1);
908
+ addWrapped(
909
+ "earlier",
910
+ `${state.judgmentHistory.length - recentJudgments.length} earlier judgments`,
911
+ "dim",
912
+ 1,
913
+ );
618
914
  }
619
915
  }
620
916
 
@@ -656,7 +952,10 @@ export class DeveloperStatusPanel {
656
952
  }
657
953
  }
658
954
 
659
- export async function showDeveloperStatus(ctx: ExtensionCommandContext, view: DeveloperStatusView): Promise<void> {
955
+ export async function showDeveloperStatus(
956
+ ctx: ExtensionCommandContext,
957
+ view: DeveloperStatusView,
958
+ ): Promise<void> {
660
959
  let mouseTerminal: WritableTerminal | undefined;
661
960
  try {
662
961
  await ctx.ui.custom<void>(
@@ -685,10 +984,14 @@ export async function showDeveloperStatus(ctx: ExtensionCommandContext, view: De
685
984
 
686
985
  export function questionResolutionPrompt(question: PendingQuestion): string {
687
986
  const requestLabel = resolutionRequestLabel(question.resolutionOwner);
987
+ const context = question.context
988
+ ? ["", "Decision or evidence context:", question.context, ""]
989
+ : [];
688
990
  return [
689
991
  "Resolve this open Developer question.",
690
992
  "",
691
993
  `Question: ${question.question}`,
994
+ ...context,
692
995
  `Resolution owner: ${question.resolutionOwner}`,
693
996
  `Gate: ${question.gate}`,
694
997
  `Resolution criteria: ${question.resolutionCriteria}`,
@@ -704,6 +1007,7 @@ export function editQuestionResolutionRequest(
704
1007
  ctx: ExtensionCommandContext,
705
1008
  question: PendingQuestion,
706
1009
  ): Promise<string | undefined> {
1010
+ if (question.responseSpec) return collectChoiceResponse(ctx, question);
707
1011
  const current = ctx.ui.getEditorText();
708
1012
  const request = questionResolutionPrompt(question);
709
1013
  const initial = current.trim() ? `${current.trimEnd()}\n\n${request}` : request;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobin/developer",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Adaptive product-development reasoning and evidence for Pi.",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "pi", "developer", "skills"],