@longtable/cli 0.1.58 → 0.1.60

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.
@@ -57,12 +57,41 @@ function readCombinedOutput(payload) {
57
57
  safeString(payload.output)
58
58
  ].filter(Boolean).join("\n").trim();
59
59
  }
60
- function formatQuestionOptions(question) {
61
- const options = question.prompt.options.map((option) => option.value);
60
+ function questionUsesKorean(question) {
61
+ return /[가-힣]/.test([
62
+ question.prompt.title,
63
+ question.prompt.question,
64
+ question.prompt.displayReason ?? "",
65
+ ...question.prompt.options.flatMap((option) => [option.label, option.description ?? ""]),
66
+ question.prompt.otherLabel ?? ""
67
+ ].join("\n"));
68
+ }
69
+ function formatQuestionChoices(question) {
70
+ const korean = questionUsesKorean(question);
71
+ const options = question.prompt.options.flatMap((option, index) => {
72
+ const recommended = option.recommended ? (korean ? " (추천)" : " (recommended)") : "";
73
+ return [
74
+ `${index + 1}. ${option.label}${recommended}`,
75
+ ...(option.description ? [` ${option.description}`] : []),
76
+ ` ${korean ? "기록값" : "Record value"}: ${option.value}`
77
+ ];
78
+ });
62
79
  if (question.prompt.allowOther) {
63
- options.push("other");
80
+ options.push(`${question.prompt.options.length + 1}. ${question.prompt.otherLabel ?? (korean ? "직접 입력" : "Other")}`);
81
+ options.push(` ${korean ? "기록값" : "Record value"}: other`);
64
82
  }
65
- return options.join("/");
83
+ return options.join("\n");
84
+ }
85
+ function buildQuestionDecisionCard(question) {
86
+ const korean = questionUsesKorean(question);
87
+ return [
88
+ korean ? "LongTable 결정 카드" : "LongTable Decision Card",
89
+ `${korean ? "체크포인트" : "Checkpoint"}: ${question.prompt.title}`,
90
+ `${korean ? "무엇이 걸렸나" : "What is blocked"}: ${question.prompt.displayReason ?? question.prompt.rationale[0] ?? question.prompt.title}`,
91
+ `${korean ? "지금 결정할 것" : "Decision needed"}: ${question.prompt.question}`,
92
+ korean ? "선택지:" : "Choices:",
93
+ formatQuestionChoices(question)
94
+ ].join("\n");
66
95
  }
67
96
  function pendingRequiredQuestions(state) {
68
97
  return (state.questionLog ?? []).filter((question) => question.status === "pending" && question.prompt.required);
@@ -271,8 +300,8 @@ function buildWorkspaceSummary(runtime, detail = "compact") {
271
300
  }
272
301
  function buildPendingQuestionContext(question) {
273
302
  return [
274
- `Required Researcher Checkpoint is still pending: ${question.prompt.question}`,
275
- `Options: ${formatQuestionOptions(question)}`,
303
+ "Required Researcher Checkpoint is still pending.",
304
+ buildQuestionDecisionCard(question),
276
305
  `Record it with longtable decide --question ${question.id} --answer <value> if you are outside MCP elicitation.`,
277
306
  "Do not choose or record an answer unless the researcher explicitly provides the selection."
278
307
  ].join("\n");
@@ -291,8 +320,7 @@ function buildGeneratedQuestionsContext(questions, created) {
291
320
  : `LongTable found ${questions.length} pending Researcher Checkpoint${questions.length === 1 ? "" : "s"} for this prompt.`
292
321
  ];
293
322
  for (const question of questions) {
294
- lines.push(`- ${question.prompt.title}: ${question.prompt.question}`);
295
- lines.push(` Options: ${formatQuestionOptions(question)}`);
323
+ lines.push(buildQuestionDecisionCard(question));
296
324
  lines.push(` Record it with longtable decide --question ${question.id} --answer <value> if you are outside MCP elicitation.`);
297
325
  }
298
326
  lines.push("Do not choose or record answers for these checkpoints unless the researcher explicitly provides the selections.");
package/dist/panel.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { CheckpointSensitivity, InteractionMode, InvocationIntent, InvocationRecord, PanelPlan, PanelResult, PanelVisibility, QuestionRecord, ProviderKind, InvocationSurface, RoleKey } from "@longtable/core";
1
+ import type { CheckpointSensitivity, InteractionMode, InvocationIntent, InvocationRecord, PanelPlan, PanelResult, PanelVisibility, QuestionOption, QuestionRecord, ProviderKind, InvocationSurface, RoleKey } from "@longtable/core";
2
2
  import { type CanonicalPersona } from "./personas.js";
3
+ import { type OutputLanguage } from "./persona-router.js";
3
4
  export interface BuildPanelPlanOptions {
4
5
  prompt: string;
5
6
  mode?: InteractionMode;
@@ -18,6 +19,16 @@ export interface PanelFallback {
18
19
  questionRecord: QuestionRecord;
19
20
  prompt: string;
20
21
  }
22
+ interface PanelDecisionContext {
23
+ language: OutputLanguage;
24
+ focus: string;
25
+ blockerSummary: string;
26
+ decisionQuestion: string;
27
+ displayReason: string;
28
+ options: QuestionOption[];
29
+ otherLabel: string;
30
+ }
31
+ export declare function buildPanelDecisionContext(prompt: string): PanelDecisionContext;
21
32
  export declare function buildPanelPlan(options: BuildPanelPlanOptions): PanelPlan;
22
33
  export declare function buildInvocationIntent(options: {
23
34
  prompt: string;
@@ -41,3 +52,4 @@ export declare function renderSequentialFallbackPrompt(plan: PanelPlan): string;
41
52
  export declare function buildPanelFallback(options: BuildPanelPlanOptions): PanelFallback;
42
53
  export declare function renderPanelSummary(plan: PanelPlan): string;
43
54
  export declare function listDefaultPanelRoles(): CanonicalPersona[];
55
+ export {};
package/dist/panel.js CHANGED
@@ -61,6 +61,336 @@ function memberForRole(role, explicitRoles, routedRoles) {
61
61
  required: DEFAULT_PANEL_ROLES.includes(role) || explicitRoles.includes(role)
62
62
  };
63
63
  }
64
+ function firstPromptClause(prompt) {
65
+ const projectContextMatch = prompt.match(/(?:^|\n)LongTable project context\n[\s\S]*?\n\n([\s\S]+)$/);
66
+ const sourcePrompt = projectContextMatch?.[1] ?? prompt;
67
+ const normalized = sourcePrompt.replace(/\s+/g, " ").trim();
68
+ const sentenceEnd = normalized.search(/[.!?]\s/);
69
+ const first = sentenceEnd >= 0 ? normalized.slice(0, sentenceEnd + 1) : normalized;
70
+ return first
71
+ .replace(/^(please\s+)?(run\s+)?(a\s+)?(structured\s+)?(panel\s+)?(review|critique|evaluate|assess|inspect)\s+/i, "")
72
+ .replace(/^(whether|if|of)\s+/i, "")
73
+ .replace(/^the\s+/i, "")
74
+ .trim();
75
+ }
76
+ function conciseFocusFromPrompt(prompt) {
77
+ const focus = firstPromptClause(prompt)
78
+ .replace(/[.:;,\s]+$/g, "")
79
+ .trim();
80
+ if (!focus) {
81
+ return "the reviewed issue";
82
+ }
83
+ return focus.length > 96 ? `${focus.slice(0, 93).trim()}...` : focus;
84
+ }
85
+ function languageText(language, en, ko) {
86
+ return language === "ko" ? ko : en;
87
+ }
88
+ function withFocus(template, focus) {
89
+ return template.replace(/\{focus\}/g, focus);
90
+ }
91
+ function shouldIncludeDeferOption(prompt) {
92
+ return /\b(defer|postpone|not decide|keep open|open tension|later|uncertain|unresolved)\b/i.test(prompt)
93
+ || /보류|미루|나중|열린\s*쟁점|열어\s*두|불확실|미해결/.test(prompt);
94
+ }
95
+ const PANEL_DECISION_COPY = {
96
+ manuscript_spine: {
97
+ focus: {
98
+ en: "the manuscript table/figure spine",
99
+ ko: "원고의 표/그림 spine"
100
+ },
101
+ blockerSummary: {
102
+ en: "The panel concern is that the current manuscript shell does not yet carry its argument through a useful table/figure spine.",
103
+ ko: "패널이 멈춘 이유는 현재 원고 shell이 논문 주장을 운반할 표/그림 spine을 충분히 보여주지 못했기 때문입니다."
104
+ },
105
+ recommendedValue: "revise",
106
+ labels: {
107
+ revise: { en: "Revise manuscript table/figure spine", ko: "원고 표/그림 spine을 다시 설계한다" },
108
+ evidence: { en: "Verify table/figure evidence", ko: "표/그림 근거를 먼저 확인한다" },
109
+ proceed: { en: "Proceed with current manuscript direction", ko: "현재 원고 방향을 유지하고 표/그림만 보강한다" },
110
+ defer: { en: "Keep manuscript spine concern open", ko: "원고 spine 우려를 열린 쟁점으로 둔다" }
111
+ },
112
+ descriptions: {
113
+ revise: {
114
+ en: "Use the panel result to redesign the table/figure spine before drafting the manuscript.",
115
+ ko: "패널 결과를 반영해 원고를 쓰기 전에 표/그림 spine을 다시 잡습니다."
116
+ },
117
+ evidence: {
118
+ en: "Check which tables, figures, analyses, and citations can actually support the manuscript claim.",
119
+ ko: "어떤 표, 그림, 분석, 인용이 실제로 원고 주장을 지탱할 수 있는지 먼저 확인합니다."
120
+ },
121
+ proceed: {
122
+ en: "Keep the current manuscript direction and only strengthen the missing table/figure pieces.",
123
+ ko: "현재 원고 방향은 유지하고 부족한 표/그림 요소만 보강합니다."
124
+ },
125
+ defer: {
126
+ en: "Do not settle the manuscript spine yet; keep the concern visible as an open issue.",
127
+ ko: "아직 원고 spine을 확정하지 않고 이 우려를 열린 쟁점으로 남깁니다."
128
+ }
129
+ }
130
+ },
131
+ manuscript_argument: {
132
+ focus: {
133
+ en: "the manuscript or draft argument",
134
+ ko: "원고 또는 초안의 논지"
135
+ },
136
+ blockerSummary: {
137
+ en: "The panel concern is that the manuscript direction could settle before the argument, evidence, and reviewer risk are aligned.",
138
+ ko: "패널이 멈춘 이유는 논지, 근거, 리뷰어 리스크가 정렬되기 전에 원고 방향이 확정될 수 있기 때문입니다."
139
+ },
140
+ recommendedValue: "revise",
141
+ labels: {
142
+ revise: { en: "Revise manuscript argument", ko: "원고 논지를 먼저 수정한다" },
143
+ evidence: { en: "Verify manuscript evidence", ko: "원고 근거를 먼저 확인한다" },
144
+ proceed: { en: "Proceed with current manuscript direction", ko: "현재 원고 방향으로 진행한다" },
145
+ defer: { en: "Keep manuscript concern open", ko: "원고 우려를 열린 쟁점으로 둔다" }
146
+ },
147
+ descriptions: {
148
+ revise: {
149
+ en: "Use the panel result to revise the claim, structure, or draft before proceeding.",
150
+ ko: "패널 결과를 반영해 주장, 구조, 초안을 먼저 수정합니다."
151
+ },
152
+ evidence: {
153
+ en: "Check source, data, artifact, or citation support for the manuscript direction before proceeding.",
154
+ ko: "현재 원고 방향을 지탱할 자료, 데이터, 산출물, 인용 근거를 먼저 확인합니다."
155
+ },
156
+ proceed: {
157
+ en: "Accept the visible risk profile and continue with the current manuscript direction.",
158
+ ko: "드러난 위험을 감수하고 현재 원고 방향으로 계속 진행합니다."
159
+ },
160
+ defer: {
161
+ en: "Do not commit yet; keep the manuscript issue visible as an open tension.",
162
+ ko: "아직 확정하지 않고 원고 쟁점을 열린 긴장으로 남깁니다."
163
+ }
164
+ }
165
+ },
166
+ measurement: {
167
+ focus: {
168
+ en: "the measurement or coding decision",
169
+ ko: "측정 또는 코딩 결정"
170
+ },
171
+ blockerSummary: {
172
+ en: "The panel concern is that measurement or coding rules decide what will count as evidence later.",
173
+ ko: "패널이 멈춘 이유는 측정 또는 코딩 규칙이 이후 무엇을 근거로 볼지 결정하기 때문입니다."
174
+ },
175
+ recommendedValue: "revise",
176
+ labels: {
177
+ revise: { en: "Revise measurement/coding plan", ko: "측정/코딩 계획을 수정한다" },
178
+ evidence: { en: "Verify measurement validity evidence", ko: "측정 타당도 근거를 확인한다" },
179
+ proceed: { en: "Proceed with current measurement/coding plan", ko: "현재 측정/코딩 계획으로 진행한다" },
180
+ defer: { en: "Keep measurement/coding concern open", ko: "측정/코딩 우려를 열린 쟁점으로 둔다" }
181
+ },
182
+ descriptions: {
183
+ revise: {
184
+ en: "Use the panel result to revise the variables, coding rules, or construct boundary before proceeding.",
185
+ ko: "패널 결과를 반영해 변수, 코딩 규칙, 구성개념 경계를 먼저 수정합니다."
186
+ },
187
+ evidence: {
188
+ en: "Check validity, reliability, source, or coding evidence before treating the plan as settled.",
189
+ ko: "타당도, 신뢰도, 출처, 코딩 근거를 확인한 뒤 계획을 확정합니다."
190
+ },
191
+ proceed: {
192
+ en: "Accept the visible measurement risk and continue with the current plan.",
193
+ ko: "드러난 측정 위험을 감수하고 현재 계획으로 계속 진행합니다."
194
+ },
195
+ defer: {
196
+ en: "Do not commit yet; keep the measurement or coding issue visible as an open tension.",
197
+ ko: "아직 확정하지 않고 측정 또는 코딩 쟁점을 열린 긴장으로 남깁니다."
198
+ }
199
+ }
200
+ },
201
+ method: {
202
+ focus: {
203
+ en: "the method or analysis design",
204
+ ko: "방법 또는 분석 설계"
205
+ },
206
+ blockerSummary: {
207
+ en: "The panel concern is that the method or analysis choice could become a hard-to-reverse research commitment.",
208
+ ko: "패널이 멈춘 이유는 방법 또는 분석 선택이 되돌리기 어려운 연구 결정이 될 수 있기 때문입니다."
209
+ },
210
+ recommendedValue: "revise",
211
+ labels: {
212
+ revise: { en: "Revise method/analysis plan", ko: "방법/분석 계획을 수정한다" },
213
+ evidence: { en: "Verify method/analysis evidence", ko: "방법/분석 근거를 확인한다" },
214
+ proceed: { en: "Proceed with current method/analysis plan", ko: "현재 방법/분석 계획으로 진행한다" },
215
+ defer: { en: "Keep method/analysis concern open", ko: "방법/분석 우려를 열린 쟁점으로 둔다" }
216
+ },
217
+ descriptions: {
218
+ revise: {
219
+ en: "Use the panel result to revise the design, model, sample, or analysis plan before proceeding.",
220
+ ko: "패널 결과를 반영해 설계, 모형, 표본, 분석 계획을 먼저 수정합니다."
221
+ },
222
+ evidence: {
223
+ en: "Check data, model, method, or artifact support before treating the plan as settled.",
224
+ ko: "데이터, 모형, 방법, 산출물 근거를 확인한 뒤 계획을 확정합니다."
225
+ },
226
+ proceed: {
227
+ en: "Accept the visible method risk and continue with the current plan.",
228
+ ko: "드러난 방법론적 위험을 감수하고 현재 계획으로 계속 진행합니다."
229
+ },
230
+ defer: {
231
+ en: "Do not commit yet; keep the method or analysis issue visible as an open tension.",
232
+ ko: "아직 확정하지 않고 방법 또는 분석 쟁점을 열린 긴장으로 남깁니다."
233
+ }
234
+ }
235
+ },
236
+ evidence: {
237
+ focus: {
238
+ en: "the evidence or source standard",
239
+ ko: "근거 또는 출처 기준"
240
+ },
241
+ blockerSummary: {
242
+ en: "The panel concern is that LongTable could proceed before the source, artifact, or citation support is explicit.",
243
+ ko: "패널이 멈춘 이유는 출처, 산출물, 인용 근거가 명시되기 전에 LongTable이 진행할 수 있기 때문입니다."
244
+ },
245
+ recommendedValue: "evidence",
246
+ labels: {
247
+ revise: { en: "Revise evidence standard", ko: "근거 기준을 수정한다" },
248
+ evidence: { en: "Verify source/citation support", ko: "출처/인용 근거를 확인한다" },
249
+ proceed: { en: "Proceed with current evidence boundary", ko: "현재 근거 경계로 진행한다" },
250
+ defer: { en: "Keep evidence concern open", ko: "근거 우려를 열린 쟁점으로 둔다" }
251
+ },
252
+ descriptions: {
253
+ revise: {
254
+ en: "Use the panel result to revise what will count as adequate evidence.",
255
+ ko: "패널 결과를 반영해 충분한 근거의 기준을 먼저 수정합니다."
256
+ },
257
+ evidence: {
258
+ en: "Check source, data, artifact, or citation support before proceeding.",
259
+ ko: "진행하기 전에 출처, 데이터, 산출물, 인용 근거를 확인합니다."
260
+ },
261
+ proceed: {
262
+ en: "Accept the visible evidence risk and continue with the current boundary.",
263
+ ko: "드러난 근거 위험을 감수하고 현재 경계로 계속 진행합니다."
264
+ },
265
+ defer: {
266
+ en: "Do not commit yet; keep the evidence issue visible as an open tension.",
267
+ ko: "아직 확정하지 않고 근거 쟁점을 열린 긴장으로 남깁니다."
268
+ }
269
+ }
270
+ },
271
+ theory: {
272
+ focus: {
273
+ en: "the theory or conceptual frame",
274
+ ko: "이론 또는 개념 프레임"
275
+ },
276
+ blockerSummary: {
277
+ en: "The panel concern is that the conceptual frame could settle before its distinctions and limits are explicit.",
278
+ ko: "패널이 멈춘 이유는 개념 구분과 한계가 명확해지기 전에 이론 프레임이 확정될 수 있기 때문입니다."
279
+ },
280
+ recommendedValue: "revise",
281
+ labels: {
282
+ revise: { en: "Revise theory/conceptual frame", ko: "이론/개념 프레임을 수정한다" },
283
+ evidence: { en: "Verify theory support", ko: "이론 근거를 확인한다" },
284
+ proceed: { en: "Proceed with current theory frame", ko: "현재 이론 프레임으로 진행한다" },
285
+ defer: { en: "Keep theory concern open", ko: "이론 우려를 열린 쟁점으로 둔다" }
286
+ },
287
+ descriptions: {
288
+ revise: {
289
+ en: "Use the panel result to revise the conceptual frame, distinctions, or scope before proceeding.",
290
+ ko: "패널 결과를 반영해 개념 프레임, 구분, 범위를 먼저 수정합니다."
291
+ },
292
+ evidence: {
293
+ en: "Check theory, literature, concept, or citation support before treating the frame as settled.",
294
+ ko: "이론, 문헌, 개념, 인용 근거를 확인한 뒤 프레임을 확정합니다."
295
+ },
296
+ proceed: {
297
+ en: "Accept the visible theory risk and continue with the current frame.",
298
+ ko: "드러난 이론적 위험을 감수하고 현재 프레임으로 계속 진행합니다."
299
+ },
300
+ defer: {
301
+ en: "Do not commit yet; keep the theory issue visible as an open tension.",
302
+ ko: "아직 확정하지 않고 이론 쟁점을 열린 긴장으로 남깁니다."
303
+ }
304
+ }
305
+ },
306
+ generic: {
307
+ focus: {
308
+ en: "the reviewed issue",
309
+ ko: "검토된 쟁점"
310
+ },
311
+ blockerSummary: {
312
+ en: "The panel concern is that LongTable has reached a decision point that should belong to the researcher.",
313
+ ko: "패널이 멈춘 이유는 LongTable이 연구자가 직접 정해야 할 결정 지점에 도달했기 때문입니다."
314
+ },
315
+ recommendedValue: "revise",
316
+ labels: {
317
+ revise: { en: "Revise reviewed issue", ko: "검토된 쟁점을 수정한다" },
318
+ evidence: { en: "Verify evidence first", ko: "근거를 먼저 확인한다" },
319
+ proceed: { en: "Proceed with current direction", ko: "현재 방향으로 진행한다" },
320
+ defer: { en: "Keep issue open", ko: "쟁점을 열린 상태로 둔다" }
321
+ },
322
+ descriptions: {
323
+ revise: {
324
+ en: "Use the panel result to revise {focus} before proceeding.",
325
+ ko: "패널 결과를 반영해 {focus}을/를 먼저 수정합니다."
326
+ },
327
+ evidence: {
328
+ en: "Check source, data, artifact, or citation support for {focus} before proceeding.",
329
+ ko: "{focus}을/를 지탱할 출처, 데이터, 산출물, 인용 근거를 먼저 확인합니다."
330
+ },
331
+ proceed: {
332
+ en: "Accept the visible risk profile for {focus} and continue.",
333
+ ko: "{focus}에 대해 드러난 위험을 감수하고 계속 진행합니다."
334
+ },
335
+ defer: {
336
+ en: "Do not commit yet; keep {focus} visible as an open tension.",
337
+ ko: "아직 확정하지 않고 {focus}을/를 열린 긴장으로 남깁니다."
338
+ }
339
+ }
340
+ }
341
+ };
342
+ function decisionDomainFromPrompt(prompt) {
343
+ const normalized = prompt.toLowerCase();
344
+ if (/get[-_ ]?journal|table\/figure|table and figure|figure spine|table spine|manuscript shell|journal manuscript/.test(normalized)) {
345
+ return "manuscript_spine";
346
+ }
347
+ if (/\bmanuscript\b|\bdraft\b|\bpaper\b|\barticle\b|\bsection\b|\bsubmission\b/.test(normalized)) {
348
+ return "manuscript_argument";
349
+ }
350
+ if (/\bmeasurement\b|\bmeasure\b|\bscale\b|\bconstruct\b|\bcoding\b|측정|척도|구성개념|코딩/.test(normalized)) {
351
+ return "measurement";
352
+ }
353
+ if (/\bmethod\b|\bdesign\b|\banalysis\b|\bmodel\b|\bsample\b|\bidentification\b|방법|설계|분석|표본/.test(normalized)) {
354
+ return "method";
355
+ }
356
+ if (/\bevidence\b|\bsource\b|\bcitation\b|\breference\b|\bpdf\b|\bcorpus\b|근거|인용|문헌|자료/.test(normalized)) {
357
+ return "evidence";
358
+ }
359
+ if (/\btheory\b|\bframework\b|\bontology\b|\bconcept\b|이론|개념|온톨로지/.test(normalized)) {
360
+ return "theory";
361
+ }
362
+ return "generic";
363
+ }
364
+ function panelDecisionOptions(copy, language, focus, includeDefer) {
365
+ const values = includeDefer
366
+ ? ["revise", "evidence", "defer"]
367
+ : ["revise", "evidence", "proceed"];
368
+ return values
369
+ .sort((left, right) => Number(right === copy.recommendedValue) - Number(left === copy.recommendedValue))
370
+ .map((value) => ({
371
+ value,
372
+ label: copy.labels[value][language],
373
+ description: withFocus(copy.descriptions[value][language], focus),
374
+ ...(value === copy.recommendedValue ? { recommended: true } : {})
375
+ }));
376
+ }
377
+ export function buildPanelDecisionContext(prompt) {
378
+ const language = detectOutputLanguage(prompt);
379
+ const domain = decisionDomainFromPrompt(prompt);
380
+ const copy = PANEL_DECISION_COPY[domain];
381
+ const domainFocus = copy.focus[language];
382
+ const focus = domain === "generic" ? conciseFocusFromPrompt(prompt) || domainFocus : domainFocus;
383
+ const blockerSummary = copy.blockerSummary[language];
384
+ return {
385
+ language,
386
+ focus,
387
+ blockerSummary,
388
+ decisionQuestion: languageText(language, `What should LongTable treat as the next human decision for ${focus} after this panel review?`, `이 패널 리뷰 이후 LongTable이 ${focus}에 대해 다음 인간 결정으로 기록해야 할 것은 무엇인가요?`),
389
+ displayReason: languageText(language, `${blockerSummary} This should be resolved by the researcher before LongTable treats the direction as settled.`, `${blockerSummary} LongTable이 방향을 확정하기 전에 연구자가 직접 선택해야 합니다.`),
390
+ options: panelDecisionOptions(copy, language, focus, shouldIncludeDeferOption(prompt)),
391
+ otherLabel: languageText(language, "Other decision", "직접 입력")
392
+ };
393
+ }
64
394
  export function buildPanelPlan(options) {
65
395
  const mode = options.mode ?? "review";
66
396
  const explicitRoles = unique([...(options.roles ?? []), ...parseRoleFlag(options.roleFlag)]);
@@ -112,6 +442,7 @@ export function buildInvocationIntent(options) {
112
442
  }
113
443
  export function createPlannedPanelQuestionRecord(plan, provider) {
114
444
  const createdAt = nowIso();
445
+ const decisionContext = buildPanelDecisionContext(plan.prompt);
115
446
  return {
116
447
  id: createId("question_record"),
117
448
  createdAt,
@@ -120,43 +451,24 @@ export function createPlannedPanelQuestionRecord(plan, provider) {
120
451
  prompt: {
121
452
  id: createId("question_prompt"),
122
453
  checkpointKey: "panel_next_decision",
123
- title: "Panel follow-up decision",
124
- question: "What should LongTable treat as the next human decision after this panel review?",
454
+ title: languageText(decisionContext.language, "Panel follow-up decision", "패널 후속 결정"),
455
+ question: decisionContext.decisionQuestion,
125
456
  type: "single_choice",
126
- options: [
127
- {
128
- value: "revise",
129
- label: "Revise before proceeding",
130
- description: "Use the panel result to revise the claim, design, or draft first."
131
- },
132
- {
133
- value: "evidence",
134
- label: "Gather or verify evidence first",
135
- description: "Do not proceed until the relevant evidence or citation support is checked."
136
- },
137
- {
138
- value: "proceed",
139
- label: "Proceed with current direction",
140
- description: "Accept the risk profile and continue with the current direction."
141
- },
142
- {
143
- value: "defer",
144
- label: "Keep this open",
145
- description: "Do not commit yet; keep the panel issue visible as an open tension."
146
- }
147
- ],
457
+ options: decisionContext.options,
148
458
  allowOther: true,
149
- otherLabel: "Other decision",
459
+ otherLabel: decisionContext.otherLabel,
150
460
  required: plan.checkpointSensitivity === "high",
151
461
  source: "runtime_guidance",
152
462
  rationale: [
153
463
  "Panel review creates disagreement or risk visibility that should connect to an explicit researcher decision.",
464
+ `Panel decision focus: ${decisionContext.focus}.`,
465
+ "Panel follow-up choices are compact by default; unlisted decisions should use Other.",
154
466
  `Panel checkpoint sensitivity: ${plan.checkpointSensitivity}.`
155
467
  ],
156
- displayReason: "Panel review can surface role disagreement that should not be collapsed without researcher approval.",
468
+ displayReason: decisionContext.displayReason,
157
469
  preferredSurfaces: provider === "claude"
158
470
  ? ["native_structured", "numbered"]
159
- : ["mcp_elicitation", "numbered"]
471
+ : ["tmux_popup", "mcp_elicitation", "numbered"]
160
472
  }
161
473
  };
162
474
  }
@@ -497,6 +497,14 @@ function formatQuestionMetadata(record) {
497
497
  ].filter(Boolean);
498
498
  return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
499
499
  }
500
+ const QUESTION_SURFACES = new Set([
501
+ "native_structured",
502
+ "tmux_popup",
503
+ "mcp_elicitation",
504
+ "numbered",
505
+ "terminal_selector",
506
+ "web_form"
507
+ ]);
500
508
  function compactLine(value, limit = 160) {
501
509
  const compacted = value.replace(/\s+/g, " ").trim();
502
510
  return compacted.length > limit ? `${compacted.slice(0, limit - 1)}…` : compacted;
@@ -506,6 +514,15 @@ function asRecord(value) {
506
514
  ? value
507
515
  : null;
508
516
  }
517
+ function asStringArray(value) {
518
+ if (Array.isArray(value)) {
519
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
520
+ }
521
+ return typeof value === "string" && value.trim().length > 0 ? [value] : [];
522
+ }
523
+ function isQuestionSurfaceValue(value) {
524
+ return typeof value === "string" && QUESTION_SURFACES.has(value);
525
+ }
509
526
  const SPEC_DIFF_IGNORED_PATHS = new Set([
510
527
  "createdAt",
511
528
  "updatedAt",
@@ -602,10 +619,10 @@ function buildResearchSpecificationGapQuestion(gaps, timestamp, sourceEvidenceId
602
619
  "Research Specification is the required durable interview artifact.",
603
620
  "Missing required sections can make later resume, screening, coding, or evidence decisions stale."
604
621
  ],
605
- preferredSurfaces: ["mcp_elicitation", "numbered"]
622
+ preferredSurfaces: ["tmux_popup", "mcp_elicitation", "numbered"]
606
623
  },
607
624
  transportStatus: {
608
- surface: "mcp_elicitation",
625
+ surface: "tmux_popup",
609
626
  status: "not_attempted",
610
627
  updatedAt: timestamp,
611
628
  ...(sourceEvidenceIds.length > 0 ? { message: `Source evidence: ${sourceEvidenceIds.join(", ")}` } : {})
@@ -2682,7 +2699,7 @@ export async function createWorkspaceFollowUpQuestions(options) {
2682
2699
  const createdAt = nowIso();
2683
2700
  const preferredSurfaces = options.provider === "claude"
2684
2701
  ? ["native_structured", "terminal_selector", "numbered"]
2685
- : ["mcp_elicitation", "terminal_selector", "numbered"];
2702
+ : ["tmux_popup", "mcp_elicitation", "terminal_selector", "numbered"];
2686
2703
  const specs = buildQuestionOpportunitySpecs(options.prompt, {
2687
2704
  includeFallback: options.force === true ? true : options.auto !== true,
2688
2705
  autoOnly: options.auto === true,
@@ -2795,7 +2812,7 @@ export async function createWorkspaceQuestion(options) {
2795
2812
  rationale,
2796
2813
  preferredSurfaces: options.provider === "claude"
2797
2814
  ? ["native_structured", "numbered"]
2798
- : ["mcp_elicitation", "numbered"]
2815
+ : ["tmux_popup", "mcp_elicitation", "numbered"]
2799
2816
  }
2800
2817
  };
2801
2818
  const updated = appendQuestionRecords(state, [question]);
@@ -2998,6 +3015,15 @@ function normalizeQuestionAnswerSelection(question, rawAnswer) {
2998
3015
  ...(inlineRationale ? { inlineRationale } : {})
2999
3016
  };
3000
3017
  }
3018
+ function selectedLabelsForValues(question, selectedValues) {
3019
+ return selectedValues.map((value) => {
3020
+ if (value === "other") {
3021
+ return question.prompt.otherLabel ?? "Other";
3022
+ }
3023
+ const option = question.prompt.options.find((candidate) => candidate.value === value);
3024
+ return option?.label ?? value;
3025
+ });
3026
+ }
3001
3027
  export async function answerWorkspaceQuestion(options) {
3002
3028
  const state = await loadResearchState(options.context.stateFilePath);
3003
3029
  const question = findQuestionForDecision(state, options.questionId);
@@ -3036,7 +3062,12 @@ export async function answerWorkspaceQuestion(options) {
3036
3062
  updatedAt: timestamp,
3037
3063
  status: "answered",
3038
3064
  answer,
3039
- decisionRecordId: decision.id
3065
+ decisionRecordId: decision.id,
3066
+ transportStatus: {
3067
+ surface: answer.surface,
3068
+ status: "accepted",
3069
+ updatedAt: timestamp
3070
+ }
3040
3071
  };
3041
3072
  const withQuestion = {
3042
3073
  ...state,
@@ -3242,6 +3273,58 @@ export async function repairWorkspaceStateConsistency(options) {
3242
3273
  })
3243
3274
  };
3244
3275
  }
3276
+ const repairTimestamp = nowIso();
3277
+ const repairedQuestionLog = (updated.questionLog ?? []).map((record) => {
3278
+ const answerRecord = asRecord(record.answer);
3279
+ if (!answerRecord) {
3280
+ return record;
3281
+ }
3282
+ const selectedValues = [
3283
+ ...asStringArray(answerRecord.selectedValues),
3284
+ ...asStringArray(answerRecord.selectedValue),
3285
+ ...asStringArray(answerRecord.selectedOptions),
3286
+ ...asStringArray(answerRecord.value)
3287
+ ];
3288
+ if (selectedValues.length === 0) {
3289
+ return record;
3290
+ }
3291
+ const selectedLabels = asStringArray(answerRecord.selectedLabels);
3292
+ const needsRepair = typeof answerRecord.promptId !== "string" ||
3293
+ selectedLabels.length === 0 ||
3294
+ !isQuestionSurfaceValue(answerRecord.surface);
3295
+ if (!needsRepair) {
3296
+ return record;
3297
+ }
3298
+ repaired.push(`normalized legacy answer shape for question ${record.id}`);
3299
+ const normalizedAnswer = {
3300
+ promptId: typeof answerRecord.promptId === "string" ? answerRecord.promptId : record.prompt.id,
3301
+ selectedValues: uniqueStrings(selectedValues),
3302
+ selectedLabels: selectedLabels.length > 0
3303
+ ? selectedLabels
3304
+ : selectedLabelsForValues(record, uniqueStrings(selectedValues)),
3305
+ ...(typeof answerRecord.otherText === "string" && answerRecord.otherText.trim()
3306
+ ? { otherText: answerRecord.otherText }
3307
+ : {}),
3308
+ ...(typeof answerRecord.rationale === "string" && answerRecord.rationale.trim()
3309
+ ? { rationale: answerRecord.rationale }
3310
+ : {}),
3311
+ ...(answerRecord.provider === "codex" || answerRecord.provider === "claude"
3312
+ ? { provider: answerRecord.provider }
3313
+ : {}),
3314
+ surface: isQuestionSurfaceValue(answerRecord.surface) ? answerRecord.surface : "numbered"
3315
+ };
3316
+ return {
3317
+ ...record,
3318
+ updatedAt: repairTimestamp,
3319
+ answer: normalizedAnswer
3320
+ };
3321
+ });
3322
+ if (repairedQuestionLog.some((record, index) => record !== (updated.questionLog ?? [])[index])) {
3323
+ updated = {
3324
+ ...updated,
3325
+ questionLog: repairedQuestionLog
3326
+ };
3327
+ }
3245
3328
  if (repaired.length > 0) {
3246
3329
  await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
3247
3330
  await syncCurrentWorkspaceView(options.context);
@@ -1,6 +1 @@
1
- export * from "./types.js";
2
- export * from "./query.js";
3
- export * from "./sources.js";
4
- export * from "./rank.js";
5
- export * from "./run.js";
6
- export * from "./publisher-access.js";
1
+ export * from "@longtable/scholar-research";
@@ -1,6 +1 @@
1
- export * from "./types.js";
2
- export * from "./query.js";
3
- export * from "./sources.js";
4
- export * from "./rank.js";
5
- export * from "./run.js";
6
- export * from "./publisher-access.js";
1
+ export * from "@longtable/scholar-research";