@longtable/cli 0.1.23 → 0.1.25

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
@@ -92,6 +92,7 @@ longtable ask --cwd "<project-path>" --prompt "..."
92
92
  longtable panel --prompt "..."
93
93
  longtable sentinel --prompt "Should I define a new measurement construct?"
94
94
  longtable hud --watch
95
+ longtable team --prompt "Review this measurement plan." --role editor,measurement_auditor --json
95
96
  longtable team --tmux --prompt "Review this measurement plan."
96
97
  longtable team --debate --prompt "Review this measurement plan." --role editor,measurement_auditor --json
97
98
  longtable codex install-skills
@@ -164,7 +165,7 @@ Default panel roles include:
164
165
 
165
166
  Use `--role` to constrain the panel when the research problem is already clear.
166
167
 
167
- ## Sentinel, HUD, And Tmux Team
168
+ ## Sentinel, HUD, And Agent Team
168
169
 
169
170
  `longtable sentinel` is an explicit gap/tacit check for prompts that may contain
170
171
  measurement, theory, method, evidence, authorship, or tacit-assumption risks.
@@ -175,16 +176,22 @@ unconfirmed inferred hypothesis.
175
176
  blocker, pending checkpoints, recent decisions, and invocation counts.
176
177
  `longtable hud --tmux` opens that view in a tmux pane.
177
178
 
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.
179
+ `longtable team` creates a file-backed agent-team review under
180
+ `.longtable/team/<id>/`: independent review, cross-review, and
181
+ synthesis/checkpoint. Use it when roles should inspect each other's concerns
182
+ before LongTable proposes a researcher decision.
183
+
184
+ `longtable team --tmux` opens role-specific panes for live discussion. The panes
185
+ can add logs, but the file-backed team artifact remains the source of truth.
182
186
 
183
187
  `longtable team --debate` creates a fixed five-round debate record under
184
188
  `.longtable/team/<id>/`: independent review, cross-review, rebuttal,
185
189
  convergence, and synthesis/checkpoint. Tmux can show live role panes, but the
186
190
  file-backed artifact directory is the source of truth.
187
191
 
192
+ See `docs/AGENT-TEAM-README.md` in the repository for a user-facing guide to
193
+ panel, team, debate, and tmux surfaces.
194
+
188
195
  ## Evidence And Search Direction
189
196
 
190
197
  LongTable should not behave like a generic web scraper. Research search should
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router
16
16
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
17
17
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
18
18
  import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceClarificationCard, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
- import { buildTeamDebate, renderTeamDebateSummary } from "./debate.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.23";
45
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.25";
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]>] [--tmux] [--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]",
@@ -2305,9 +2305,13 @@ async function runTeam(args) {
2305
2305
  if (!prompt) {
2306
2306
  throw new Error("A prompt is required.");
2307
2307
  }
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.");
2308
+ const isDebate = args.debate === true;
2309
+ const expectedRounds = isDebate ? 5 : 3;
2310
+ const rounds = typeof args.rounds === "string" ? Number(args.rounds) : expectedRounds;
2311
+ if (!Number.isInteger(rounds) || rounds !== expectedRounds) {
2312
+ throw new Error(isDebate
2313
+ ? "LongTable team debate v1 supports `--rounds 5` only."
2314
+ : "LongTable team v1 supports `--rounds 3` cross-review only.");
2311
2315
  }
2312
2316
  const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
2313
2317
  const projectContext = await loadProjectContextFromDirectory(workingDirectory);
@@ -2315,16 +2319,9 @@ async function runTeam(args) {
2315
2319
  await assertWorkspaceNotBlocked(projectContext);
2316
2320
  }
2317
2321
  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
2322
  const teamId = localId("team");
2326
2323
  const teamDir = join(workingDirectory, ".longtable", "team", teamId);
2327
- if (args.debate === true) {
2324
+ if (isDebate) {
2328
2325
  const debate = buildTeamDebate({
2329
2326
  teamId,
2330
2327
  teamDir,
@@ -2395,17 +2392,42 @@ async function runTeam(args) {
2395
2392
  console.log(`- checkpoint: ${debate.questionRecord.id}`);
2396
2393
  return;
2397
2394
  }
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");
2395
+ const team = buildTeamReview({
2396
+ teamId,
2397
+ teamDir,
2398
+ prompt: projectAware.prompt,
2399
+ roleFlag: typeof args.role === "string" ? args.role : undefined,
2400
+ provider: setup?.providerSelection.provider,
2401
+ visibility: "always_visible",
2402
+ roundCount: rounds,
2403
+ tmux: args.tmux === true
2404
+ });
2405
+ await writeTeamDebateArtifacts(team, teamDir, prompt);
2406
+ const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
2407
+ if (canRecordWorkspace) {
2408
+ await appendInvocationRecordToWorkspace(projectContext, team.invocationRecord, [team.questionRecord]);
2409
+ }
2401
2410
  if (args.json === true) {
2402
- console.log(JSON.stringify({ teamId, teamDir, plan: fallback.plan }, null, 2));
2411
+ console.log(JSON.stringify({
2412
+ teamId,
2413
+ teamDir,
2414
+ plan: team.plan,
2415
+ run: team.run,
2416
+ questionRecord: team.questionRecord,
2417
+ invocationRecord: team.invocationRecord,
2418
+ execution: {
2419
+ status: "completed",
2420
+ surface: team.run.surface,
2421
+ interactionDepth: team.run.interactionDepth,
2422
+ projectContextFound: projectAware.projectContextFound,
2423
+ invocationLogged: canRecordWorkspace
2424
+ }
2425
+ }, null, 2));
2403
2426
  return;
2404
2427
  }
2405
2428
  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.");
2429
+ console.log(renderTeamDebateSummary(team.run));
2430
+ console.log(`- checkpoint: ${team.questionRecord.id}`);
2409
2431
  return;
2410
2432
  }
2411
2433
  const sessionName = `longtable-${teamId.replaceAll("_", "-")}`;
@@ -2413,13 +2435,13 @@ async function runTeam(args) {
2413
2435
  const launcher = process.argv[1] ?? "longtable";
2414
2436
  const leaderCommand = [
2415
2437
  `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)}`)}`,
2438
+ `echo ${shellEscape(`Artifacts: ${teamDir}`)}`,
2439
+ `echo ${shellEscape("Cross-review artifacts are recorded. Role panes can add live review logs.")}`,
2440
+ `echo ${shellEscape(`Checkpoint: ${team.questionRecord.id}`)}`,
2419
2441
  `exec ${shellEscape(shell)}`
2420
2442
  ].join("; ");
2421
2443
  execFileSync("tmux", ["new-session", "-d", "-s", sessionName, "-c", workingDirectory, leaderCommand], { stdio: "inherit" });
2422
- for (const member of fallback.plan.members) {
2444
+ for (const member of team.plan.members) {
2423
2445
  const rolePrompt = [
2424
2446
  `LongTable team discussion role: ${member.label} (${member.role}).`,
2425
2447
  "Give claims, objections, open questions, and evidence needs. Address likely disagreement with other roles.",
package/dist/debate.d.ts CHANGED
@@ -17,5 +17,6 @@ export interface TeamDebateBundle {
17
17
  questionRecord: QuestionRecord;
18
18
  }
19
19
  export declare function createTeamDebateQuestionRecord(run: TeamDebateRun, provider?: ProviderKind): QuestionRecord;
20
+ export declare function buildTeamReview(options: BuildTeamDebateOptions): TeamDebateBundle;
20
21
  export declare function buildTeamDebate(options: BuildTeamDebateOptions): TeamDebateBundle;
21
22
  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,21 +234,29 @@ 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"]
239
- : ["numbered", "native_structured"]
249
+ : ["mcp_elicitation", "numbered"]
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,
@@ -307,14 +323,15 @@ export function buildTeamDebate(options) {
307
323
  roles: plan.members,
308
324
  status: "completed",
309
325
  surface: options.tmux ? "tmux_console" : "file_backed_debate",
310
- roundPolicy: "fixed",
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,6 +368,7 @@ 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
374
  degradationReason: options.tmux
@@ -363,12 +383,19 @@ export function buildTeamDebate(options) {
363
383
  questionRecord
364
384
  };
365
385
  }
386
+ export function buildTeamReview(options) {
387
+ return buildTeamBundle(options, "team");
388
+ }
389
+ export function buildTeamDebate(options) {
390
+ return buildTeamBundle(options, "debate");
391
+ }
366
392
  export function renderTeamDebateSummary(run) {
367
393
  return [
368
394
  "LongTable Team Debate",
369
395
  `- team: ${run.teamId}`,
370
396
  `- surface: ${run.surface}`,
371
- `- rounds: ${run.roundCount} fixed`,
397
+ `- interaction depth: ${run.interactionDepth}`,
398
+ `- rounds: ${run.roundCount} ${run.roundPolicy}`,
372
399
  `- roles: ${run.roles.map((role) => `${role.label} (${role.role})`).join(", ")}`,
373
400
  `- artifacts: ${run.artifactRoot}`,
374
401
  "",
package/dist/panel.js CHANGED
@@ -143,9 +143,10 @@ 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
- : ["numbered", "native_structured"]
149
+ : ["mcp_elicitation", "numbered"]
149
150
  }
150
151
  };
151
152
  }
@@ -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."
@@ -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 {
@@ -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<{
@@ -473,6 +473,9 @@ function questionTitleForCheckpoint(family, checkpointKey) {
473
473
  if (checkpointKey === "tacit_assumption_probe") {
474
474
  return "Tacit-assumption checkpoint";
475
475
  }
476
+ if (checkpointKey === "panel_disagreement_resolution") {
477
+ return "Panel-disagreement checkpoint";
478
+ }
476
479
  switch (family) {
477
480
  case "meta_decision":
478
481
  return "Meta-decision checkpoint";
@@ -499,6 +502,9 @@ function questionTextForCheckpoint(family, prompt, checkpointKey) {
499
502
  if (checkpointKey === "tacit_assumption_probe") {
500
503
  return "What tacit assumption should LongTable surface before treating this direction as acceptable?";
501
504
  }
505
+ if (checkpointKey === "panel_disagreement_resolution") {
506
+ return "How should LongTable handle the unresolved panel disagreement before turning it into one synthesis?";
507
+ }
502
508
  switch (family) {
503
509
  case "meta_decision":
504
510
  return "What should LongTable do before treating this platform decision as settled?";
@@ -533,6 +539,14 @@ function optionsForCheckpointTrigger(family, checkpointKey) {
533
539
  { value: "defer", label: "Keep the assumption open", description: "Do not treat this framing as settled yet." }
534
540
  ];
535
541
  }
542
+ if (checkpointKey === "panel_disagreement_resolution") {
543
+ return [
544
+ { value: "surface_disagreement", label: "Surface disagreement first", description: "Show the role conflict before choosing a synthesis." },
545
+ { value: "compare_frames", label: "Compare candidate framings", description: "Keep alternatives visible before selecting one frame." },
546
+ { value: "proceed_with_trace", label: "Proceed with an explicit trace", description: "Choose a synthesis, but record the unresolved disagreement." },
547
+ { value: "defer", label: "Keep disagreement open", description: "Do not collapse the disagreement into one conclusion yet." }
548
+ ];
549
+ }
536
550
  if (family === "evidence") {
537
551
  return [
538
552
  { value: "verify", label: "Verify evidence first", description: "Check whether the source supports the specific claim." },
@@ -728,7 +742,7 @@ export async function createWorkspaceClarificationCard(options) {
728
742
  const createdAt = nowIso();
729
743
  const preferredSurfaces = options.provider === "claude"
730
744
  ? ["native_structured", "terminal_selector", "numbered"]
731
- : ["terminal_selector", "numbered", "native_structured"];
745
+ : ["mcp_elicitation", "terminal_selector", "numbered"];
732
746
  const questions = buildClarificationQuestionSpecs(options.prompt).map((spec) => ({
733
747
  id: createId("question_record"),
734
748
  createdAt,
@@ -763,6 +777,7 @@ export async function createWorkspaceQuestion(options) {
763
777
  unresolvedTensions: state.openTensions ?? [],
764
778
  studyContract: state.studyContract
765
779
  });
780
+ const checkpointKey = options.checkpointKey ?? trigger.signal.checkpointKey;
766
781
  const createdAt = nowIso();
767
782
  const question = {
768
783
  id: createId("question_record"),
@@ -771,15 +786,16 @@ export async function createWorkspaceQuestion(options) {
771
786
  status: "pending",
772
787
  prompt: {
773
788
  id: createId("question_prompt"),
774
- checkpointKey: trigger.signal.checkpointKey,
775
- title: options.title ?? questionTitleForCheckpoint(trigger.family, trigger.signal.checkpointKey),
776
- 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),
777
792
  type: "single_choice",
778
- options: optionsForCheckpointTrigger(trigger.family, trigger.signal.checkpointKey),
793
+ options: options.questionOptions ?? optionsForCheckpointTrigger(trigger.family, checkpointKey),
779
794
  allowOther: true,
780
795
  otherLabel: "Other decision",
781
796
  required: options.required ?? trigger.requiresQuestionBeforeClosure,
782
797
  source: "checkpoint",
798
+ displayReason: options.displayReason ?? trigger.rationale[0],
783
799
  rationale: [
784
800
  ...trigger.rationale,
785
801
  `Trigger family: ${trigger.family}.`,
@@ -788,7 +804,7 @@ export async function createWorkspaceQuestion(options) {
788
804
  ],
789
805
  preferredSurfaces: options.provider === "claude"
790
806
  ? ["native_structured", "numbered"]
791
- : ["numbered", "native_structured"]
807
+ : ["mcp_elicitation", "numbered"]
792
808
  }
793
809
  };
794
810
  const updated = appendQuestionRecords(state, [question]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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.23",
32
- "@longtable/core": "0.1.23",
33
- "@longtable/memory": "0.1.23",
34
- "@longtable/provider-claude": "0.1.23",
35
- "@longtable/provider-codex": "0.1.23",
36
- "@longtable/setup": "0.1.23"
31
+ "@longtable/checkpoints": "0.1.25",
32
+ "@longtable/core": "0.1.25",
33
+ "@longtable/memory": "0.1.25",
34
+ "@longtable/provider-claude": "0.1.25",
35
+ "@longtable/provider-codex": "0.1.25",
36
+ "@longtable/setup": "0.1.25"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.1",