@longtable/cli 0.1.16 → 0.1.17

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
@@ -94,6 +94,7 @@ longtable panel --prompt "..."
94
94
  longtable sentinel --prompt "Should I define a new measurement construct?"
95
95
  longtable hud --watch
96
96
  longtable team --tmux --prompt "Review this measurement plan."
97
+ longtable team --debate --prompt "Review this measurement plan." --role editor,measurement_auditor --json
97
98
  longtable codex install-skills
98
99
  longtable claude install-skills
99
100
  ```
@@ -180,6 +181,11 @@ writes logs under `.longtable/team/<id>/`. This is panel discussion, not merely
180
181
  parallel execution: role panes are prompted to state claims, objections, open
181
182
  questions, and likely disagreement.
182
183
 
184
+ `longtable team --debate` creates a fixed five-round debate record under
185
+ `.longtable/team/<id>/`: independent review, cross-review, rebuttal,
186
+ convergence, and synthesis/checkpoint. Tmux can show live role panes, but the
187
+ file-backed artifact directory is the source of truth.
188
+
183
189
  ## Evidence And Search Direction
184
190
 
185
191
  LongTable should not behave like a generic web scraper. Research search should
package/dist/cli.js CHANGED
@@ -16,6 +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
20
  const VALID_MODES = new Set([
20
21
  "explore",
21
22
  "review",
@@ -41,7 +42,7 @@ const ANSI = {
41
42
  green: "\u001B[32m"
42
43
  };
43
44
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
44
- const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.14";
45
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.17";
45
46
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
46
47
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
47
48
  function style(text, prefix) {
@@ -89,7 +90,7 @@ function usage() {
89
90
  " longtable mcp install [--provider codex|claude|all] [--write] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
90
91
  " longtable hud [--watch] [--tmux] [--preset minimal|full] [--cwd <path>] [--json]",
91
92
  " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
92
- " longtable team --prompt <text> [--role <role[,role]>] [--tmux] [--cwd <path>] [--json]",
93
+ " longtable team --prompt <text> [--role <role[,role]>] [--tmux] [--debate] [--rounds 5] [--cwd <path>] [--json]",
93
94
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
94
95
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
95
96
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
@@ -1561,9 +1562,6 @@ async function runModeCommand(mode, args) {
1561
1562
  }
1562
1563
  const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
1563
1564
  const projectContext = await loadProjectContextFromDirectory(workingDirectory);
1564
- if (projectContext) {
1565
- await assertWorkspaceNotBlocked(projectContext);
1566
- }
1567
1565
  const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
1568
1566
  const panelPreference = setup?.profileSeed.panelPreference;
1569
1567
  const panelRequested = args.panel === true ||
@@ -1947,6 +1945,25 @@ function localId(prefix) {
1947
1945
  function shellEscape(value) {
1948
1946
  return `'${value.replaceAll("'", "'\\''")}'`;
1949
1947
  }
1948
+ async function writeJsonFile(path, value) {
1949
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1950
+ }
1951
+ async function writeTeamDebateArtifacts(bundle, teamDir, prompt) {
1952
+ await mkdir(teamDir, { recursive: true });
1953
+ await writeFile(join(teamDir, "prompt.txt"), prompt, "utf8");
1954
+ await writeJsonFile(join(teamDir, "plan.json"), bundle.plan);
1955
+ await writeJsonFile(join(teamDir, "run.json"), bundle.run);
1956
+ for (const round of bundle.run.rounds) {
1957
+ await mkdir(round.artifactDir, { recursive: true });
1958
+ await writeJsonFile(join(round.artifactDir, "round.json"), round);
1959
+ for (const contribution of round.contributions) {
1960
+ await writeJsonFile(join(teamDir, contribution.artifactPath), contribution);
1961
+ }
1962
+ }
1963
+ await writeJsonFile(join(teamDir, "synthesis.json"), bundle.run.synthesis);
1964
+ await writeJsonFile(join(teamDir, "checkpoint.json"), bundle.questionRecord);
1965
+ await writeJsonFile(join(teamDir, "invocation.json"), bundle.invocationRecord);
1966
+ }
1950
1967
  function sentinelSummary(prompt, workingDirectory) {
1951
1968
  const trigger = classifyCheckpointTrigger(prompt, {
1952
1969
  fallbackMode: "explore",
@@ -2084,15 +2101,96 @@ async function runTeam(args) {
2084
2101
  if (!prompt) {
2085
2102
  throw new Error("A prompt is required.");
2086
2103
  }
2104
+ const rounds = typeof args.rounds === "string" ? Number(args.rounds) : 5;
2105
+ if (!Number.isInteger(rounds) || rounds !== 5) {
2106
+ throw new Error("LongTable team debate v1 supports `--rounds 5` only.");
2107
+ }
2108
+ const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
2109
+ const projectContext = await loadProjectContextFromDirectory(workingDirectory);
2110
+ if (projectContext) {
2111
+ await assertWorkspaceNotBlocked(projectContext);
2112
+ }
2113
+ const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
2087
2114
  const fallback = buildPanelFallback({
2088
2115
  prompt,
2089
2116
  mode: "review",
2090
2117
  roleFlag: typeof args.role === "string" ? args.role : undefined,
2091
- provider: "codex",
2118
+ provider: setup?.providerSelection.provider,
2092
2119
  visibility: "always_visible"
2093
2120
  });
2094
2121
  const teamId = localId("team");
2095
2122
  const teamDir = join(workingDirectory, ".longtable", "team", teamId);
2123
+ if (args.debate === true) {
2124
+ const debate = buildTeamDebate({
2125
+ teamId,
2126
+ teamDir,
2127
+ prompt: projectAware.prompt,
2128
+ roleFlag: typeof args.role === "string" ? args.role : undefined,
2129
+ provider: setup?.providerSelection.provider,
2130
+ visibility: "always_visible",
2131
+ roundCount: rounds,
2132
+ tmux: args.tmux === true
2133
+ });
2134
+ await writeTeamDebateArtifacts(debate, teamDir, prompt);
2135
+ const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
2136
+ if (canRecordWorkspace) {
2137
+ await appendInvocationRecordToWorkspace(projectContext, debate.invocationRecord, [debate.questionRecord]);
2138
+ }
2139
+ if (args.json === true) {
2140
+ console.log(JSON.stringify({
2141
+ teamId,
2142
+ teamDir,
2143
+ plan: debate.plan,
2144
+ run: debate.run,
2145
+ questionRecord: debate.questionRecord,
2146
+ invocationRecord: debate.invocationRecord,
2147
+ execution: {
2148
+ status: "completed",
2149
+ surface: debate.run.surface,
2150
+ projectContextFound: projectAware.projectContextFound,
2151
+ invocationLogged: canRecordWorkspace
2152
+ }
2153
+ }, null, 2));
2154
+ return;
2155
+ }
2156
+ if (args.tmux === true) {
2157
+ const sessionName = `longtable-${teamId.replaceAll("_", "-")}`;
2158
+ const shell = process.env.SHELL || "/bin/sh";
2159
+ const launcher = process.argv[1] ?? "longtable";
2160
+ const leaderCommand = [
2161
+ `echo ${shellEscape(`LongTable debate ${teamId}`)}`,
2162
+ `echo ${shellEscape(`Artifacts: ${teamDir}`)}`,
2163
+ `echo ${shellEscape("Fixed rounds are recorded. Role panes can add live review logs.")}`,
2164
+ `echo ${shellEscape(`Checkpoint: ${debate.questionRecord.id}`)}`,
2165
+ `exec ${shellEscape(shell)}`
2166
+ ].join("; ");
2167
+ execFileSync("tmux", ["new-session", "-d", "-s", sessionName, "-c", workingDirectory, leaderCommand], { stdio: "inherit" });
2168
+ for (const member of debate.plan.members) {
2169
+ const rolePrompt = [
2170
+ `LongTable autonomous debate role: ${member.label} (${member.role}).`,
2171
+ "Use the fixed debate artifacts as the shared record. Add live notes only; do not answer the researcher checkpoint.",
2172
+ `Artifacts: ${teamDir}`,
2173
+ "",
2174
+ projectAware.prompt
2175
+ ].join("\n");
2176
+ const logPath = join(teamDir, `${member.role}.debate.log`);
2177
+ const command = [
2178
+ `node ${shellEscape(launcher)} review --role ${shellEscape(member.role)} --prompt ${shellEscape(rolePrompt)} --cwd ${shellEscape(workingDirectory)} 2>&1 | tee ${shellEscape(logPath)}`,
2179
+ `echo ${shellEscape(`Debate role log written to ${logPath}`)}`,
2180
+ `exec ${shellEscape(shell)}`
2181
+ ].join("; ");
2182
+ execFileSync("tmux", ["split-window", "-t", sessionName, "-c", workingDirectory, command], { stdio: "inherit" });
2183
+ execFileSync("tmux", ["select-layout", "-t", sessionName, "tiled"], { stdio: "ignore" });
2184
+ }
2185
+ console.log(`LongTable tmux debate launched: ${sessionName}`);
2186
+ console.log(`Attach with: tmux attach -t ${sessionName}`);
2187
+ console.log(`Artifacts: ${teamDir}`);
2188
+ return;
2189
+ }
2190
+ console.log(renderTeamDebateSummary(debate.run));
2191
+ console.log(`- checkpoint: ${debate.questionRecord.id}`);
2192
+ return;
2193
+ }
2096
2194
  await mkdir(teamDir, { recursive: true });
2097
2195
  await writeFile(join(teamDir, "prompt.txt"), prompt, "utf8");
2098
2196
  await writeFile(join(teamDir, "plan.json"), JSON.stringify(fallback.plan, null, 2), "utf8");
@@ -0,0 +1,21 @@
1
+ import type { InvocationIntent, InvocationRecord, PanelPlan, PanelVisibility, ProviderKind, QuestionRecord, TeamDebateRun } from "@longtable/core";
2
+ export interface BuildTeamDebateOptions {
3
+ teamId: string;
4
+ teamDir: string;
5
+ prompt: string;
6
+ roleFlag?: string;
7
+ provider?: ProviderKind;
8
+ visibility?: PanelVisibility;
9
+ roundCount?: number;
10
+ tmux?: boolean;
11
+ }
12
+ export interface TeamDebateBundle {
13
+ plan: PanelPlan;
14
+ run: TeamDebateRun;
15
+ intent: InvocationIntent;
16
+ invocationRecord: InvocationRecord;
17
+ questionRecord: QuestionRecord;
18
+ }
19
+ export declare function createTeamDebateQuestionRecord(run: TeamDebateRun, provider?: ProviderKind): QuestionRecord;
20
+ export declare function buildTeamDebate(options: BuildTeamDebateOptions): TeamDebateBundle;
21
+ export declare function renderTeamDebateSummary(run: TeamDebateRun): string;
package/dist/debate.js ADDED
@@ -0,0 +1,380 @@
1
+ import { join } from "node:path";
2
+ import { buildInvocationIntent, buildPanelPlan } from "./panel.js";
3
+ import { getPersonaDefinition, parsePersonaKey } from "./personas.js";
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+ function createId(prefix) {
8
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
9
+ }
10
+ function signalTags(prompt) {
11
+ const normalized = prompt.toLowerCase();
12
+ const tags = [];
13
+ if (/\bmeasure|\bscale|\bvalid|reliab|측정|척도|타당|신뢰/.test(normalized))
14
+ tags.push("measurement");
15
+ if (/\btheor|\bconstruct|\bconcept|이론|개념|구성개념/.test(normalized))
16
+ tags.push("theory");
17
+ if (/\bmethod|\bdesign|\bsample|\banalysis|방법|설계|분석|표본/.test(normalized))
18
+ tags.push("method");
19
+ if (/\bevidence|\bcitation|\breference|근거|인용|문헌/.test(normalized))
20
+ tags.push("evidence");
21
+ if (/\bethic|\birb|consent|윤리|동의/.test(normalized))
22
+ tags.push("ethics");
23
+ if (/\bvoice|\bauthor|narrative|저자|서사|문체/.test(normalized))
24
+ tags.push("authorship");
25
+ return tags.length > 0 ? tags : ["general"];
26
+ }
27
+ function roleFocus(role) {
28
+ const key = parsePersonaKey(role);
29
+ return key ? getPersonaDefinition(key).shortDescription : "Inspect the research decision for hidden risk.";
30
+ }
31
+ function readableSignals(tags) {
32
+ return tags.join(", ");
33
+ }
34
+ function contribution(options) {
35
+ return {
36
+ id: createId("team_contribution"),
37
+ ...options
38
+ };
39
+ }
40
+ function independentContribution(roundId, plan, role, label, artifactPath) {
41
+ const tags = signalTags(plan.prompt);
42
+ return contribution({
43
+ roundId,
44
+ role,
45
+ label,
46
+ artifactPath,
47
+ summary: `${label} independently reviews the prompt for ${readableSignals(tags)} risks before any shared synthesis.`,
48
+ claims: [
49
+ `${label} should preserve a separate judgment lane rather than accepting the first synthesis.`,
50
+ `Primary role focus: ${roleFocus(role)}`
51
+ ],
52
+ objections: [
53
+ "The prompt may hide a commitment that has not yet been named by the researcher.",
54
+ "A fluent answer could collapse measurement, theory, evidence, and authorship concerns into one premature recommendation."
55
+ ],
56
+ openQuestions: [
57
+ "Which decision would become hard to reverse if LongTable proceeded now?",
58
+ "What evidence or researcher preference is still missing?"
59
+ ],
60
+ evidenceNeeds: [
61
+ "Local project state, manuscript/data references, or cited literature should be checked before closure.",
62
+ "If the prompt makes an external claim, attach source links or mark the claim as inference."
63
+ ],
64
+ tacitAssumptions: [
65
+ "The researcher may know constraints not present in the prompt.",
66
+ "The role's critique may over-weight its own domain unless challenged by other roles."
67
+ ],
68
+ checkpointTriggers: tags.map((tag) => `${tag}_commitment`)
69
+ });
70
+ }
71
+ function crossReviewContribution(roundId, plan, role, label, targetRole, targetLabel, artifactPath) {
72
+ return contribution({
73
+ roundId,
74
+ role,
75
+ label,
76
+ targetRole,
77
+ artifactPath,
78
+ summary: `${label} challenges ${targetLabel}'s likely blind spot before synthesis.`,
79
+ claims: [
80
+ `${targetLabel}'s concern is useful only if it does not erase ${label}'s domain-specific risk.`,
81
+ "The debate should expose disagreement as a researcher decision point rather than normalize it away."
82
+ ],
83
+ objections: [
84
+ `${targetLabel} may be treating its own role priority as the main research problem.`,
85
+ "The prompt still needs a concrete next decision, not only a list of concerns."
86
+ ],
87
+ openQuestions: [
88
+ `What would make ${targetLabel}'s objection decisive rather than advisory?`,
89
+ `What must the researcher answer before ${label} can accept ${targetLabel}'s framing?`
90
+ ],
91
+ evidenceNeeds: [
92
+ "Compare role objections against project state and available artifacts.",
93
+ "Separate source-backed objections from inferred risk."
94
+ ],
95
+ tacitAssumptions: [
96
+ "Roles may disagree because they are protecting different commitments.",
97
+ "The absence of evidence in the prompt is not evidence that the researcher lacks it."
98
+ ],
99
+ checkpointTriggers: ["role_disagreement", "unresolved_gap"]
100
+ });
101
+ }
102
+ function rebuttalContribution(roundId, role, label, artifactPath) {
103
+ return contribution({
104
+ roundId,
105
+ role,
106
+ label,
107
+ artifactPath,
108
+ summary: `${label} revises its position after cross-review while preserving unresolved risk.`,
109
+ claims: [
110
+ "Some objections should be accepted as constraints, not treated as blockers.",
111
+ "The final synthesis should distinguish actionable next steps from background concern."
112
+ ],
113
+ objections: [
114
+ "A role may over-correct after critique and lose the original high-stakes warning.",
115
+ "Convergence without a checkpoint would turn debate into hidden automation."
116
+ ],
117
+ openQuestions: [
118
+ "Which objection changes the next action?",
119
+ "Which unresolved disagreement should remain visible to the researcher?"
120
+ ],
121
+ evidenceNeeds: [
122
+ "Record which objections were accepted, rejected, or deferred.",
123
+ "Preserve references needed to verify the most consequential claim."
124
+ ],
125
+ tacitAssumptions: [
126
+ "A clean synthesis may be less honest than a visible unresolved tension.",
127
+ "The researcher should own the final prioritization."
128
+ ],
129
+ checkpointTriggers: ["synthesis_boundary", "researcher_authority"]
130
+ });
131
+ }
132
+ function convergenceContribution(roundId, plan, role, label, artifactPath) {
133
+ const otherRoles = plan.members.filter((member) => member.role !== role).map((member) => member.label);
134
+ return contribution({
135
+ roundId,
136
+ role,
137
+ label,
138
+ artifactPath,
139
+ summary: `${label} states what it can accept and what must remain open.`,
140
+ claims: [
141
+ `Can accept synthesis if it preserves ${label}'s domain warning.`,
142
+ `Must still show disagreement with: ${otherRoles.join(", ") || "no other roles"}.`
143
+ ],
144
+ objections: [
145
+ "Do not convert unresolved disagreement into a single confident recommendation.",
146
+ "Do not let the coordinator answer the checkpoint on behalf of the researcher."
147
+ ],
148
+ openQuestions: [
149
+ "Which path should LongTable recommend as the next researcher decision?",
150
+ "Which issue should be logged as an open tension if not answered now?"
151
+ ],
152
+ evidenceNeeds: [
153
+ "Link the synthesis back to role contributions and local artifacts.",
154
+ "Mark any missing sources as evidence gaps."
155
+ ],
156
+ tacitAssumptions: [
157
+ "A role's agreement may be conditional.",
158
+ "The researcher may choose to proceed despite unresolved risk."
159
+ ],
160
+ checkpointTriggers: ["panel_next_decision"]
161
+ });
162
+ }
163
+ function buildSynthesis(plan, artifactPath) {
164
+ const labels = plan.members.map((member) => member.label);
165
+ const highSensitivity = plan.checkpointSensitivity === "high";
166
+ return {
167
+ 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.`,
169
+ consensus: [
170
+ "The researcher should see role disagreement before LongTable drafts, commits, or submits a conclusion.",
171
+ "Evidence gaps and tacit assumptions should remain visible instead of being smoothed into fluent prose."
172
+ ],
173
+ disagreements: [
174
+ "Roles may prioritize different first moves: theory framing, method defensibility, measurement validity, evidence verification, or authorship trace.",
175
+ "The coordinator should not decide which role wins without a researcher checkpoint."
176
+ ],
177
+ unresolvedGaps: [
178
+ "Which role concern is decisive for the next action?",
179
+ "What source, data, or local project artifact should be checked before closure?"
180
+ ],
181
+ researcherDecisionPoints: [
182
+ "Prioritize revision, evidence gathering, proceeding with risk, or keeping the issue open.",
183
+ "Choose whether the debate should affect the current artifact, the research design, or only the decision log."
184
+ ],
185
+ 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?"
188
+ };
189
+ }
190
+ export function createTeamDebateQuestionRecord(run, provider) {
191
+ const createdAt = nowIso();
192
+ return {
193
+ id: createId("question_record"),
194
+ createdAt,
195
+ updatedAt: createdAt,
196
+ status: "pending",
197
+ prompt: {
198
+ id: createId("question_prompt"),
199
+ checkpointKey: "team_debate_next_decision",
200
+ title: "Team debate follow-up decision",
201
+ question: run.synthesis.recommendedCheckpoint,
202
+ type: "single_choice",
203
+ options: [
204
+ {
205
+ value: "revise",
206
+ label: "Revise before proceeding",
207
+ description: "Use the debate result to revise the claim, design, or draft first."
208
+ },
209
+ {
210
+ value: "evidence",
211
+ label: "Gather or verify evidence first",
212
+ description: "Check source, data, or local artifact support before proceeding."
213
+ },
214
+ {
215
+ value: "proceed",
216
+ label: "Proceed with current direction",
217
+ description: "Accept the risk profile and continue with the current direction."
218
+ },
219
+ {
220
+ value: "defer",
221
+ label: "Keep this open",
222
+ description: "Do not commit yet; keep the debate issue visible as an open tension."
223
+ }
224
+ ],
225
+ allowOther: true,
226
+ otherLabel: "Other decision",
227
+ required: run.roles.some((member) => {
228
+ const key = parsePersonaKey(member.role);
229
+ return key ? getPersonaDefinition(key).checkpointSensitivity === "high" : false;
230
+ }),
231
+ source: "runtime_guidance",
232
+ 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}.`
236
+ ],
237
+ preferredSurfaces: provider === "claude"
238
+ ? ["native_structured", "numbered"]
239
+ : ["numbered", "native_structured"]
240
+ }
241
+ };
242
+ }
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.");
247
+ }
248
+ const createdAt = nowIso();
249
+ const plan = buildPanelPlan({
250
+ prompt: options.prompt,
251
+ mode: "review",
252
+ roleFlag: options.roleFlag,
253
+ provider: options.provider,
254
+ visibility: options.visibility ?? "always_visible"
255
+ });
256
+ const rounds = [];
257
+ const round1Id = createId("team_round");
258
+ rounds.push({
259
+ id: round1Id,
260
+ index: 1,
261
+ kind: "independent_review",
262
+ title: "Independent review",
263
+ status: "completed",
264
+ 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`)))
266
+ });
267
+ const round2Id = createId("team_round");
268
+ const crossContributions = plan.members.flatMap((member) => plan.members
269
+ .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`))));
271
+ rounds.push({
272
+ id: round2Id,
273
+ index: 2,
274
+ kind: "cross_review",
275
+ title: "Cross-review",
276
+ status: "completed",
277
+ artifactDir: join(options.teamDir, "round-2-cross-review"),
278
+ contributions: crossContributions
279
+ });
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");
301
+ const run = {
302
+ id: createId("team_debate_run"),
303
+ teamId: options.teamId,
304
+ createdAt,
305
+ updatedAt: createdAt,
306
+ prompt: options.prompt,
307
+ roles: plan.members,
308
+ status: "completed",
309
+ surface: options.tmux ? "tmux_console" : "file_backed_debate",
310
+ roundPolicy: "fixed",
311
+ roundCount,
312
+ artifactRoot: options.teamDir,
313
+ rounds: [
314
+ ...rounds,
315
+ {
316
+ id: createId("team_round"),
317
+ index: 5,
318
+ kind: "synthesis",
319
+ title: "Coordinator synthesis and checkpoint",
320
+ status: "completed",
321
+ artifactDir: options.teamDir,
322
+ contributions: []
323
+ }
324
+ ],
325
+ synthesis,
326
+ linkedQuestionRecordIds: []
327
+ };
328
+ const questionRecord = createTeamDebateQuestionRecord(run, options.provider);
329
+ run.linkedQuestionRecordIds = [questionRecord.id];
330
+ const intent = buildInvocationIntent({
331
+ prompt: options.prompt,
332
+ mode: "review",
333
+ roles: plan.members.map((member) => member.role),
334
+ provider: options.provider,
335
+ visibility: plan.visibility,
336
+ checkpointSensitivity: plan.checkpointSensitivity,
337
+ rationale: [
338
+ "Autonomous debate requested through LongTable team orchestration.",
339
+ "File-backed fixed rounds keep disagreement inspectable before researcher closure."
340
+ ]
341
+ });
342
+ intent.kind = "team_debate";
343
+ intent.requestedSurface = run.surface;
344
+ const invocationRecord = {
345
+ id: createId("invocation_record"),
346
+ createdAt,
347
+ updatedAt: createdAt,
348
+ intent,
349
+ status: "completed",
350
+ provider: options.provider,
351
+ surface: run.surface,
352
+ panelPlan: plan,
353
+ teamDebateRun: run,
354
+ degradationReason: options.tmux
355
+ ? undefined
356
+ : "Tmux was not requested; file-backed debate artifacts are the canonical execution record."
357
+ };
358
+ return {
359
+ plan,
360
+ run,
361
+ intent,
362
+ invocationRecord,
363
+ questionRecord
364
+ };
365
+ }
366
+ export function renderTeamDebateSummary(run) {
367
+ return [
368
+ "LongTable Team Debate",
369
+ `- team: ${run.teamId}`,
370
+ `- surface: ${run.surface}`,
371
+ `- rounds: ${run.roundCount} fixed`,
372
+ `- roles: ${run.roles.map((role) => `${role.label} (${role.role})`).join(", ")}`,
373
+ `- artifacts: ${run.artifactRoot}`,
374
+ "",
375
+ run.synthesis.summary,
376
+ "",
377
+ "Researcher checkpoint:",
378
+ `- ${run.synthesis.recommendedCheckpoint}`
379
+ ].join("\n");
380
+ }
@@ -893,6 +893,7 @@ export async function createOrUpdateProjectWorkspace(options) {
893
893
  const project = existsSync(projectFilePath)
894
894
  ? {
895
895
  ...JSON.parse(await readFile(projectFilePath, "utf8")),
896
+ projectPath,
896
897
  contractVersion: "workspace-v2",
897
898
  locale
898
899
  }
@@ -992,8 +993,15 @@ export async function loadProjectContextFromDirectory(startPath) {
992
993
  const projectFilePath = join(metaDir, "project.json");
993
994
  const sessionFilePath = join(metaDir, "current-session.json");
994
995
  if (existsSync(projectFilePath) && existsSync(sessionFilePath)) {
995
- const project = JSON.parse(await readFile(projectFilePath, "utf8"));
996
- const session = JSON.parse(await readFile(sessionFilePath, "utf8"));
996
+ const workspaceRoot = current;
997
+ const project = {
998
+ ...JSON.parse(await readFile(projectFilePath, "utf8")),
999
+ projectPath: workspaceRoot
1000
+ };
1001
+ const session = {
1002
+ ...JSON.parse(await readFile(sessionFilePath, "utf8")),
1003
+ projectPath: workspaceRoot
1004
+ };
997
1005
  return {
998
1006
  project,
999
1007
  session: {
@@ -1006,8 +1014,8 @@ export async function loadProjectContextFromDirectory(startPath) {
1006
1014
  },
1007
1015
  projectFilePath,
1008
1016
  sessionFilePath,
1009
- stateFilePath: resolveStateFilePath(project.projectPath),
1010
- currentFilePath: resolveCurrentFilePath(project.projectPath),
1017
+ stateFilePath: resolveStateFilePath(workspaceRoot),
1018
+ currentFilePath: resolveCurrentFilePath(workspaceRoot),
1011
1019
  metaDir
1012
1020
  };
1013
1021
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
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.16",
32
- "@longtable/core": "0.1.16",
33
- "@longtable/memory": "0.1.16",
34
- "@longtable/provider-claude": "0.1.16",
35
- "@longtable/provider-codex": "0.1.16",
36
- "@longtable/setup": "0.1.16"
31
+ "@longtable/checkpoints": "0.1.17",
32
+ "@longtable/core": "0.1.17",
33
+ "@longtable/memory": "0.1.17",
34
+ "@longtable/provider-claude": "0.1.17",
35
+ "@longtable/provider-codex": "0.1.17",
36
+ "@longtable/setup": "0.1.17"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.1",