@longtable/cli 0.1.52 → 0.1.54
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 +25 -1
- package/dist/cli.js +45 -82
- package/dist/debate.js +21 -21
- package/dist/longtable-codex-native-hook.js +17 -6
- package/dist/panel.js +1 -1
- package/dist/persona-router.d.ts +1 -1
- package/dist/persona-router.js +2 -2
- package/dist/project-session.d.ts +2 -1
- package/dist/project-session.js +29 -41
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@ Researcher-facing CLI for LongTable.
|
|
|
4
4
|
|
|
5
5
|
LongTable keeps scholarly project state in `.longtable/` and exposes generated
|
|
6
6
|
provider skills for Codex and Claude Code. The CLI installs setup, state,
|
|
7
|
-
checkpoint, search, and diagnostic tooling. It does not replace the
|
|
7
|
+
checkpoint, search, panel, and diagnostic tooling. It does not replace the
|
|
8
|
+
provider.
|
|
8
9
|
|
|
9
10
|
## Install
|
|
10
11
|
|
|
@@ -74,11 +75,34 @@ longtable question --prompt "<decision context>"
|
|
|
74
75
|
longtable decide --question <id> --answer <value>
|
|
75
76
|
longtable spec read --cwd "<project-path>"
|
|
76
77
|
longtable search --query "<topic>"
|
|
78
|
+
longtable panel --prompt "review this measurement plan" --json
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
`longtable start` remains available for scripted workspace creation with
|
|
80
82
|
`--no-interview --json`, but it is not the primary research-start surface.
|
|
81
83
|
|
|
84
|
+
## Panel Orchestration
|
|
85
|
+
|
|
86
|
+
Panel orchestration is for moments where disagreement matters: methods risk,
|
|
87
|
+
measurement validity, theory fit, literature positioning, and claims that need
|
|
88
|
+
challenge before they become project memory.
|
|
89
|
+
|
|
90
|
+
The CLI creates a provider-neutral `PanelPlan` and returns a planned
|
|
91
|
+
`PanelResult`. When native subagents are unavailable, LongTable uses a stable
|
|
92
|
+
sequential fallback prompt. That keeps the same research semantics available in
|
|
93
|
+
Codex and Claude Code without making either provider's native question or agent
|
|
94
|
+
tool the source of truth.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
longtable panel --prompt "Review this measurement plan." --role editor,measurement_auditor --json
|
|
98
|
+
longtable panel --visibility always_visible --prompt "Keep unresolved disagreement visible." --json
|
|
99
|
+
longtable ask --prompt "lt debate: Review this design before I commit it." --json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Team-style requests route through panel. Explicit debate-language requests write
|
|
103
|
+
panel debate records under `.longtable/panel/`; LongTable team execution is
|
|
104
|
+
disabled for new work.
|
|
105
|
+
|
|
82
106
|
## Development
|
|
83
107
|
|
|
84
108
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
|
|
|
20
20
|
import { buildPanelFallback, renderPanelSummary } from "./panel.js";
|
|
21
21
|
import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, getMissingManagedCodexHookTrustState, mergeCodexHookTrustState, mergeManagedCodexHooksConfig, removeCodexHookTrustState, removeManagedCodexHooks } from "./codex-hooks.js";
|
|
22
22
|
import { appendInvocationRecordToWorkspace, applyResearchSpecificationPatch, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, diffResearchSpecifications, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, findUnincorporatedResearchEvidence, proposeResearchSpecificationPatch, pruneWorkspaceQuestions, readResearchSpecificationHistory, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
|
|
23
|
-
import { buildTeamDebate
|
|
23
|
+
import { buildTeamDebate } from "./debate.js";
|
|
24
24
|
import { createPromptRenderer } from "./prompt-renderer.js";
|
|
25
25
|
const VALID_MODES = new Set([
|
|
26
26
|
"explore",
|
|
@@ -154,7 +154,6 @@ function usage() {
|
|
|
154
154
|
" longtable access probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
|
|
155
155
|
" longtable search --query <text> [--intent literature|theory|measurement|citation|metadata|venue] [--field <text>] [--source all|crossref,arxiv,openalex,semantic_scholar,pubmed,eric,doaj,unpaywall] [--must <term[,term]>] [--exclude <term[,term]>] [--limit <n>] [--allow-partial] [--publisher-access] [--record] [--cwd <path>] [--json]",
|
|
156
156
|
" longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
|
|
157
|
-
" longtable team --prompt <text> [--role <role[,role]>] [--debate] [--rounds 3|5] [--cwd <path>] [--json]",
|
|
158
157
|
" longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
|
|
159
158
|
" longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
|
|
160
159
|
" longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
|
|
@@ -2264,10 +2263,10 @@ function inferCollaborationRoute(prompt) {
|
|
|
2264
2263
|
if (explicitDebate) {
|
|
2265
2264
|
return "debate";
|
|
2266
2265
|
}
|
|
2267
|
-
const
|
|
2266
|
+
const explicitPanelTeam = /\bagent team\b|\bresearch team\b|\bteam review\b|\bteam-style\b|\buse a team\b/i.test(prompt) ||
|
|
2268
2267
|
/에이전트\s*팀|연구\s*팀|팀\s*(리뷰|검토)|팀으로/.test(prompt);
|
|
2269
|
-
if (
|
|
2270
|
-
return "
|
|
2268
|
+
if (explicitPanelTeam) {
|
|
2269
|
+
return "panel";
|
|
2271
2270
|
}
|
|
2272
2271
|
const panelCue = /\bpanel\b|\bmulti[- ]?role\b|\bmultiple perspectives\b|\brole disagreement\b|\bdisagreement\b|\bconflict\b/i.test(prompt) ||
|
|
2273
2272
|
/패널|여러\s*관점|복수\s*관점|역할.*불일치|불일치|충돌/.test(prompt);
|
|
@@ -2282,10 +2281,10 @@ function inferCollaborationRoute(prompt) {
|
|
|
2282
2281
|
trigger.signal.artifactStakes === "study_protocol" ||
|
|
2283
2282
|
trigger.requiresQuestionBeforeClosure;
|
|
2284
2283
|
if (panelCue && trigger.signal.artifactStakes === "external_submission") {
|
|
2285
|
-
return "
|
|
2284
|
+
return "panel";
|
|
2286
2285
|
}
|
|
2287
2286
|
if (highStakes) {
|
|
2288
|
-
return "
|
|
2287
|
+
return "panel";
|
|
2289
2288
|
}
|
|
2290
2289
|
return "panel";
|
|
2291
2290
|
}
|
|
@@ -3386,10 +3385,10 @@ async function runAsk(args) {
|
|
|
3386
3385
|
(directive.panel || delegatedArgs.panel === true
|
|
3387
3386
|
? "panel"
|
|
3388
3387
|
: inferCollaborationRoute(effectivePrompt) ?? (inferred === "panel" ? "panel" : null));
|
|
3389
|
-
if (collaborationRoute === "
|
|
3390
|
-
await
|
|
3388
|
+
if (collaborationRoute === "debate") {
|
|
3389
|
+
await runPanelDebateCommand({
|
|
3391
3390
|
...delegatedArgs,
|
|
3392
|
-
debate:
|
|
3391
|
+
debate: true
|
|
3393
3392
|
});
|
|
3394
3393
|
return;
|
|
3395
3394
|
}
|
|
@@ -3408,21 +3407,21 @@ function localId(prefix) {
|
|
|
3408
3407
|
async function writeJsonFile(path, value) {
|
|
3409
3408
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
3410
3409
|
}
|
|
3411
|
-
async function
|
|
3412
|
-
await mkdir(
|
|
3413
|
-
await writeFile(join(
|
|
3414
|
-
await writeJsonFile(join(
|
|
3415
|
-
await writeJsonFile(join(
|
|
3410
|
+
async function writePanelDebateArtifacts(bundle, panelDir, prompt) {
|
|
3411
|
+
await mkdir(panelDir, { recursive: true });
|
|
3412
|
+
await writeFile(join(panelDir, "prompt.txt"), prompt, "utf8");
|
|
3413
|
+
await writeJsonFile(join(panelDir, "plan.json"), bundle.plan);
|
|
3414
|
+
await writeJsonFile(join(panelDir, "run.json"), bundle.run);
|
|
3416
3415
|
for (const round of bundle.run.rounds) {
|
|
3417
3416
|
await mkdir(round.artifactDir, { recursive: true });
|
|
3418
3417
|
await writeJsonFile(join(round.artifactDir, "round.json"), round);
|
|
3419
3418
|
for (const contribution of round.contributions) {
|
|
3420
|
-
await writeJsonFile(join(
|
|
3419
|
+
await writeJsonFile(join(panelDir, contribution.artifactPath), contribution);
|
|
3421
3420
|
}
|
|
3422
3421
|
}
|
|
3423
|
-
await writeJsonFile(join(
|
|
3424
|
-
await writeJsonFile(join(
|
|
3425
|
-
await writeJsonFile(join(
|
|
3422
|
+
await writeJsonFile(join(panelDir, "synthesis.json"), bundle.run.synthesis);
|
|
3423
|
+
await writeJsonFile(join(panelDir, "checkpoint.json"), bundle.questionRecord);
|
|
3424
|
+
await writeJsonFile(join(panelDir, "invocation.json"), bundle.invocationRecord);
|
|
3426
3425
|
}
|
|
3427
3426
|
function sentinelSummary(prompt, workingDirectory) {
|
|
3428
3427
|
const trigger = classifyCheckpointTrigger(prompt, {
|
|
@@ -3501,19 +3500,15 @@ async function runSentinel(args) {
|
|
|
3501
3500
|
console.log(context ? `- recorded in: ${context.stateFilePath}` : "- record skipped: no LongTable workspace found");
|
|
3502
3501
|
}
|
|
3503
3502
|
}
|
|
3504
|
-
async function
|
|
3503
|
+
async function runPanelDebateCommand(args) {
|
|
3505
3504
|
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
3506
3505
|
const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
|
|
3507
3506
|
if (!prompt) {
|
|
3508
3507
|
throw new Error("A prompt is required.");
|
|
3509
3508
|
}
|
|
3510
|
-
const
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
if (!Number.isInteger(rounds) || rounds !== expectedRounds) {
|
|
3514
|
-
throw new Error(isDebate
|
|
3515
|
-
? "LongTable team debate v1 supports `--rounds 5` only."
|
|
3516
|
-
: "LongTable team v1 supports `--rounds 3` cross-review only.");
|
|
3509
|
+
const rounds = typeof args.rounds === "string" ? Number(args.rounds) : 5;
|
|
3510
|
+
if (!Number.isInteger(rounds) || rounds !== 5) {
|
|
3511
|
+
throw new Error("LongTable panel debate v1 supports `--rounds 5` only.");
|
|
3517
3512
|
}
|
|
3518
3513
|
const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
|
|
3519
3514
|
const projectContext = await loadProjectContextFromDirectory(workingDirectory);
|
|
@@ -3521,78 +3516,47 @@ async function runTeam(args) {
|
|
|
3521
3516
|
await assertWorkspaceNotBlocked(projectContext);
|
|
3522
3517
|
}
|
|
3523
3518
|
const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
|
|
3524
|
-
const
|
|
3525
|
-
const
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
teamDir,
|
|
3530
|
-
prompt: projectAware.prompt,
|
|
3531
|
-
roleFlag: typeof args.role === "string" ? args.role : undefined,
|
|
3532
|
-
provider: setup?.providerSelection.provider,
|
|
3533
|
-
visibility: "always_visible",
|
|
3534
|
-
roundCount: rounds
|
|
3535
|
-
});
|
|
3536
|
-
await writeTeamDebateArtifacts(debate, teamDir, prompt);
|
|
3537
|
-
const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
|
|
3538
|
-
if (canRecordWorkspace) {
|
|
3539
|
-
await appendInvocationRecordToWorkspace(projectContext, debate.invocationRecord, [debate.questionRecord]);
|
|
3540
|
-
}
|
|
3541
|
-
if (args.json === true) {
|
|
3542
|
-
console.log(JSON.stringify({
|
|
3543
|
-
teamId,
|
|
3544
|
-
teamDir,
|
|
3545
|
-
plan: debate.plan,
|
|
3546
|
-
run: debate.run,
|
|
3547
|
-
questionRecord: debate.questionRecord,
|
|
3548
|
-
invocationRecord: debate.invocationRecord,
|
|
3549
|
-
execution: {
|
|
3550
|
-
status: "completed",
|
|
3551
|
-
surface: debate.run.surface,
|
|
3552
|
-
projectContextFound: projectAware.projectContextFound,
|
|
3553
|
-
invocationLogged: canRecordWorkspace
|
|
3554
|
-
}
|
|
3555
|
-
}, null, 2));
|
|
3556
|
-
return;
|
|
3557
|
-
}
|
|
3558
|
-
console.log(renderTeamDebateSummary(debate.run));
|
|
3559
|
-
console.log(`- checkpoint: ${debate.questionRecord.id}`);
|
|
3560
|
-
return;
|
|
3561
|
-
}
|
|
3562
|
-
const team = buildTeamReview({
|
|
3563
|
-
teamId,
|
|
3564
|
-
teamDir,
|
|
3519
|
+
const panelId = localId("panel_debate");
|
|
3520
|
+
const panelDir = join(workingDirectory, ".longtable", "panel", panelId);
|
|
3521
|
+
const debate = buildTeamDebate({
|
|
3522
|
+
teamId: panelId,
|
|
3523
|
+
teamDir: panelDir,
|
|
3565
3524
|
prompt: projectAware.prompt,
|
|
3566
3525
|
roleFlag: typeof args.role === "string" ? args.role : undefined,
|
|
3567
3526
|
provider: setup?.providerSelection.provider,
|
|
3568
3527
|
visibility: "always_visible",
|
|
3569
3528
|
roundCount: rounds
|
|
3570
3529
|
});
|
|
3571
|
-
await
|
|
3530
|
+
await writePanelDebateArtifacts(debate, panelDir, prompt);
|
|
3572
3531
|
const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
|
|
3573
3532
|
if (canRecordWorkspace) {
|
|
3574
|
-
await appendInvocationRecordToWorkspace(projectContext,
|
|
3533
|
+
await appendInvocationRecordToWorkspace(projectContext, debate.invocationRecord, [debate.questionRecord]);
|
|
3575
3534
|
}
|
|
3576
3535
|
if (args.json === true) {
|
|
3577
3536
|
console.log(JSON.stringify({
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
plan:
|
|
3581
|
-
run:
|
|
3582
|
-
questionRecord:
|
|
3583
|
-
invocationRecord:
|
|
3537
|
+
panelId,
|
|
3538
|
+
panelDir,
|
|
3539
|
+
plan: debate.plan,
|
|
3540
|
+
run: debate.run,
|
|
3541
|
+
questionRecord: debate.questionRecord,
|
|
3542
|
+
invocationRecord: debate.invocationRecord,
|
|
3584
3543
|
execution: {
|
|
3585
3544
|
status: "completed",
|
|
3586
|
-
surface:
|
|
3587
|
-
interactionDepth: team.run.interactionDepth,
|
|
3545
|
+
surface: debate.run.surface,
|
|
3588
3546
|
projectContextFound: projectAware.projectContextFound,
|
|
3589
3547
|
invocationLogged: canRecordWorkspace
|
|
3590
3548
|
}
|
|
3591
3549
|
}, null, 2));
|
|
3592
3550
|
return;
|
|
3593
3551
|
}
|
|
3594
|
-
console.log(
|
|
3595
|
-
console.log(`-
|
|
3552
|
+
console.log("LongTable Panel Debate");
|
|
3553
|
+
console.log(`- panel: ${panelId}`);
|
|
3554
|
+
console.log(`- interaction depth: ${debate.run.interactionDepth}`);
|
|
3555
|
+
console.log(`- rounds: ${debate.run.roundCount}`);
|
|
3556
|
+
console.log(`- checkpoint: ${debate.questionRecord.id}`);
|
|
3557
|
+
}
|
|
3558
|
+
function disabledTeamCommandError() {
|
|
3559
|
+
return new Error("`longtable team` is disabled. Use `longtable panel --prompt <text>` for visible multi-role review, or `longtable ask --prompt \"lt debate: <text>\"` when debate is explicitly requested.");
|
|
3596
3560
|
}
|
|
3597
3561
|
async function runDecide(args) {
|
|
3598
3562
|
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
@@ -4037,8 +4001,7 @@ async function main() {
|
|
|
4037
4001
|
return;
|
|
4038
4002
|
}
|
|
4039
4003
|
if (command === "team") {
|
|
4040
|
-
|
|
4041
|
-
return;
|
|
4004
|
+
throw disabledTeamCommandError();
|
|
4042
4005
|
}
|
|
4043
4006
|
if (command === "decide") {
|
|
4044
4007
|
await runDecide(values);
|
package/dist/debate.js
CHANGED
|
@@ -166,7 +166,7 @@ function convergenceContribution(roundId, plan, role, label, artifactPath) {
|
|
|
166
166
|
function buildSynthesis(plan, artifactPath, kind) {
|
|
167
167
|
const labels = plan.members.map((member) => member.label);
|
|
168
168
|
const highSensitivity = plan.checkpointSensitivity === "high";
|
|
169
|
-
const runLabel = kind === "debate" ? "debate" : "
|
|
169
|
+
const runLabel = kind === "debate" ? "panel debate" : "panel review";
|
|
170
170
|
return {
|
|
171
171
|
artifactPath,
|
|
172
172
|
summary: `The ${runLabel} completed across ${labels.join(", ")}. It should slow closure by turning role disagreement into an explicit researcher decision.`,
|
|
@@ -201,8 +201,8 @@ export function createTeamDebateQuestionRecord(run, provider) {
|
|
|
201
201
|
status: "pending",
|
|
202
202
|
prompt: {
|
|
203
203
|
id: createId("question_prompt"),
|
|
204
|
-
checkpointKey: "
|
|
205
|
-
title: isDebate ? "
|
|
204
|
+
checkpointKey: "panel_debate_next_decision",
|
|
205
|
+
title: isDebate ? "Panel debate follow-up decision" : "Panel review follow-up decision",
|
|
206
206
|
question: run.synthesis.recommendedCheckpoint,
|
|
207
207
|
type: "single_choice",
|
|
208
208
|
options: [
|
|
@@ -238,11 +238,11 @@ export function createTeamDebateQuestionRecord(run, provider) {
|
|
|
238
238
|
? "Role rebuttals and convergence should connect to an explicit researcher decision."
|
|
239
239
|
: "Cross-review created role disagreement that should connect to an explicit researcher decision.",
|
|
240
240
|
rationale: [
|
|
241
|
-
"
|
|
241
|
+
"LongTable panel orchestration is a research harness surface, not a substitute for researcher judgment.",
|
|
242
242
|
isDebate
|
|
243
243
|
? "The fixed debate rounds created disagreement that should connect to an explicit researcher decision."
|
|
244
244
|
: "The cross-review round created disagreement that should connect to an explicit researcher decision.",
|
|
245
|
-
`
|
|
245
|
+
`LongTable panel run: ${run.id}.`
|
|
246
246
|
],
|
|
247
247
|
preferredSurfaces: provider === "claude"
|
|
248
248
|
? ["native_structured", "numbered"]
|
|
@@ -255,8 +255,8 @@ function buildTeamBundle(options, kind) {
|
|
|
255
255
|
const expectedRounds = kind === "debate" ? 5 : 3;
|
|
256
256
|
if (roundCount !== expectedRounds) {
|
|
257
257
|
throw new Error(kind === "debate"
|
|
258
|
-
? "LongTable debate v1 supports fixed 5-round debate only."
|
|
259
|
-
: "LongTable
|
|
258
|
+
? "LongTable panel debate v1 supports fixed 5-round debate only."
|
|
259
|
+
: "LongTable panel review v1 supports fixed 3-round cross-review only.");
|
|
260
260
|
}
|
|
261
261
|
const createdAt = nowIso();
|
|
262
262
|
const plan = buildPanelPlan({
|
|
@@ -267,7 +267,7 @@ function buildTeamBundle(options, kind) {
|
|
|
267
267
|
visibility: options.visibility ?? "always_visible"
|
|
268
268
|
});
|
|
269
269
|
const rounds = [];
|
|
270
|
-
const round1Id = createId("
|
|
270
|
+
const round1Id = createId("panel_round");
|
|
271
271
|
const independentContributions = plan.members.map((member) => independentContribution(round1Id, plan, member.role, member.label, join("round-1-independent", `${member.role}.json`)));
|
|
272
272
|
rounds.push({
|
|
273
273
|
id: round1Id,
|
|
@@ -278,7 +278,7 @@ function buildTeamBundle(options, kind) {
|
|
|
278
278
|
artifactDir: join(options.teamDir, "round-1-independent"),
|
|
279
279
|
contributions: independentContributions
|
|
280
280
|
});
|
|
281
|
-
const round2Id = createId("
|
|
281
|
+
const round2Id = createId("panel_round");
|
|
282
282
|
const crossContributions = plan.members.flatMap((member) => plan.members
|
|
283
283
|
.filter((target) => target.role !== member.role)
|
|
284
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`))));
|
|
@@ -292,7 +292,7 @@ function buildTeamBundle(options, kind) {
|
|
|
292
292
|
contributions: crossContributions
|
|
293
293
|
});
|
|
294
294
|
if (kind === "debate") {
|
|
295
|
-
const round3Id = createId("
|
|
295
|
+
const round3Id = createId("panel_round");
|
|
296
296
|
rounds.push({
|
|
297
297
|
id: round3Id,
|
|
298
298
|
index: 3,
|
|
@@ -302,7 +302,7 @@ function buildTeamBundle(options, kind) {
|
|
|
302
302
|
artifactDir: join(options.teamDir, "round-3-rebuttal"),
|
|
303
303
|
contributions: plan.members.map((member) => rebuttalContribution(round3Id, member.role, member.label, join("round-3-rebuttal", `${member.role}.json`)))
|
|
304
304
|
});
|
|
305
|
-
const round4Id = createId("
|
|
305
|
+
const round4Id = createId("panel_round");
|
|
306
306
|
rounds.push({
|
|
307
307
|
id: round4Id,
|
|
308
308
|
index: 4,
|
|
@@ -315,22 +315,22 @@ function buildTeamBundle(options, kind) {
|
|
|
315
315
|
}
|
|
316
316
|
const synthesis = buildSynthesis(plan, "synthesis.json", kind);
|
|
317
317
|
const run = {
|
|
318
|
-
id: createId("
|
|
318
|
+
id: createId("panel_debate_run"),
|
|
319
319
|
teamId: options.teamId,
|
|
320
320
|
createdAt,
|
|
321
321
|
updatedAt: createdAt,
|
|
322
322
|
prompt: options.prompt,
|
|
323
323
|
roles: plan.members,
|
|
324
324
|
status: "completed",
|
|
325
|
-
surface: "
|
|
325
|
+
surface: "file_backed_panel_debate",
|
|
326
326
|
interactionDepth: kind === "debate" ? "debated" : "cross_reviewed",
|
|
327
|
-
roundPolicy: kind === "debate" ? "fixed" : "
|
|
327
|
+
roundPolicy: kind === "debate" ? "fixed" : "panel_cross_review",
|
|
328
328
|
roundCount,
|
|
329
329
|
artifactRoot: options.teamDir,
|
|
330
330
|
rounds: [
|
|
331
331
|
...rounds,
|
|
332
332
|
{
|
|
333
|
-
id: createId("
|
|
333
|
+
id: createId("panel_round"),
|
|
334
334
|
index: roundCount,
|
|
335
335
|
kind: "synthesis",
|
|
336
336
|
title: "Coordinator synthesis and checkpoint",
|
|
@@ -353,12 +353,12 @@ function buildTeamBundle(options, kind) {
|
|
|
353
353
|
checkpointSensitivity: plan.checkpointSensitivity,
|
|
354
354
|
rationale: [
|
|
355
355
|
kind === "debate"
|
|
356
|
-
? "Autonomous debate requested through LongTable
|
|
357
|
-
: "
|
|
356
|
+
? "Autonomous debate requested through LongTable panel orchestration."
|
|
357
|
+
: "Cross-role review requested through LongTable panel orchestration.",
|
|
358
358
|
"File-backed rounds keep disagreement inspectable before researcher closure."
|
|
359
359
|
]
|
|
360
360
|
});
|
|
361
|
-
intent.kind = kind === "debate" ? "
|
|
361
|
+
intent.kind = kind === "debate" ? "panel_debate" : "panel";
|
|
362
362
|
intent.requestedSurface = run.surface;
|
|
363
363
|
const invocationRecord = {
|
|
364
364
|
id: createId("invocation_record"),
|
|
@@ -371,7 +371,7 @@ function buildTeamBundle(options, kind) {
|
|
|
371
371
|
interactionDepth: run.interactionDepth,
|
|
372
372
|
panelPlan: plan,
|
|
373
373
|
teamDebateRun: run,
|
|
374
|
-
degradationReason: "File-backed
|
|
374
|
+
degradationReason: "File-backed panel artifacts are the canonical execution record."
|
|
375
375
|
};
|
|
376
376
|
return {
|
|
377
377
|
plan,
|
|
@@ -389,8 +389,8 @@ export function buildTeamDebate(options) {
|
|
|
389
389
|
}
|
|
390
390
|
export function renderTeamDebateSummary(run) {
|
|
391
391
|
return [
|
|
392
|
-
"LongTable
|
|
393
|
-
`-
|
|
392
|
+
"LongTable Panel Debate",
|
|
393
|
+
`- panel: ${run.teamId}`,
|
|
394
394
|
`- surface: ${run.surface}`,
|
|
395
395
|
`- interaction depth: ${run.interactionDepth}`,
|
|
396
396
|
`- rounds: ${run.roundCount} ${run.roundPolicy}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
|
-
import { collectHardStopBlockers } from "@longtable/core";
|
|
2
|
+
import { collectHardStopBlockers, evaluateResearchSpecificationReadiness } from "@longtable/core";
|
|
3
3
|
import { createWorkspaceFollowUpQuestions, loadProjectContextFromDirectory, loadWorkspaceState, pendingQuestionObligations } from "./index.js";
|
|
4
4
|
function safeString(value) {
|
|
5
5
|
return typeof value === "string" ? value : "";
|
|
@@ -225,7 +225,7 @@ function mutatesLongTableResearchState(command) {
|
|
|
225
225
|
return false;
|
|
226
226
|
}
|
|
227
227
|
return /\.longtable(?:\/|\b)|\bCURRENT\.md\b/.test(normalized)
|
|
228
|
-
|| /\blongtable\s+(?:start|question|clear-question|prune-questions|ask|clarify|panel
|
|
228
|
+
|| /\blongtable\s+(?:start|question|clear-question|prune-questions|ask|clarify|panel)\b/.test(normalized);
|
|
229
229
|
}
|
|
230
230
|
async function loadLongTableRuntime(startPath) {
|
|
231
231
|
const context = await loadProjectContextFromDirectory(startPath);
|
|
@@ -237,13 +237,22 @@ async function loadLongTableRuntime(startPath) {
|
|
|
237
237
|
}
|
|
238
238
|
function buildWorkspaceSummary(runtime, detail = "compact") {
|
|
239
239
|
const { context, state } = runtime;
|
|
240
|
+
const readiness = evaluateResearchSpecificationReadiness({
|
|
241
|
+
firstResearchShape: state.firstResearchShape ?? context.session.firstResearchShape,
|
|
242
|
+
researchSpecification: state.researchSpecification ?? context.session.researchSpecification,
|
|
243
|
+
questionLog: state.questionLog,
|
|
244
|
+
questionObligations: state.questionObligations
|
|
245
|
+
});
|
|
240
246
|
if (detail === "compact") {
|
|
241
|
-
const primaryContext = state.
|
|
242
|
-
? `
|
|
243
|
-
:
|
|
247
|
+
const primaryContext = state.researchSpecification
|
|
248
|
+
? `Research Specification: ${compactContextValue(state.researchSpecification.title, 96)} (${readiness.status}).`
|
|
249
|
+
: state.firstResearchShape
|
|
250
|
+
? `First research shape: ${compactContextValue(state.firstResearchShape.handle, 96)}.`
|
|
251
|
+
: `Current goal: ${compactContextValue(context.session.currentGoal, 120)}.`;
|
|
244
252
|
return [
|
|
245
253
|
"LongTable workspace detected; research context restored.",
|
|
246
254
|
primaryContext,
|
|
255
|
+
readiness.usableForInterview || readiness.status === "no_spec" ? "" : `Research Specification readiness: ${readiness.status}; next action: ${readiness.nextAction}.`,
|
|
247
256
|
context.session.nextAction ? `Next action: ${compactContextValue(context.session.nextAction)}.` : "",
|
|
248
257
|
context.session.protectedDecision ? "Protected decision: active; full text is in `.longtable/` and `CURRENT.md`." : ""
|
|
249
258
|
].filter(Boolean);
|
|
@@ -254,6 +263,8 @@ function buildWorkspaceSummary(runtime, detail = "compact") {
|
|
|
254
263
|
context.session.currentBlocker ? `Current blocker: ${context.session.currentBlocker}.` : "",
|
|
255
264
|
context.session.protectedDecision ? `Protected decision: ${context.session.protectedDecision}.` : "",
|
|
256
265
|
state.firstResearchShape ? `First research shape: ${state.firstResearchShape.handle}.` : "",
|
|
266
|
+
state.researchSpecification ? `Research Specification: ${state.researchSpecification.title} (${readiness.status}).` : "",
|
|
267
|
+
readiness.usableForInterview || readiness.status === "no_spec" ? "" : `Research Specification readiness: ${readiness.status}; next action: ${readiness.nextAction}; gaps: ${readiness.blockingGaps.join("; ")}.`,
|
|
257
268
|
context.session.nextAction ? `Next action: ${context.session.nextAction}.` : ""
|
|
258
269
|
].filter(Boolean);
|
|
259
270
|
return lines;
|
|
@@ -339,7 +350,7 @@ function buildActiveInterviewContext(hook) {
|
|
|
339
350
|
"A LongTable interview is currently active.",
|
|
340
351
|
`Interview status: ${hook.status}.`,
|
|
341
352
|
`Turns recorded: ${turnCount}.`,
|
|
342
|
-
"Do not finalize the research direction until the interview
|
|
353
|
+
"Do not finalize the research direction until the interview has created a Research Specification and either confirmed it or left an explicit pending/deferred confirmation."
|
|
343
354
|
].join("\n");
|
|
344
355
|
}
|
|
345
356
|
function sessionStartContext(runtime) {
|
package/dist/panel.js
CHANGED
|
@@ -183,7 +183,7 @@ export function createPlannedInvocationRecord(options) {
|
|
|
183
183
|
interactionDepth: "independent",
|
|
184
184
|
panelPlan: options.plan,
|
|
185
185
|
panelResult: options.result,
|
|
186
|
-
degradationReason: "
|
|
186
|
+
degradationReason: "Sequential fallback is the stable LongTable panel surface."
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
function roleInstruction(member) {
|
package/dist/persona-router.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface LongTableInvocationDirective {
|
|
|
12
12
|
explicit: boolean;
|
|
13
13
|
cleanedPrompt: string;
|
|
14
14
|
mode?: InteractionMode | "panel" | "status";
|
|
15
|
-
collaboration?: "panel" | "
|
|
15
|
+
collaboration?: "panel" | "debate";
|
|
16
16
|
roles: CanonicalPersona[];
|
|
17
17
|
panel: boolean;
|
|
18
18
|
showConflicts: boolean;
|
package/dist/persona-router.js
CHANGED
|
@@ -8,8 +8,8 @@ const DIRECTIVE_MAP = [
|
|
|
8
8
|
{ key: "draft", mode: "draft" },
|
|
9
9
|
{ key: "commit", mode: "commit" },
|
|
10
10
|
{ key: "panel", mode: "panel", collaboration: "panel", panel: true, showConflicts: true },
|
|
11
|
-
{ key: "team", mode: "
|
|
12
|
-
{ key: "debate", mode: "
|
|
11
|
+
{ key: "team", mode: "panel", collaboration: "panel", panel: true, showConflicts: true },
|
|
12
|
+
{ key: "debate", mode: "panel", collaboration: "debate", panel: true, showConflicts: true },
|
|
13
13
|
{ key: "status", mode: "status" },
|
|
14
14
|
{ key: "editor", mode: "review", roles: ["editor"] },
|
|
15
15
|
{ key: "reviewer", mode: "review", roles: ["reviewer"] },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecisionRecord, EvidenceRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, HardStopScope, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionPromptType, QuestionRecord, ResearchSpecificationChange, ResearchSpecificationPatch, ResearchSpecificationPatchSource, ResearchSpecificationRevision, ResearchState } from "@longtable/core";
|
|
1
|
+
import type { DecisionRecord, EvidenceRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, HardStopScope, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionPromptType, QuestionRecord, ResearchSpecificationChange, ResearchSpecificationPatch, ResearchSpecificationPatchSource, ResearchSpecificationReadiness, ResearchSpecificationRevision, ResearchState } from "@longtable/core";
|
|
2
2
|
import type { SetupPersistedOutput } from "@longtable/setup";
|
|
3
3
|
import { type HardStopVerdict } from "@longtable/core";
|
|
4
4
|
export type ProjectDisagreementPreference = "synthesis_only" | "show_on_conflict" | "always_visible";
|
|
@@ -222,6 +222,7 @@ export interface LongTableWorkspaceInspection {
|
|
|
222
222
|
specRevisions?: number;
|
|
223
223
|
};
|
|
224
224
|
hardStop?: HardStopVerdict;
|
|
225
|
+
researchSpecificationReadiness?: ResearchSpecificationReadiness;
|
|
225
226
|
recentInvocations?: Array<{
|
|
226
227
|
id: string;
|
|
227
228
|
kind: string;
|
package/dist/project-session.js
CHANGED
|
@@ -4,7 +4,7 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { appendDecisionRecord as appendDecisionToResearchState, appendInvocationRecord as appendInvocationToResearchState, appendQuestionRecords, createEmptyResearchState } from "@longtable/memory";
|
|
6
6
|
import { classifyCheckpointTrigger } from "@longtable/checkpoints";
|
|
7
|
-
import { collectHardStopBlockers } from "@longtable/core";
|
|
7
|
+
import { collectHardStopBlockers, evaluateResearchSpecificationReadiness, requiredResearchSpecificationGaps } from "@longtable/core";
|
|
8
8
|
import { ensureRequiredQuestionObligation, pendingQuestionObligations, resolveQuestionObligationByQuestionId } from "./question-obligations.js";
|
|
9
9
|
const CURRENT_FILE_NAME = "CURRENT.md";
|
|
10
10
|
const LEGACY_ROOT_FILES = ["LONGTABLE.md", "START-HERE.md", "NEXT-STEPS.md", "SESSION-SNAPSHOT.md"];
|
|
@@ -165,11 +165,15 @@ function renderResearchSpecificationSummary(specification, locale) {
|
|
|
165
165
|
return lines;
|
|
166
166
|
}
|
|
167
167
|
function renderResearchSpecificationStatus(session, locale) {
|
|
168
|
-
|
|
168
|
+
const readiness = evaluateResearchSpecificationReadiness({
|
|
169
|
+
firstResearchShape: session.firstResearchShape,
|
|
170
|
+
researchSpecification: session.researchSpecification
|
|
171
|
+
});
|
|
172
|
+
if (readiness.status === "no_spec") {
|
|
169
173
|
return [];
|
|
170
174
|
}
|
|
171
175
|
const korean = locale === "ko";
|
|
172
|
-
if (
|
|
176
|
+
if (readiness.status === "shape_only") {
|
|
173
177
|
return [
|
|
174
178
|
"",
|
|
175
179
|
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
@@ -184,21 +188,27 @@ function renderResearchSpecificationStatus(session, locale) {
|
|
|
184
188
|
: "- Next protocol: when enough detail exists, run `summarize_research_specification` to create the preview, then `confirm_research_specification` to confirm, ask one more question, revise a section, or keep it open."
|
|
185
189
|
];
|
|
186
190
|
}
|
|
187
|
-
|
|
188
|
-
? "confirmed"
|
|
189
|
-
: session.researchSpecification.status ?? "draft";
|
|
190
|
-
if (status === "confirmed") {
|
|
191
|
+
if (readiness.status === "confirmed") {
|
|
191
192
|
return [];
|
|
192
193
|
}
|
|
193
194
|
return [
|
|
194
195
|
"",
|
|
195
196
|
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
196
197
|
korean
|
|
197
|
-
? `- 상태: ${status}. Research Specification은 저장되어 있지만 아직 확정된 종료 지점이 아닙니다.`
|
|
198
|
-
: `- Status: ${status}. Research Specification exists, but it is not a confirmed closure point yet.`,
|
|
198
|
+
? `- 상태: ${readiness.status}. Research Specification은 저장되어 있지만 아직 확정된 종료 지점이 아닙니다.`
|
|
199
|
+
: `- Status: ${readiness.status}. Research Specification exists, but it is not a confirmed closure point yet.`,
|
|
200
|
+
...(readiness.blockingGaps.length > 0
|
|
201
|
+
? [korean
|
|
202
|
+
? `- 남은 gap: ${readiness.blockingGaps.join("; ")}.`
|
|
203
|
+
: `- Remaining gaps: ${readiness.blockingGaps.join("; ")}.`]
|
|
204
|
+
: []),
|
|
199
205
|
korean
|
|
200
|
-
?
|
|
201
|
-
|
|
206
|
+
? readiness.nextAction === "start"
|
|
207
|
+
? "- 다음 프로토콜: `$longtable-start`에서 빠진 섹션을 더 묻고 Research Specification을 업데이트해야 합니다."
|
|
208
|
+
: "- 다음 프로토콜: `confirm_research_specification`으로 preview 확인을 받아야 합니다."
|
|
209
|
+
: readiness.nextAction === "start"
|
|
210
|
+
? "- Next protocol: stay in `$longtable-start`, ask for the missing sections, and update the Research Specification."
|
|
211
|
+
: "- Next protocol: return to `confirm_research_specification` for preview confirmation."
|
|
202
212
|
];
|
|
203
213
|
}
|
|
204
214
|
function renderResearchSpecificationAudit(state, locale) {
|
|
@@ -563,35 +573,6 @@ function cloneResearchSpecification(specification) {
|
|
|
563
573
|
function mergeStringLists(...lists) {
|
|
564
574
|
return [...new Set(lists.flatMap((list) => list ?? []).filter(Boolean))];
|
|
565
575
|
}
|
|
566
|
-
function requiredResearchSpecificationGaps(specification) {
|
|
567
|
-
const gaps = [];
|
|
568
|
-
if (!specification.researchDirection.question?.trim()) {
|
|
569
|
-
gaps.push("research question");
|
|
570
|
-
}
|
|
571
|
-
if (specification.constructOntology.coreConstructs.length === 0) {
|
|
572
|
-
gaps.push("construct map/core constructs");
|
|
573
|
-
}
|
|
574
|
-
if (specification.researchDirection.inclusionCriteria?.length === 0 &&
|
|
575
|
-
specification.researchDirection.exclusionCriteria?.length === 0) {
|
|
576
|
-
gaps.push("inclusion/exclusion rule");
|
|
577
|
-
}
|
|
578
|
-
if (specification.evidenceAccess.requiredSources?.length === 0 &&
|
|
579
|
-
specification.evidenceAccess.evidenceStandards?.length === 0) {
|
|
580
|
-
gaps.push("evidence boundary");
|
|
581
|
-
}
|
|
582
|
-
if (!specification.methodAnalysis.design?.trim() &&
|
|
583
|
-
specification.methodAnalysis.analysisOptions.length === 0) {
|
|
584
|
-
gaps.push("method commitment");
|
|
585
|
-
}
|
|
586
|
-
if (specification.openQuestions.length === 0 && specification.protectedDecisions.length === 0) {
|
|
587
|
-
gaps.push("unresolved decisions/protected decisions");
|
|
588
|
-
}
|
|
589
|
-
if (specification.evidenceAccess.accessRequirements?.length === 0 &&
|
|
590
|
-
specification.evidenceAccess.requiredSources?.length === 0) {
|
|
591
|
-
gaps.push("search/access assumptions");
|
|
592
|
-
}
|
|
593
|
-
return gaps;
|
|
594
|
-
}
|
|
595
576
|
function buildResearchSpecificationGapQuestion(gaps, timestamp, sourceEvidenceIds) {
|
|
596
577
|
return {
|
|
597
578
|
id: createId("question"),
|
|
@@ -767,6 +748,12 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
767
748
|
const answeredQuestions = questions.filter((record) => record.status === "answered");
|
|
768
749
|
const pendingObligations = visiblePendingObligations(state);
|
|
769
750
|
const hardStop = collectHardStopBlockers(state);
|
|
751
|
+
const researchSpecificationReadiness = evaluateResearchSpecificationReadiness({
|
|
752
|
+
firstResearchShape: state.firstResearchShape ?? context.session.firstResearchShape,
|
|
753
|
+
researchSpecification: state.researchSpecification ?? context.session.researchSpecification,
|
|
754
|
+
questionLog: state.questionLog,
|
|
755
|
+
questionObligations: state.questionObligations
|
|
756
|
+
});
|
|
770
757
|
return {
|
|
771
758
|
found: true,
|
|
772
759
|
rootPath: context.project.projectPath,
|
|
@@ -815,6 +802,7 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
815
802
|
specRevisions: (state.specRevisions ?? []).length
|
|
816
803
|
},
|
|
817
804
|
hardStop,
|
|
805
|
+
researchSpecificationReadiness,
|
|
818
806
|
recentInvocations: recentInvocationRecords(state, 5).map((record) => ({
|
|
819
807
|
id: record.id,
|
|
820
808
|
kind: record.intent.kind,
|
|
@@ -2275,7 +2263,7 @@ const COMMITMENT_FAMILY_BY_CHECKPOINT = [
|
|
|
2275
2263
|
[/research_question|research_direction|scope|boundary|inclusion|exclusion/, "scope"],
|
|
2276
2264
|
[/theory|construct|conceptual/, "construct"],
|
|
2277
2265
|
[/measurement|coding|codebook|extraction/, "coding"],
|
|
2278
|
-
[/method|analysis|panel_disagreement|team_debate|review/, "method"],
|
|
2266
|
+
[/method|analysis|panel_disagreement|panel_debate|team_debate|review/, "method"],
|
|
2279
2267
|
[/evidence|scholarly_access|source_authority/, "evidence"],
|
|
2280
2268
|
[/knowledge_gap|tacit_assumption|epistemic/, "epistemic_authority"]
|
|
2281
2269
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Researcher-facing LongTable CLI",
|
|
6
6
|
"type": "module",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.2.0",
|
|
32
|
-
"@longtable/checkpoints": "0.1.
|
|
33
|
-
"@longtable/core": "0.1.
|
|
34
|
-
"@longtable/memory": "0.1.
|
|
35
|
-
"@longtable/provider-claude": "0.1.
|
|
36
|
-
"@longtable/provider-codex": "0.1.
|
|
37
|
-
"@longtable/setup": "0.1.
|
|
32
|
+
"@longtable/checkpoints": "0.1.54",
|
|
33
|
+
"@longtable/core": "0.1.54",
|
|
34
|
+
"@longtable/memory": "0.1.54",
|
|
35
|
+
"@longtable/provider-claude": "0.1.54",
|
|
36
|
+
"@longtable/provider-codex": "0.1.54",
|
|
37
|
+
"@longtable/setup": "0.1.54"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|