@longtable/cli 0.1.16 → 0.1.18
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 +6 -0
- package/dist/cli.js +104 -6
- package/dist/debate.d.ts +21 -0
- package/dist/debate.js +380 -0
- package/dist/project-session.js +13 -4
- package/package.json +7 -7
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.
|
|
45
|
+
const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.18";
|
|
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:
|
|
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");
|
package/dist/debate.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/project-session.js
CHANGED
|
@@ -193,6 +193,7 @@ async function loadResearchState(stateFilePath) {
|
|
|
193
193
|
}
|
|
194
194
|
const parsed = JSON.parse(await readFile(stateFilePath, "utf8"));
|
|
195
195
|
return {
|
|
196
|
+
...parsed,
|
|
196
197
|
explicitState: parsed.explicitState ?? {},
|
|
197
198
|
workingState: parsed.workingState ?? {},
|
|
198
199
|
inferredHypotheses: parsed.inferredHypotheses ?? [],
|
|
@@ -893,6 +894,7 @@ export async function createOrUpdateProjectWorkspace(options) {
|
|
|
893
894
|
const project = existsSync(projectFilePath)
|
|
894
895
|
? {
|
|
895
896
|
...JSON.parse(await readFile(projectFilePath, "utf8")),
|
|
897
|
+
projectPath,
|
|
896
898
|
contractVersion: "workspace-v2",
|
|
897
899
|
locale
|
|
898
900
|
}
|
|
@@ -992,8 +994,15 @@ export async function loadProjectContextFromDirectory(startPath) {
|
|
|
992
994
|
const projectFilePath = join(metaDir, "project.json");
|
|
993
995
|
const sessionFilePath = join(metaDir, "current-session.json");
|
|
994
996
|
if (existsSync(projectFilePath) && existsSync(sessionFilePath)) {
|
|
995
|
-
const
|
|
996
|
-
const
|
|
997
|
+
const workspaceRoot = current;
|
|
998
|
+
const project = {
|
|
999
|
+
...JSON.parse(await readFile(projectFilePath, "utf8")),
|
|
1000
|
+
projectPath: workspaceRoot
|
|
1001
|
+
};
|
|
1002
|
+
const session = {
|
|
1003
|
+
...JSON.parse(await readFile(sessionFilePath, "utf8")),
|
|
1004
|
+
projectPath: workspaceRoot
|
|
1005
|
+
};
|
|
997
1006
|
return {
|
|
998
1007
|
project,
|
|
999
1008
|
session: {
|
|
@@ -1006,8 +1015,8 @@ export async function loadProjectContextFromDirectory(startPath) {
|
|
|
1006
1015
|
},
|
|
1007
1016
|
projectFilePath,
|
|
1008
1017
|
sessionFilePath,
|
|
1009
|
-
stateFilePath: resolveStateFilePath(
|
|
1010
|
-
currentFilePath: resolveCurrentFilePath(
|
|
1018
|
+
stateFilePath: resolveStateFilePath(workspaceRoot),
|
|
1019
|
+
currentFilePath: resolveCurrentFilePath(workspaceRoot),
|
|
1011
1020
|
metaDir
|
|
1012
1021
|
};
|
|
1013
1022
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
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.
|
|
32
|
-
"@longtable/core": "0.1.
|
|
33
|
-
"@longtable/memory": "0.1.
|
|
34
|
-
"@longtable/provider-claude": "0.1.
|
|
35
|
-
"@longtable/provider-codex": "0.1.
|
|
36
|
-
"@longtable/setup": "0.1.
|
|
31
|
+
"@longtable/checkpoints": "0.1.18",
|
|
32
|
+
"@longtable/core": "0.1.18",
|
|
33
|
+
"@longtable/memory": "0.1.18",
|
|
34
|
+
"@longtable/provider-claude": "0.1.18",
|
|
35
|
+
"@longtable/provider-codex": "0.1.18",
|
|
36
|
+
"@longtable/setup": "0.1.18"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^22.10.1",
|