@longtable/cli 0.1.24 → 0.1.26

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/README.md CHANGED
@@ -20,8 +20,7 @@ npm install -g @longtable/cli
20
20
  ```
21
21
 
22
22
  The npm install only installs the CLI. It does not write Codex skills, MCP
23
- config, hooks, tmux state, or provider runtime files without explicit setup
24
- approval.
23
+ config, hooks, or provider runtime files without explicit setup approval.
25
24
 
26
25
  ## Primary Flow
27
26
 
@@ -91,8 +90,7 @@ longtable roles
91
90
  longtable ask --cwd "<project-path>" --prompt "..."
92
91
  longtable panel --prompt "..."
93
92
  longtable sentinel --prompt "Should I define a new measurement construct?"
94
- longtable hud --watch
95
- longtable team --tmux --prompt "Review this measurement plan."
93
+ longtable team --prompt "Review this measurement plan." --role editor,measurement_auditor --json
96
94
  longtable team --debate --prompt "Review this measurement plan." --role editor,measurement_auditor --json
97
95
  longtable codex install-skills
98
96
  longtable claude install-skills
@@ -110,6 +108,16 @@ longtable ask --prompt "lt panel: show the disagreement before I commit" --json
110
108
 
111
109
  Natural language should be the default.
112
110
 
111
+ Codex UI Researcher Checkpoints are a core LongTable feature when enabled:
112
+
113
+ ```bash
114
+ longtable setup --provider codex --surfaces skills_mcp --checkpoint-ui strong
115
+ ```
116
+
117
+ That setup writes the MCP configuration and Codex elicitation approval needed
118
+ for form-style checkpoint prompts. Without it, LongTable keeps the same
119
+ `QuestionRecord` pending and falls back to numbered text.
120
+
113
121
  Explicit short forms are available when needed:
114
122
 
115
123
  ```text
@@ -151,10 +159,6 @@ Inside a LongTable project workspace, panel planning also appends an
151
159
  `InvocationRecord` to `.longtable/state.json`, creates a pending follow-up
152
160
  `QuestionRecord`, and refreshes `CURRENT.md`.
153
161
 
154
- ```bash
155
- longtable decide --answer evidence --rationale "Need citation support before continuing."
156
- ```
157
-
158
162
  Default panel roles include:
159
163
 
160
164
  - `reviewer`
@@ -164,26 +168,25 @@ Default panel roles include:
164
168
 
165
169
  Use `--role` to constrain the panel when the research problem is already clear.
166
170
 
167
- ## Sentinel, HUD, And Tmux Team
171
+ ## Sentinel And Agent Team
168
172
 
169
173
  `longtable sentinel` is an explicit gap/tacit check for prompts that may contain
170
174
  measurement, theory, method, evidence, authorship, or tacit-assumption risks.
171
175
  Use `--record` inside a LongTable workspace to store the finding as an
172
176
  unconfirmed inferred hypothesis.
173
177
 
174
- `longtable hud --watch` renders a compact view of the current project goal,
175
- blocker, pending checkpoints, recent decisions, and invocation counts.
176
- `longtable hud --tmux` opens that view in a tmux pane.
177
-
178
- `longtable team --tmux` opens role-specific panes for research discussion and
179
- writes logs under `.longtable/team/<id>/`. This is panel discussion, not merely
180
- parallel execution: role panes are prompted to state claims, objections, open
181
- questions, and likely disagreement.
178
+ `longtable team` creates a file-backed agent-team review under
179
+ `.longtable/team/<id>/`: independent review, cross-review, and
180
+ synthesis/checkpoint. Use it when roles should inspect each other's concerns
181
+ before LongTable proposes a researcher decision.
182
182
 
183
183
  `longtable team --debate` creates a fixed five-round debate record under
184
184
  `.longtable/team/<id>/`: independent review, cross-review, rebuttal,
185
- convergence, and synthesis/checkpoint. Tmux can show live role panes, but the
186
- file-backed artifact directory is the source of truth.
185
+ convergence, and synthesis/checkpoint. The file-backed artifact directory is
186
+ the source of truth.
187
+
188
+ See `docs/AGENT-TEAM-README.md` in the repository for a user-facing guide to
189
+ panel, team, and debate surfaces.
187
190
 
188
191
  ## Evidence And Search Direction
189
192
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
- import { execFileSync, execSync } from "node:child_process";
4
+ import { execSync } from "node:child_process";
5
5
  import { emitKeypressEvents } from "node:readline";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { stdin as input, stdout as output, cwd, exit } from "node:process";
@@ -15,8 +15,8 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
15
15
  import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
16
16
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
17
17
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
18
- import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceClarificationCard, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
- import { buildTeamDebate, renderTeamDebateSummary } from "./debate.js";
18
+ import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
+ import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
20
20
  const VALID_MODES = new Set([
21
21
  "explore",
22
22
  "review",
@@ -42,7 +42,7 @@ const ANSI = {
42
42
  green: "\u001B[32m"
43
43
  };
44
44
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
45
- const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.24";
45
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.26";
46
46
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
47
47
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
48
48
  function style(text, prefix) {
@@ -89,7 +89,7 @@ function usage() {
89
89
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
90
90
  " longtable mcp install [--provider codex|claude|all] [--write] [--checkpoint-ui off|interactive|strong] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
91
91
  " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
92
- " longtable team --prompt <text> [--role <role[,role]>] [--tmux] [--debate] [--rounds 5] [--cwd <path>] [--json]",
92
+ " longtable team --prompt <text> [--role <role[,role]>] [--debate] [--rounds 3|5] [--cwd <path>] [--json]",
93
93
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
94
94
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
95
95
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
@@ -622,12 +622,12 @@ function buildPermissionSetupChoices() {
622
622
  {
623
623
  id: "strong",
624
624
  label: "Strong Researcher Checkpoint UI",
625
- description: "Why: uses Codex MCP elicitation for high-responsibility research decisions. What you get: clickable checkpoints with other/rationale. Tradeoff: requires Codex MCP elicitation approval."
625
+ description: "Why: makes Codex UI checkpoints the default for high-responsibility research decisions. What you get: MCP form checkpoints with a single decision field. Tradeoff: requires Codex MCP elicitation approval."
626
626
  },
627
627
  {
628
628
  id: "interactive",
629
629
  label: "Interactive checkpoint UI",
630
- description: "Why: keeps UI prompts available for required checkpoints. What you get: MCP form checkpoints when supported. Tradeoff: requires Codex MCP elicitation approval."
630
+ description: "Why: keeps Codex UI prompts available for required checkpoints. What you get: MCP form checkpoints when supported. Tradeoff: requires Codex MCP elicitation approval."
631
631
  },
632
632
  {
633
633
  id: "off",
@@ -768,7 +768,6 @@ async function runSetup(args) {
768
768
  interventionPosture: effectiveIntervention,
769
769
  checkpointUiMode: checkpointUi,
770
770
  workspaceCreationPreference: workspacePreference,
771
- tmuxMode: "standard",
772
771
  teamMode: "panel"
773
772
  };
774
773
  if (surfaces === "skills_mcp_sentinel") {
@@ -1769,6 +1768,38 @@ function inferModeFromPrompt(prompt) {
1769
1768
  }
1770
1769
  return "explore";
1771
1770
  }
1771
+ function inferCollaborationRoute(prompt) {
1772
+ const normalized = prompt.toLowerCase();
1773
+ const explicitDebate = /\bdebate\b|\bdebated\b|\brebuttal\b|\bconvergence\b|\bargue both sides\b/i.test(prompt) ||
1774
+ /토론|논쟁|반박|재반박|수렴/.test(prompt);
1775
+ if (explicitDebate) {
1776
+ return "debate";
1777
+ }
1778
+ const explicitTeam = /\bagent team\b|\bresearch team\b|\bteam review\b|\bteam-style\b|\buse a team\b/i.test(prompt) ||
1779
+ /에이전트\s*팀|연구\s*팀|팀\s*(리뷰|검토)|팀으로/.test(prompt);
1780
+ if (explicitTeam) {
1781
+ return "team";
1782
+ }
1783
+ const panelCue = /\bpanel\b|\bmulti[- ]?role\b|\bmultiple perspectives\b|\brole disagreement\b|\bdisagreement\b|\bconflict\b/i.test(prompt) ||
1784
+ /패널|여러\s*관점|복수\s*관점|역할.*불일치|불일치|충돌/.test(prompt);
1785
+ const multiPerspectiveCue = panelCue ||
1786
+ /\bperspectives?\b|\broles?\b|\breviewer and editor\b|\beditor and reviewer\b|\bmethods and measurement\b/i.test(prompt) ||
1787
+ /관점|역할|리뷰어.*에디터|에디터.*리뷰어|방법.*측정|측정.*방법/.test(prompt);
1788
+ if (!multiPerspectiveCue) {
1789
+ return null;
1790
+ }
1791
+ const trigger = classifyCheckpointTrigger(prompt, {});
1792
+ const highStakes = trigger.signal.artifactStakes === "external_submission" ||
1793
+ trigger.signal.artifactStakes === "study_protocol" ||
1794
+ trigger.requiresQuestionBeforeClosure;
1795
+ if (panelCue && trigger.signal.artifactStakes === "external_submission") {
1796
+ return "debate";
1797
+ }
1798
+ if (highStakes) {
1799
+ return "team";
1800
+ }
1801
+ return "panel";
1802
+ }
1772
1803
  function parsePanelVisibility(value) {
1773
1804
  if (value === "synthesis_only" ||
1774
1805
  value === "show_on_conflict" ||
@@ -1820,6 +1851,9 @@ async function runModeCommand(mode, args) {
1820
1851
  }
1821
1852
  const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
1822
1853
  const projectContext = await loadProjectContextFromDirectory(workingDirectory);
1854
+ if (projectContext) {
1855
+ await assertWorkspaceNotBlocked(projectContext);
1856
+ }
1823
1857
  const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
1824
1858
  const panelPreference = setup?.profileSeed.panelPreference;
1825
1859
  const panelRequested = args.panel === true ||
@@ -2019,9 +2053,9 @@ function questionRecordToChoices(record) {
2019
2053
  : [])
2020
2054
  ];
2021
2055
  }
2022
- function renderClarificationCard(questions) {
2056
+ function renderFollowUpQuestions(questions) {
2023
2057
  if (questions.length === 0) {
2024
- return "No new clarification questions are pending for this prompt.";
2058
+ return "No new follow-up questions are pending for this prompt.";
2025
2059
  }
2026
2060
  const width = 44;
2027
2061
  const boxLine = (text = "") => `│ ${text.padEnd(width, " ")} │`;
@@ -2072,13 +2106,13 @@ function renderClarificationCard(questions) {
2072
2106
  lines.push("Answer in a terminal with `longtable clarify --prompt ...`, or record choices with `longtable decide --question <id> --answer <value>`.");
2073
2107
  return lines.join("\n");
2074
2108
  }
2075
- async function answerClarificationCardInTerminal(context, questions, provider) {
2109
+ async function answerFollowUpQuestionsInTerminal(context, questions, provider) {
2076
2110
  if (questions.length === 0) {
2077
2111
  return;
2078
2112
  }
2079
2113
  const rl = createInterface({ input, output });
2080
2114
  try {
2081
- console.log(renderBrandBanner("LongTable", "Clarification Card"));
2115
+ console.log(renderBrandBanner("LongTable", "Follow-up Questions"));
2082
2116
  console.log("");
2083
2117
  for (let index = 0; index < questions.length; index += 1) {
2084
2118
  const question = questions[index];
@@ -2109,7 +2143,7 @@ async function runClarify(args) {
2109
2143
  }
2110
2144
  const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
2111
2145
  const required = args.required === true ? true : args.advisory === true ? false : undefined;
2112
- const result = await createWorkspaceClarificationCard({
2146
+ const result = await createWorkspaceFollowUpQuestions({
2113
2147
  context,
2114
2148
  prompt,
2115
2149
  provider,
@@ -2129,17 +2163,17 @@ async function runClarify(args) {
2129
2163
  return;
2130
2164
  }
2131
2165
  if (args.print === true || !isInteractiveTerminal()) {
2132
- console.log(renderClarificationCard(result.questions));
2166
+ console.log(renderFollowUpQuestions(result.questions));
2133
2167
  return;
2134
2168
  }
2135
- await answerClarificationCardInTerminal(context, result.questions, provider);
2169
+ await answerFollowUpQuestionsInTerminal(context, result.questions, provider);
2136
2170
  console.log("");
2137
- console.log("LongTable clarification decisions recorded");
2171
+ console.log("LongTable follow-up decisions recorded");
2138
2172
  console.log(`- answered: ${result.questions.length}`);
2139
2173
  console.log(`- state: ${context.stateFilePath}`);
2140
2174
  console.log(`- current: ${context.currentFilePath}`);
2141
2175
  }
2142
- async function runAutomaticClarificationIfNeeded(prompt, args) {
2176
+ async function runAutomaticFollowUpIfNeeded(prompt, args) {
2143
2177
  if (args["no-clarify"] === true || args.print === true || args.json === true) {
2144
2178
  return false;
2145
2179
  }
@@ -2149,7 +2183,7 @@ async function runAutomaticClarificationIfNeeded(prompt, args) {
2149
2183
  return false;
2150
2184
  }
2151
2185
  const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
2152
- const result = await createWorkspaceClarificationCard({
2186
+ const result = await createWorkspaceFollowUpQuestions({
2153
2187
  context,
2154
2188
  prompt,
2155
2189
  provider,
@@ -2159,17 +2193,22 @@ async function runAutomaticClarificationIfNeeded(prompt, args) {
2159
2193
  return false;
2160
2194
  }
2161
2195
  if (!isInteractiveTerminal()) {
2162
- console.log(renderClarificationCard(result.questions));
2196
+ console.log(renderFollowUpQuestions(result.questions));
2163
2197
  return true;
2164
2198
  }
2165
- await answerClarificationCardInTerminal(context, result.questions, provider);
2199
+ await answerFollowUpQuestionsInTerminal(context, result.questions, provider);
2166
2200
  return false;
2167
2201
  }
2168
2202
  async function runAsk(args) {
2203
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2169
2204
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
2170
2205
  if (!prompt) {
2171
2206
  throw new Error("A prompt is required.");
2172
2207
  }
2208
+ const projectContext = await loadProjectContextFromDirectory(workingDirectory);
2209
+ if (projectContext) {
2210
+ await assertWorkspaceNotBlocked(projectContext);
2211
+ }
2173
2212
  const directive = parseInvocationDirective(prompt);
2174
2213
  const effectivePrompt = directive.cleanedPrompt;
2175
2214
  const inferred = directive.mode ?? inferModeFromPrompt(effectivePrompt);
@@ -2178,7 +2217,7 @@ async function runAsk(args) {
2178
2217
  return;
2179
2218
  }
2180
2219
  const mode = inferred === "panel" ? "review" : inferred;
2181
- if (await runAutomaticClarificationIfNeeded(effectivePrompt, args)) {
2220
+ if (await runAutomaticFollowUpIfNeeded(effectivePrompt, args)) {
2182
2221
  return;
2183
2222
  }
2184
2223
  const delegatedArgs = {
@@ -2188,7 +2227,18 @@ async function runAsk(args) {
2188
2227
  if (directive.roles.length > 0 && typeof delegatedArgs.role !== "string") {
2189
2228
  delegatedArgs.role = directive.roles.join(",");
2190
2229
  }
2191
- if (inferred === "panel" || directive.panel || delegatedArgs.panel === true) {
2230
+ const collaborationRoute = directive.collaboration ??
2231
+ (directive.panel || delegatedArgs.panel === true
2232
+ ? "panel"
2233
+ : inferCollaborationRoute(effectivePrompt) ?? (inferred === "panel" ? "panel" : null));
2234
+ if (collaborationRoute === "team" || collaborationRoute === "debate") {
2235
+ await runTeam({
2236
+ ...delegatedArgs,
2237
+ debate: collaborationRoute === "debate"
2238
+ });
2239
+ return;
2240
+ }
2241
+ if (collaborationRoute === "panel") {
2192
2242
  await runPanelCommand({
2193
2243
  ...delegatedArgs,
2194
2244
  visibility: "always_visible"
@@ -2200,9 +2250,6 @@ async function runAsk(args) {
2200
2250
  function localId(prefix) {
2201
2251
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2202
2252
  }
2203
- function shellEscape(value) {
2204
- return `'${value.replaceAll("'", "'\\''")}'`;
2205
- }
2206
2253
  async function writeJsonFile(path, value) {
2207
2254
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
2208
2255
  }
@@ -2305,9 +2352,13 @@ async function runTeam(args) {
2305
2352
  if (!prompt) {
2306
2353
  throw new Error("A prompt is required.");
2307
2354
  }
2308
- const rounds = typeof args.rounds === "string" ? Number(args.rounds) : 5;
2309
- if (!Number.isInteger(rounds) || rounds !== 5) {
2310
- throw new Error("LongTable team debate v1 supports `--rounds 5` only.");
2355
+ const isDebate = args.debate === true;
2356
+ const expectedRounds = isDebate ? 5 : 3;
2357
+ const rounds = typeof args.rounds === "string" ? Number(args.rounds) : expectedRounds;
2358
+ if (!Number.isInteger(rounds) || rounds !== expectedRounds) {
2359
+ throw new Error(isDebate
2360
+ ? "LongTable team debate v1 supports `--rounds 5` only."
2361
+ : "LongTable team v1 supports `--rounds 3` cross-review only.");
2311
2362
  }
2312
2363
  const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
2313
2364
  const projectContext = await loadProjectContextFromDirectory(workingDirectory);
@@ -2315,16 +2366,9 @@ async function runTeam(args) {
2315
2366
  await assertWorkspaceNotBlocked(projectContext);
2316
2367
  }
2317
2368
  const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
2318
- const fallback = buildPanelFallback({
2319
- prompt,
2320
- mode: "review",
2321
- roleFlag: typeof args.role === "string" ? args.role : undefined,
2322
- provider: setup?.providerSelection.provider,
2323
- visibility: "always_visible"
2324
- });
2325
2369
  const teamId = localId("team");
2326
2370
  const teamDir = join(workingDirectory, ".longtable", "team", teamId);
2327
- if (args.debate === true) {
2371
+ if (isDebate) {
2328
2372
  const debate = buildTeamDebate({
2329
2373
  teamId,
2330
2374
  teamDir,
@@ -2332,8 +2376,7 @@ async function runTeam(args) {
2332
2376
  roleFlag: typeof args.role === "string" ? args.role : undefined,
2333
2377
  provider: setup?.providerSelection.provider,
2334
2378
  visibility: "always_visible",
2335
- roundCount: rounds,
2336
- tmux: args.tmux === true
2379
+ roundCount: rounds
2337
2380
  });
2338
2381
  await writeTeamDebateArtifacts(debate, teamDir, prompt);
2339
2382
  const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
@@ -2357,87 +2400,44 @@ async function runTeam(args) {
2357
2400
  }, null, 2));
2358
2401
  return;
2359
2402
  }
2360
- if (args.tmux === true) {
2361
- const sessionName = `longtable-${teamId.replaceAll("_", "-")}`;
2362
- const shell = process.env.SHELL || "/bin/sh";
2363
- const launcher = process.argv[1] ?? "longtable";
2364
- const leaderCommand = [
2365
- `echo ${shellEscape(`LongTable debate ${teamId}`)}`,
2366
- `echo ${shellEscape(`Artifacts: ${teamDir}`)}`,
2367
- `echo ${shellEscape("Fixed rounds are recorded. Role panes can add live review logs.")}`,
2368
- `echo ${shellEscape(`Checkpoint: ${debate.questionRecord.id}`)}`,
2369
- `exec ${shellEscape(shell)}`
2370
- ].join("; ");
2371
- execFileSync("tmux", ["new-session", "-d", "-s", sessionName, "-c", workingDirectory, leaderCommand], { stdio: "inherit" });
2372
- for (const member of debate.plan.members) {
2373
- const rolePrompt = [
2374
- `LongTable autonomous debate role: ${member.label} (${member.role}).`,
2375
- "Use the fixed debate artifacts as the shared record. Add live notes only; do not answer the researcher checkpoint.",
2376
- `Artifacts: ${teamDir}`,
2377
- "",
2378
- projectAware.prompt
2379
- ].join("\n");
2380
- const logPath = join(teamDir, `${member.role}.debate.log`);
2381
- const command = [
2382
- `node ${shellEscape(launcher)} review --role ${shellEscape(member.role)} --prompt ${shellEscape(rolePrompt)} --cwd ${shellEscape(workingDirectory)} 2>&1 | tee ${shellEscape(logPath)}`,
2383
- `echo ${shellEscape(`Debate role log written to ${logPath}`)}`,
2384
- `exec ${shellEscape(shell)}`
2385
- ].join("; ");
2386
- execFileSync("tmux", ["split-window", "-t", sessionName, "-c", workingDirectory, command], { stdio: "inherit" });
2387
- execFileSync("tmux", ["select-layout", "-t", sessionName, "tiled"], { stdio: "ignore" });
2388
- }
2389
- console.log(`LongTable tmux debate launched: ${sessionName}`);
2390
- console.log(`Attach with: tmux attach -t ${sessionName}`);
2391
- console.log(`Artifacts: ${teamDir}`);
2392
- return;
2393
- }
2394
2403
  console.log(renderTeamDebateSummary(debate.run));
2395
2404
  console.log(`- checkpoint: ${debate.questionRecord.id}`);
2396
2405
  return;
2397
2406
  }
2398
- await mkdir(teamDir, { recursive: true });
2399
- await writeFile(join(teamDir, "prompt.txt"), prompt, "utf8");
2400
- await writeFile(join(teamDir, "plan.json"), JSON.stringify(fallback.plan, null, 2), "utf8");
2401
- if (args.json === true) {
2402
- console.log(JSON.stringify({ teamId, teamDir, plan: fallback.plan }, null, 2));
2403
- return;
2407
+ const team = buildTeamReview({
2408
+ teamId,
2409
+ teamDir,
2410
+ prompt: projectAware.prompt,
2411
+ roleFlag: typeof args.role === "string" ? args.role : undefined,
2412
+ provider: setup?.providerSelection.provider,
2413
+ visibility: "always_visible",
2414
+ roundCount: rounds
2415
+ });
2416
+ await writeTeamDebateArtifacts(team, teamDir, prompt);
2417
+ const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
2418
+ if (canRecordWorkspace) {
2419
+ await appendInvocationRecordToWorkspace(projectContext, team.invocationRecord, [team.questionRecord]);
2404
2420
  }
2405
- if (args.tmux !== true) {
2406
- console.log(renderPanelSummary(fallback.plan));
2407
- console.log("");
2408
- console.log("Run with `--tmux` to launch role panes for parallel discussion.");
2421
+ if (args.json === true) {
2422
+ console.log(JSON.stringify({
2423
+ teamId,
2424
+ teamDir,
2425
+ plan: team.plan,
2426
+ run: team.run,
2427
+ questionRecord: team.questionRecord,
2428
+ invocationRecord: team.invocationRecord,
2429
+ execution: {
2430
+ status: "completed",
2431
+ surface: team.run.surface,
2432
+ interactionDepth: team.run.interactionDepth,
2433
+ projectContextFound: projectAware.projectContextFound,
2434
+ invocationLogged: canRecordWorkspace
2435
+ }
2436
+ }, null, 2));
2409
2437
  return;
2410
2438
  }
2411
- const sessionName = `longtable-${teamId.replaceAll("_", "-")}`;
2412
- const shell = process.env.SHELL || "/bin/sh";
2413
- const launcher = process.argv[1] ?? "longtable";
2414
- const leaderCommand = [
2415
- `echo ${shellEscape(`LongTable team ${teamId}`)}`,
2416
- `echo ${shellEscape(`Logs: ${teamDir}`)}`,
2417
- "echo 'Role panes are running. Review logs, then run:'",
2418
- `echo ${shellEscape(`longtable panel --role ${fallback.plan.members.map((member) => member.role).join(",")} --prompt ${JSON.stringify(prompt)}`)}`,
2419
- `exec ${shellEscape(shell)}`
2420
- ].join("; ");
2421
- execFileSync("tmux", ["new-session", "-d", "-s", sessionName, "-c", workingDirectory, leaderCommand], { stdio: "inherit" });
2422
- for (const member of fallback.plan.members) {
2423
- const rolePrompt = [
2424
- `LongTable team discussion role: ${member.label} (${member.role}).`,
2425
- "Give claims, objections, open questions, and evidence needs. Address likely disagreement with other roles.",
2426
- "",
2427
- prompt
2428
- ].join("\n");
2429
- const logPath = join(teamDir, `${member.role}.log`);
2430
- const command = [
2431
- `node ${shellEscape(launcher)} review --role ${shellEscape(member.role)} --prompt ${shellEscape(rolePrompt)} --cwd ${shellEscape(workingDirectory)} 2>&1 | tee ${shellEscape(logPath)}`,
2432
- `echo ${shellEscape(`Role log written to ${logPath}`)}`,
2433
- `exec ${shellEscape(shell)}`
2434
- ].join("; ");
2435
- execFileSync("tmux", ["split-window", "-t", sessionName, "-c", workingDirectory, command], { stdio: "inherit" });
2436
- execFileSync("tmux", ["select-layout", "-t", sessionName, "tiled"], { stdio: "ignore" });
2437
- }
2438
- console.log(`LongTable tmux team launched: ${sessionName}`);
2439
- console.log(`Attach with: tmux attach -t ${sessionName}`);
2440
- console.log(`Logs: ${teamDir}`);
2439
+ console.log(renderTeamDebateSummary(team.run));
2440
+ console.log(`- checkpoint: ${team.questionRecord.id}`);
2441
2441
  }
2442
2442
  async function runDecide(args) {
2443
2443
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
package/dist/debate.d.ts CHANGED
@@ -7,7 +7,6 @@ export interface BuildTeamDebateOptions {
7
7
  provider?: ProviderKind;
8
8
  visibility?: PanelVisibility;
9
9
  roundCount?: number;
10
- tmux?: boolean;
11
10
  }
12
11
  export interface TeamDebateBundle {
13
12
  plan: PanelPlan;
@@ -17,5 +16,6 @@ export interface TeamDebateBundle {
17
16
  questionRecord: QuestionRecord;
18
17
  }
19
18
  export declare function createTeamDebateQuestionRecord(run: TeamDebateRun, provider?: ProviderKind): QuestionRecord;
19
+ export declare function buildTeamReview(options: BuildTeamDebateOptions): TeamDebateBundle;
20
20
  export declare function buildTeamDebate(options: BuildTeamDebateOptions): TeamDebateBundle;
21
21
  export declare function renderTeamDebateSummary(run: TeamDebateRun): string;
package/dist/debate.js CHANGED
@@ -68,15 +68,18 @@ function independentContribution(roundId, plan, role, label, artifactPath) {
68
68
  checkpointTriggers: tags.map((tag) => `${tag}_commitment`)
69
69
  });
70
70
  }
71
- function crossReviewContribution(roundId, plan, role, label, targetRole, targetLabel, artifactPath) {
71
+ function crossReviewContribution(roundId, plan, role, label, targetRole, targetLabel, targetContribution, artifactPath) {
72
72
  return contribution({
73
73
  roundId,
74
74
  role,
75
75
  label,
76
76
  targetRole,
77
+ respondsToContributionId: targetContribution.id,
78
+ stance: "conditional",
77
79
  artifactPath,
78
- summary: `${label} challenges ${targetLabel}'s likely blind spot before synthesis.`,
80
+ summary: `${label} cross-reviews ${targetLabel}'s independent contribution (${targetContribution.id}) before synthesis.`,
79
81
  claims: [
82
+ `Responds to ${targetLabel}'s claim: ${targetContribution.claims[0]}`,
80
83
  `${targetLabel}'s concern is useful only if it does not erase ${label}'s domain-specific risk.`,
81
84
  "The debate should expose disagreement as a researcher decision point rather than normalize it away."
82
85
  ],
@@ -160,12 +163,13 @@ function convergenceContribution(roundId, plan, role, label, artifactPath) {
160
163
  checkpointTriggers: ["panel_next_decision"]
161
164
  });
162
165
  }
163
- function buildSynthesis(plan, artifactPath) {
166
+ function buildSynthesis(plan, artifactPath, kind) {
164
167
  const labels = plan.members.map((member) => member.label);
165
168
  const highSensitivity = plan.checkpointSensitivity === "high";
169
+ const runLabel = kind === "debate" ? "debate" : "team review";
166
170
  return {
167
171
  artifactPath,
168
- summary: `The debate completed fixed 5-round review across ${labels.join(", ")}. It should slow closure by turning role disagreement into an explicit researcher decision.`,
172
+ summary: `The ${runLabel} completed across ${labels.join(", ")}. It should slow closure by turning role disagreement into an explicit researcher decision.`,
169
173
  consensus: [
170
174
  "The researcher should see role disagreement before LongTable drafts, commits, or submits a conclusion.",
171
175
  "Evidence gaps and tacit assumptions should remain visible instead of being smoothed into fluent prose."
@@ -183,12 +187,13 @@ function buildSynthesis(plan, artifactPath) {
183
187
  "Choose whether the debate should affect the current artifact, the research design, or only the decision log."
184
188
  ],
185
189
  recommendedCheckpoint: highSensitivity
186
- ? "The debate surfaced high-sensitivity disagreement. What should LongTable treat as the next human decision before closure?"
187
- : "The debate surfaced role disagreement. Should LongTable revise, verify evidence, proceed, or keep the tension open?"
190
+ ? `The ${runLabel} surfaced high-sensitivity disagreement. What should LongTable treat as the next human decision before closure?`
191
+ : `The ${runLabel} surfaced role disagreement. Should LongTable revise, verify evidence, proceed, or keep the tension open?`
188
192
  };
189
193
  }
190
194
  export function createTeamDebateQuestionRecord(run, provider) {
191
195
  const createdAt = nowIso();
196
+ const isDebate = run.interactionDepth === "debated";
192
197
  return {
193
198
  id: createId("question_record"),
194
199
  createdAt,
@@ -197,7 +202,7 @@ export function createTeamDebateQuestionRecord(run, provider) {
197
202
  prompt: {
198
203
  id: createId("question_prompt"),
199
204
  checkpointKey: "team_debate_next_decision",
200
- title: "Team debate follow-up decision",
205
+ title: isDebate ? "Team debate follow-up decision" : "Agent team follow-up decision",
201
206
  question: run.synthesis.recommendedCheckpoint,
202
207
  type: "single_choice",
203
208
  options: [
@@ -229,10 +234,15 @@ export function createTeamDebateQuestionRecord(run, provider) {
229
234
  return key ? getPersonaDefinition(key).checkpointSensitivity === "high" : false;
230
235
  }),
231
236
  source: "runtime_guidance",
237
+ displayReason: isDebate
238
+ ? "Role rebuttals and convergence should connect to an explicit researcher decision."
239
+ : "Cross-review created role disagreement that should connect to an explicit researcher decision.",
232
240
  rationale: [
233
- "Autonomous team debate is a research harness surface, not a substitute for researcher judgment.",
234
- "The fixed debate rounds created disagreement that should connect to an explicit researcher decision.",
235
- `Team debate run: ${run.id}.`
241
+ "Agent team orchestration is a research harness surface, not a substitute for researcher judgment.",
242
+ isDebate
243
+ ? "The fixed debate rounds created disagreement that should connect to an explicit researcher decision."
244
+ : "The cross-review round created disagreement that should connect to an explicit researcher decision.",
245
+ `Agent team run: ${run.id}.`
236
246
  ],
237
247
  preferredSurfaces: provider === "claude"
238
248
  ? ["native_structured", "numbered"]
@@ -240,10 +250,13 @@ export function createTeamDebateQuestionRecord(run, provider) {
240
250
  }
241
251
  };
242
252
  }
243
- export function buildTeamDebate(options) {
244
- const roundCount = options.roundCount ?? 5;
245
- if (roundCount !== 5) {
246
- throw new Error("LongTable debate v1 supports fixed 5-round debate only.");
253
+ function buildTeamBundle(options, kind) {
254
+ const roundCount = options.roundCount ?? (kind === "debate" ? 5 : 3);
255
+ const expectedRounds = kind === "debate" ? 5 : 3;
256
+ if (roundCount !== expectedRounds) {
257
+ throw new Error(kind === "debate"
258
+ ? "LongTable debate v1 supports fixed 5-round debate only."
259
+ : "LongTable team v1 supports fixed 3-round cross-review only.");
247
260
  }
248
261
  const createdAt = nowIso();
249
262
  const plan = buildPanelPlan({
@@ -255,6 +268,7 @@ export function buildTeamDebate(options) {
255
268
  });
256
269
  const rounds = [];
257
270
  const round1Id = createId("team_round");
271
+ const independentContributions = plan.members.map((member) => independentContribution(round1Id, plan, member.role, member.label, join("round-1-independent", `${member.role}.json`)));
258
272
  rounds.push({
259
273
  id: round1Id,
260
274
  index: 1,
@@ -262,12 +276,12 @@ export function buildTeamDebate(options) {
262
276
  title: "Independent review",
263
277
  status: "completed",
264
278
  artifactDir: join(options.teamDir, "round-1-independent"),
265
- contributions: plan.members.map((member) => independentContribution(round1Id, plan, member.role, member.label, join("round-1-independent", `${member.role}.json`)))
279
+ contributions: independentContributions
266
280
  });
267
281
  const round2Id = createId("team_round");
268
282
  const crossContributions = plan.members.flatMap((member) => plan.members
269
283
  .filter((target) => target.role !== member.role)
270
- .map((target) => crossReviewContribution(round2Id, plan, member.role, member.label, target.role, target.label, join("round-2-cross-review", `${member.role}-on-${target.role}.json`))));
284
+ .map((target) => crossReviewContribution(round2Id, plan, member.role, member.label, target.role, target.label, independentContributions.find((contribution) => contribution.role === target.role), join("round-2-cross-review", `${member.role}-on-${target.role}.json`))));
271
285
  rounds.push({
272
286
  id: round2Id,
273
287
  index: 2,
@@ -277,27 +291,29 @@ export function buildTeamDebate(options) {
277
291
  artifactDir: join(options.teamDir, "round-2-cross-review"),
278
292
  contributions: crossContributions
279
293
  });
280
- const round3Id = createId("team_round");
281
- rounds.push({
282
- id: round3Id,
283
- index: 3,
284
- kind: "rebuttal",
285
- title: "Rebuttal and revision",
286
- status: "completed",
287
- artifactDir: join(options.teamDir, "round-3-rebuttal"),
288
- contributions: plan.members.map((member) => rebuttalContribution(round3Id, member.role, member.label, join("round-3-rebuttal", `${member.role}.json`)))
289
- });
290
- const round4Id = createId("team_round");
291
- rounds.push({
292
- id: round4Id,
293
- index: 4,
294
- kind: "convergence",
295
- title: "Convergence and unresolved gaps",
296
- status: "completed",
297
- artifactDir: join(options.teamDir, "round-4-convergence"),
298
- contributions: plan.members.map((member) => convergenceContribution(round4Id, plan, member.role, member.label, join("round-4-convergence", `${member.role}.json`)))
299
- });
300
- const synthesis = buildSynthesis(plan, "synthesis.json");
294
+ if (kind === "debate") {
295
+ const round3Id = createId("team_round");
296
+ rounds.push({
297
+ id: round3Id,
298
+ index: 3,
299
+ kind: "rebuttal",
300
+ title: "Rebuttal and revision",
301
+ status: "completed",
302
+ artifactDir: join(options.teamDir, "round-3-rebuttal"),
303
+ contributions: plan.members.map((member) => rebuttalContribution(round3Id, member.role, member.label, join("round-3-rebuttal", `${member.role}.json`)))
304
+ });
305
+ const round4Id = createId("team_round");
306
+ rounds.push({
307
+ id: round4Id,
308
+ index: 4,
309
+ kind: "convergence",
310
+ title: "Convergence and unresolved gaps",
311
+ status: "completed",
312
+ artifactDir: join(options.teamDir, "round-4-convergence"),
313
+ contributions: plan.members.map((member) => convergenceContribution(round4Id, plan, member.role, member.label, join("round-4-convergence", `${member.role}.json`)))
314
+ });
315
+ }
316
+ const synthesis = buildSynthesis(plan, "synthesis.json", kind);
301
317
  const run = {
302
318
  id: createId("team_debate_run"),
303
319
  teamId: options.teamId,
@@ -306,15 +322,16 @@ export function buildTeamDebate(options) {
306
322
  prompt: options.prompt,
307
323
  roles: plan.members,
308
324
  status: "completed",
309
- surface: options.tmux ? "tmux_console" : "file_backed_debate",
310
- roundPolicy: "fixed",
325
+ surface: "file_backed_debate",
326
+ interactionDepth: kind === "debate" ? "debated" : "cross_reviewed",
327
+ roundPolicy: kind === "debate" ? "fixed" : "team_cross_review",
311
328
  roundCount,
312
329
  artifactRoot: options.teamDir,
313
330
  rounds: [
314
331
  ...rounds,
315
332
  {
316
333
  id: createId("team_round"),
317
- index: 5,
334
+ index: roundCount,
318
335
  kind: "synthesis",
319
336
  title: "Coordinator synthesis and checkpoint",
320
337
  status: "completed",
@@ -335,11 +352,13 @@ export function buildTeamDebate(options) {
335
352
  visibility: plan.visibility,
336
353
  checkpointSensitivity: plan.checkpointSensitivity,
337
354
  rationale: [
338
- "Autonomous debate requested through LongTable team orchestration.",
339
- "File-backed fixed rounds keep disagreement inspectable before researcher closure."
355
+ kind === "debate"
356
+ ? "Autonomous debate requested through LongTable team orchestration."
357
+ : "Agent team cross-review requested through LongTable team orchestration.",
358
+ "File-backed rounds keep disagreement inspectable before researcher closure."
340
359
  ]
341
360
  });
342
- intent.kind = "team_debate";
361
+ intent.kind = kind === "debate" ? "team_debate" : "team";
343
362
  intent.requestedSurface = run.surface;
344
363
  const invocationRecord = {
345
364
  id: createId("invocation_record"),
@@ -349,11 +368,10 @@ export function buildTeamDebate(options) {
349
368
  status: "completed",
350
369
  provider: options.provider,
351
370
  surface: run.surface,
371
+ interactionDepth: run.interactionDepth,
352
372
  panelPlan: plan,
353
373
  teamDebateRun: run,
354
- degradationReason: options.tmux
355
- ? undefined
356
- : "Tmux was not requested; file-backed debate artifacts are the canonical execution record."
374
+ degradationReason: "File-backed team artifacts are the canonical execution record."
357
375
  };
358
376
  return {
359
377
  plan,
@@ -363,12 +381,19 @@ export function buildTeamDebate(options) {
363
381
  questionRecord
364
382
  };
365
383
  }
384
+ export function buildTeamReview(options) {
385
+ return buildTeamBundle(options, "team");
386
+ }
387
+ export function buildTeamDebate(options) {
388
+ return buildTeamBundle(options, "debate");
389
+ }
366
390
  export function renderTeamDebateSummary(run) {
367
391
  return [
368
392
  "LongTable Team Debate",
369
393
  `- team: ${run.teamId}`,
370
394
  `- surface: ${run.surface}`,
371
- `- rounds: ${run.roundCount} fixed`,
395
+ `- interaction depth: ${run.interactionDepth}`,
396
+ `- rounds: ${run.roundCount} ${run.roundPolicy}`,
372
397
  `- roles: ${run.roles.map((role) => `${role.label} (${role.role})`).join(", ")}`,
373
398
  `- artifacts: ${run.artifactRoot}`,
374
399
  "",
package/dist/panel.js CHANGED
@@ -143,6 +143,7 @@ export function createPlannedPanelQuestionRecord(plan, provider) {
143
143
  "Panel review creates disagreement or risk visibility that should connect to an explicit researcher decision.",
144
144
  `Panel checkpoint sensitivity: ${plan.checkpointSensitivity}.`
145
145
  ],
146
+ displayReason: "Panel review can surface role disagreement that should not be collapsed without researcher approval.",
146
147
  preferredSurfaces: provider === "claude"
147
148
  ? ["native_structured", "numbered"]
148
149
  : ["mcp_elicitation", "numbered"]
@@ -159,6 +160,7 @@ export function createPlannedPanelResult(plan, provider, linkedQuestionRecordIds
159
160
  provider,
160
161
  surface: "sequential_fallback",
161
162
  status: "planned",
163
+ interactionDepth: "independent",
162
164
  memberResults: plan.members.map((member) => ({
163
165
  role: member.role,
164
166
  label: member.label,
@@ -178,6 +180,7 @@ export function createPlannedInvocationRecord(options) {
178
180
  status: "planned",
179
181
  provider: options.provider,
180
182
  surface: "sequential_fallback",
183
+ interactionDepth: "independent",
181
184
  panelPlan: options.plan,
182
185
  panelResult: options.result,
183
186
  degradationReason: "Native provider team execution is optional; sequential fallback is the stable LongTable surface."
@@ -12,6 +12,7 @@ export interface LongTableInvocationDirective {
12
12
  explicit: boolean;
13
13
  cleanedPrompt: string;
14
14
  mode?: InteractionMode | "panel" | "status";
15
+ collaboration?: "panel" | "team" | "debate";
15
16
  roles: CanonicalPersona[];
16
17
  panel: boolean;
17
18
  showConflicts: boolean;
@@ -7,7 +7,9 @@ const DIRECTIVE_MAP = [
7
7
  { key: "critique", mode: "critique" },
8
8
  { key: "draft", mode: "draft" },
9
9
  { key: "commit", mode: "commit" },
10
- { key: "panel", mode: "panel", panel: true, showConflicts: true },
10
+ { key: "panel", mode: "panel", collaboration: "panel", panel: true, showConflicts: true },
11
+ { key: "team", mode: "review", collaboration: "team", panel: true, showConflicts: true },
12
+ { key: "debate", mode: "review", collaboration: "debate", panel: true, showConflicts: true },
11
13
  { key: "status", mode: "status" },
12
14
  { key: "editor", mode: "review", roles: ["editor"] },
13
15
  { key: "reviewer", mode: "review", roles: ["reviewer"] },
@@ -72,6 +74,7 @@ export function parseInvocationDirective(prompt) {
72
74
  explicit: true,
73
75
  cleanedPrompt: body || prompt,
74
76
  mode: directive.mode,
77
+ collaboration: directive.collaboration,
75
78
  roles: directive.roles ?? [],
76
79
  panel: directive.panel === true,
77
80
  showConflicts: directive.showConflicts === true
@@ -1,4 +1,4 @@
1
- import type { DecisionRecord, InvocationRecord, ProviderKind, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
1
+ import type { DecisionRecord, InvocationRecord, ProviderKind, QuestionOption, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
2
2
  import type { SetupPersistedOutput } from "@longtable/setup";
3
3
  export type ProjectDisagreementPreference = "synthesis_only" | "show_on_conflict" | "always_visible";
4
4
  export interface LongTableProjectRecord {
@@ -113,7 +113,7 @@ export declare function syncCurrentWorkspaceView(context: LongTableProjectContex
113
113
  export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<ResearchState>;
114
114
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
115
115
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
116
- export declare function createWorkspaceClarificationCard(options: {
116
+ export declare function createWorkspaceFollowUpQuestions(options: {
117
117
  context: LongTableProjectContext;
118
118
  prompt: string;
119
119
  provider?: ProviderKind;
@@ -130,6 +130,9 @@ export declare function createWorkspaceQuestion(options: {
130
130
  prompt: string;
131
131
  title?: string;
132
132
  question?: string;
133
+ checkpointKey?: string;
134
+ questionOptions?: QuestionOption[];
135
+ displayReason?: string;
133
136
  provider?: ProviderKind;
134
137
  required?: boolean;
135
138
  }): Promise<{
@@ -647,10 +647,10 @@ function optionsForCheckpointTrigger(family, checkpointKey) {
647
647
  function includesAny(prompt, patterns) {
648
648
  return patterns.some((pattern) => pattern.test(prompt));
649
649
  }
650
- function clarificationOptions(first, second, third, fourth) {
650
+ function followUpQuestionOptions(first, second, third, fourth) {
651
651
  return [first, second, third, ...(fourth ? [fourth] : [])];
652
652
  }
653
- function buildClarificationQuestionSpecs(prompt) {
653
+ function buildFollowUpQuestionSpecs(prompt) {
654
654
  const normalized = prompt.toLowerCase();
655
655
  const specs = [];
656
656
  function push(spec) {
@@ -664,7 +664,7 @@ function buildClarificationQuestionSpecs(prompt) {
664
664
  title: "Rubric update basis",
665
665
  question: "How should LongTable use the available materials to update the rubric?",
666
666
  whyNow: "Rubric updates can silently change grading criteria if LongTable guesses the calibration basis.",
667
- options: clarificationOptions({ value: "calibrate_to_exemplars", label: "Calibrate criteria to exemplars", description: "Use strong submissions to refine what each criterion means.", recommended: true }, { value: "polish_existing", label: "Polish existing rubric only", description: "Keep criteria stable and improve wording or consistency." }, { value: "rewrite_structure", label: "Restructure the rubric", description: "Change categories or levels where the materials suggest a better structure." })
667
+ options: followUpQuestionOptions({ value: "calibrate_to_exemplars", label: "Calibrate criteria to exemplars", description: "Use strong submissions to refine what each criterion means.", recommended: true }, { value: "polish_existing", label: "Polish existing rubric only", description: "Keep criteria stable and improve wording or consistency." }, { value: "rewrite_structure", label: "Restructure the rubric", description: "Change categories or levels where the materials suggest a better structure." })
668
668
  });
669
669
  }
670
670
  if (includesAny(normalized, [/\bexemplar\b/, /\bbest submission\b/, /\bselected submission\b/, /\bTA\b/i, /우수\s*답안|예시|선정|조교/])) {
@@ -673,7 +673,7 @@ function buildClarificationQuestionSpecs(prompt) {
673
673
  title: "Exemplar use",
674
674
  question: "How should LongTable use selected exemplars or TA guidance?",
675
675
  whyNow: "Exemplars can either calibrate criteria privately or become visible evidence inside the output.",
676
- options: clarificationOptions({ value: "calibrate_only", label: "Use as private calibration", description: "Adjust criteria using exemplars without quoting them.", recommended: true }, { value: "include_deidentified_excerpts", label: "Include de-identified excerpts", description: "Add short anonymized examples where they clarify quality." }, { value: "separate_notes", label: "Keep examples in separate notes", description: "Use exemplars outside the main artifact." })
676
+ options: followUpQuestionOptions({ value: "calibrate_only", label: "Use as private calibration", description: "Adjust criteria using exemplars without quoting them.", recommended: true }, { value: "include_deidentified_excerpts", label: "Include de-identified excerpts", description: "Add short anonymized examples where they clarify quality." }, { value: "separate_notes", label: "Keep examples in separate notes", description: "Use exemplars outside the main artifact." })
677
677
  });
678
678
  }
679
679
  if (includesAny(normalized, [/\binstruction/, /\bguidance\b/, /\bsource\b/, /\bfile\b/, /\bdocx?\b/, /지침|가이드|문서|파일|자료/])) {
@@ -682,7 +682,7 @@ function buildClarificationQuestionSpecs(prompt) {
682
682
  title: "Source authority",
683
683
  question: "If sources conflict or leave gaps, which source should LongTable privilege?",
684
684
  whyNow: "Without an authority rule, LongTable may resolve conflicts by convenience rather than researcher intent.",
685
- options: clarificationOptions({ value: "explicit_user_instruction", label: "Your explicit instruction", description: "Use the researcher's current instruction as the highest authority.", recommended: true }, { value: "project_files", label: "Project files", description: "Treat supplied files or existing artifacts as authoritative." }, { value: "external_guidance", label: "TA or external guidance", description: "Prioritize instructor, TA, venue, or policy guidance." })
685
+ options: followUpQuestionOptions({ value: "explicit_user_instruction", label: "Your explicit instruction", description: "Use the researcher's current instruction as the highest authority.", recommended: true }, { value: "project_files", label: "Project files", description: "Treat supplied files or existing artifacts as authoritative." }, { value: "external_guidance", label: "TA or external guidance", description: "Prioritize instructor, TA, venue, or policy guidance." })
686
686
  });
687
687
  }
688
688
  if (includesAny(normalized, [/\bdeliver\b/, /\boutput\b/, /\btracked?[- ]?change/, /\bdocx?\b/, /\bmarkdown\b/, /\btable\b/, /전달|산출물|결과물|수정\s*표시|트랙|형식|포맷/])) {
@@ -691,7 +691,7 @@ function buildClarificationQuestionSpecs(prompt) {
691
691
  title: "Delivery format",
692
692
  question: "How should LongTable deliver the clarified output?",
693
693
  whyNow: "Format and change-tracking choices affect whether the result is usable for review or handoff.",
694
- options: clarificationOptions({ value: "tracked_changes", label: "Tracked-change artifact", description: "Produce a reviewable changed version where possible.", recommended: true }, { value: "clean_final", label: "Clean final artifact", description: "Deliver the final version without change markup." }, { value: "summary_plus_artifact", label: "Summary plus artifact", description: "Include a concise change summary with the output." })
694
+ options: followUpQuestionOptions({ value: "tracked_changes", label: "Tracked-change artifact", description: "Produce a reviewable changed version where possible.", recommended: true }, { value: "clean_final", label: "Clean final artifact", description: "Deliver the final version without change markup." }, { value: "summary_plus_artifact", label: "Summary plus artifact", description: "Include a concise change summary with the output." })
695
695
  });
696
696
  }
697
697
  if (includesAny(normalized, [/\bupdate\b/, /\bchange\b/, /\bedit\b/, /\bfix\b/, /\bimplement\b/, /\bbuild\b/, /\bcreate\b/, /업데이트|수정|변경|구현|만들|고쳐/])) {
@@ -700,7 +700,7 @@ function buildClarificationQuestionSpecs(prompt) {
700
700
  title: "Autonomy boundary",
701
701
  question: "How much should LongTable do before checking back with you?",
702
702
  whyNow: "Execution requests can move from advice to authorship or artifact ownership unless the boundary is explicit.",
703
- options: clarificationOptions({ value: "ask_then_act", label: "Clarify first, then act", description: "Ask needed questions before changing the artifact.", recommended: true }, { value: "act_with_defaults", label: "Act with visible defaults", description: "Proceed using recommended defaults and record them." }, { value: "recommend_only", label: "Recommend only", description: "Describe changes but do not alter artifacts." })
703
+ options: followUpQuestionOptions({ value: "ask_then_act", label: "Clarify first, then act", description: "Ask needed questions before changing the artifact.", recommended: true }, { value: "act_with_defaults", label: "Act with visible defaults", description: "Proceed using recommended defaults and record them." }, { value: "recommend_only", label: "Recommend only", description: "Describe changes but do not alter artifacts." })
704
704
  });
705
705
  }
706
706
  if (includesAny(normalized, [/\bperformance\b/, /\btest\b/, /\bevaluate\b/, /\bcheck\b/, /\bbenchmark\b/, /성능|테스트|평가|체크|검증/])) {
@@ -709,7 +709,7 @@ function buildClarificationQuestionSpecs(prompt) {
709
709
  title: "Evaluation target",
710
710
  question: "What should LongTable treat as the main performance target?",
711
711
  whyNow: "Performance checks can optimize for UX, correctness, trigger sensitivity, or delivery reliability.",
712
- options: clarificationOptions({ value: "question_sensitivity", label: "Question sensitivity", description: "Check whether LongTable asks at the right knowledge-gap moments.", recommended: true }, { value: "renderer_convenience", label: "Renderer convenience", description: "Check whether the most convenient question UI is used." }, { value: "state_reliability", label: "State reliability", description: "Check whether questions and answers persist correctly." })
712
+ options: followUpQuestionOptions({ value: "question_sensitivity", label: "Question sensitivity", description: "Check whether LongTable asks at the right knowledge-gap moments.", recommended: true }, { value: "renderer_convenience", label: "Renderer convenience", description: "Check whether the most convenient question UI is used." }, { value: "state_reliability", label: "State reliability", description: "Check whether questions and answers persist correctly." })
713
713
  });
714
714
  }
715
715
  if (specs.length === 0) {
@@ -718,19 +718,19 @@ function buildClarificationQuestionSpecs(prompt) {
718
718
  title: "Missing context",
719
719
  question: "What should LongTable clarify before proceeding?",
720
720
  whyNow: "The request can be answered in multiple ways, and choosing silently would hide a researcher judgment.",
721
- options: clarificationOptions({ value: "scope", label: "Clarify scope first", description: "Ask what is included and excluded before acting.", recommended: true }, { value: "criteria", label: "Clarify success criteria", description: "Ask what would count as a good result." }, { value: "proceed", label: "Proceed with visible assumptions", description: "Continue, but make assumptions explicit." })
721
+ options: followUpQuestionOptions({ value: "scope", label: "Clarify scope first", description: "Ask what is included and excluded before acting.", recommended: true }, { value: "criteria", label: "Clarify success criteria", description: "Ask what would count as a good result." }, { value: "proceed", label: "Proceed with visible assumptions", description: "Continue, but make assumptions explicit." })
722
722
  });
723
723
  }
724
724
  return specs;
725
725
  }
726
- const CLARIFICATION_PROMPT_PREFIX = "Clarification prompt:";
727
- function hasClarificationPrompt(record, prompt) {
728
- return record.prompt.rationale.includes(`${CLARIFICATION_PROMPT_PREFIX} ${prompt}`);
726
+ const FOLLOW_UP_PROMPT_PREFIX = "Follow-up prompt:";
727
+ function hasFollowUpPrompt(record, prompt) {
728
+ return record.prompt.rationale.includes(`${FOLLOW_UP_PROMPT_PREFIX} ${prompt}`);
729
729
  }
730
- export async function createWorkspaceClarificationCard(options) {
730
+ export async function createWorkspaceFollowUpQuestions(options) {
731
731
  const state = await loadResearchState(options.context.stateFilePath);
732
732
  if (!options.force) {
733
- const existing = (state.questionLog ?? []).filter((record) => hasClarificationPrompt(record, options.prompt));
733
+ const existing = (state.questionLog ?? []).filter((record) => hasFollowUpPrompt(record, options.prompt));
734
734
  const pending = existing.filter((record) => record.status === "pending");
735
735
  if (pending.length > 0) {
736
736
  return { questions: pending, state, created: false, alreadyAnswered: false };
@@ -743,14 +743,14 @@ export async function createWorkspaceClarificationCard(options) {
743
743
  const preferredSurfaces = options.provider === "claude"
744
744
  ? ["native_structured", "terminal_selector", "numbered"]
745
745
  : ["mcp_elicitation", "terminal_selector", "numbered"];
746
- const questions = buildClarificationQuestionSpecs(options.prompt).map((spec) => ({
746
+ const questions = buildFollowUpQuestionSpecs(options.prompt).map((spec) => ({
747
747
  id: createId("question_record"),
748
748
  createdAt,
749
749
  updatedAt: createdAt,
750
750
  status: "pending",
751
751
  prompt: {
752
752
  id: createId("question_prompt"),
753
- checkpointKey: `clarification_${spec.key}`,
753
+ checkpointKey: `follow_up_${spec.key}`,
754
754
  title: spec.title,
755
755
  question: spec.question,
756
756
  type: "single_choice",
@@ -761,7 +761,7 @@ export async function createWorkspaceClarificationCard(options) {
761
761
  source: "runtime_guidance",
762
762
  rationale: [
763
763
  spec.whyNow,
764
- `${CLARIFICATION_PROMPT_PREFIX} ${options.prompt}`
764
+ `${FOLLOW_UP_PROMPT_PREFIX} ${options.prompt}`
765
765
  ],
766
766
  preferredSurfaces: preferredSurfaces
767
767
  }
@@ -777,6 +777,7 @@ export async function createWorkspaceQuestion(options) {
777
777
  unresolvedTensions: state.openTensions ?? [],
778
778
  studyContract: state.studyContract
779
779
  });
780
+ const checkpointKey = options.checkpointKey ?? trigger.signal.checkpointKey;
780
781
  const createdAt = nowIso();
781
782
  const question = {
782
783
  id: createId("question_record"),
@@ -785,15 +786,16 @@ export async function createWorkspaceQuestion(options) {
785
786
  status: "pending",
786
787
  prompt: {
787
788
  id: createId("question_prompt"),
788
- checkpointKey: trigger.signal.checkpointKey,
789
- title: options.title ?? questionTitleForCheckpoint(trigger.family, trigger.signal.checkpointKey),
790
- question: options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, trigger.signal.checkpointKey),
789
+ checkpointKey,
790
+ title: options.title ?? questionTitleForCheckpoint(trigger.family, checkpointKey),
791
+ question: options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, checkpointKey),
791
792
  type: "single_choice",
792
- options: optionsForCheckpointTrigger(trigger.family, trigger.signal.checkpointKey),
793
+ options: options.questionOptions ?? optionsForCheckpointTrigger(trigger.family, checkpointKey),
793
794
  allowOther: true,
794
795
  otherLabel: "Other decision",
795
796
  required: options.required ?? trigger.requiresQuestionBeforeClosure,
796
797
  source: "checkpoint",
798
+ displayReason: options.displayReason ?? trigger.rationale[0],
797
799
  rationale: [
798
800
  ...trigger.rationale,
799
801
  `Trigger family: ${trigger.family}.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -28,12 +28,12 @@
28
28
  "typecheck": "tsc -p tsconfig.json --noEmit"
29
29
  },
30
30
  "dependencies": {
31
- "@longtable/checkpoints": "0.1.24",
32
- "@longtable/core": "0.1.24",
33
- "@longtable/memory": "0.1.24",
34
- "@longtable/provider-claude": "0.1.24",
35
- "@longtable/provider-codex": "0.1.24",
36
- "@longtable/setup": "0.1.24"
31
+ "@longtable/checkpoints": "0.1.26",
32
+ "@longtable/core": "0.1.26",
33
+ "@longtable/memory": "0.1.26",
34
+ "@longtable/provider-claude": "0.1.26",
35
+ "@longtable/provider-codex": "0.1.26",
36
+ "@longtable/setup": "0.1.26"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.1",