@longtable/cli 0.1.55 → 0.1.57
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/dist/cli.js +368 -20
- package/dist/panel-runtime.d.ts +20 -0
- package/dist/panel-runtime.js +525 -0
- package/dist/panel.d.ts +4 -1
- package/dist/panel.js +34 -7
- package/dist/project-session.d.ts +36 -1
- package/dist/project-session.js +451 -18
- package/package.json +7 -7
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
import type { DecisionRecord, EvidenceRecord, InvocationRecord, InvocationStatus, InvocationSurface, LongTableQuestionObligation, PanelMemberResult, 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";
|
|
@@ -269,6 +269,32 @@ export interface LongTableWorkspaceInspection {
|
|
|
269
269
|
suggestion?: string;
|
|
270
270
|
}>;
|
|
271
271
|
}
|
|
272
|
+
export interface PanelResultRecordInput {
|
|
273
|
+
invocationId?: string;
|
|
274
|
+
status?: InvocationStatus;
|
|
275
|
+
surface?: InvocationSurface;
|
|
276
|
+
synthesis?: string;
|
|
277
|
+
conflictSummary?: string;
|
|
278
|
+
decisionPrompt?: string;
|
|
279
|
+
memberResults?: Array<Partial<PanelMemberResult> & {
|
|
280
|
+
role: string;
|
|
281
|
+
}>;
|
|
282
|
+
}
|
|
283
|
+
export interface PanelResultRecordOutput {
|
|
284
|
+
invocation: InvocationRecord;
|
|
285
|
+
evidenceRecords: EvidenceRecord[];
|
|
286
|
+
state: LongTableWorkspaceState;
|
|
287
|
+
}
|
|
288
|
+
export interface LongTableHandoffOutput {
|
|
289
|
+
id: string;
|
|
290
|
+
createdAt: string;
|
|
291
|
+
path: string;
|
|
292
|
+
content: string;
|
|
293
|
+
sourceEvidenceIds: string[];
|
|
294
|
+
pendingQuestionIds: string[];
|
|
295
|
+
proposedPatchIds: string[];
|
|
296
|
+
latestInvocationId?: string;
|
|
297
|
+
}
|
|
272
298
|
export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<LongTableWorkspaceState>;
|
|
273
299
|
export declare function diffResearchSpecifications(before: ResearchSpecification | undefined, after: ResearchSpecification): ResearchSpecificationChange[];
|
|
274
300
|
export declare function applyResearchSpecificationAuditUpdate(state: LongTableWorkspaceState, options: {
|
|
@@ -291,6 +317,14 @@ export declare function applyResearchSpecificationAuditUpdate(state: LongTableWo
|
|
|
291
317
|
};
|
|
292
318
|
export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
|
|
293
319
|
export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<LongTableWorkspaceState>;
|
|
320
|
+
export declare function recordPanelResultInWorkspace(options: {
|
|
321
|
+
context: LongTableProjectContext;
|
|
322
|
+
result: PanelResultRecordInput;
|
|
323
|
+
}): Promise<PanelResultRecordOutput>;
|
|
324
|
+
export declare function createWorkspaceHandoff(options: {
|
|
325
|
+
context: LongTableProjectContext;
|
|
326
|
+
outputPath?: string;
|
|
327
|
+
}): Promise<LongTableHandoffOutput>;
|
|
294
328
|
export declare function beginLongTableInterview(options: {
|
|
295
329
|
context: LongTableProjectContext;
|
|
296
330
|
provider?: ProviderKind;
|
|
@@ -456,6 +490,7 @@ export declare function pruneWorkspaceQuestions(options: {
|
|
|
456
490
|
}>;
|
|
457
491
|
export declare function repairWorkspaceStateConsistency(options: {
|
|
458
492
|
context: LongTableProjectContext;
|
|
493
|
+
dryRun?: boolean;
|
|
459
494
|
}): Promise<{
|
|
460
495
|
state: ResearchState;
|
|
461
496
|
repaired: string[];
|
package/dist/project-session.js
CHANGED
|
@@ -497,6 +497,98 @@ function formatQuestionMetadata(record) {
|
|
|
497
497
|
].filter(Boolean);
|
|
498
498
|
return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
|
|
499
499
|
}
|
|
500
|
+
const QUESTION_SURFACES = new Set([
|
|
501
|
+
"native_structured",
|
|
502
|
+
"mcp_elicitation",
|
|
503
|
+
"numbered",
|
|
504
|
+
"terminal_selector",
|
|
505
|
+
"web_form"
|
|
506
|
+
]);
|
|
507
|
+
function asStringArray(value) {
|
|
508
|
+
if (Array.isArray(value)) {
|
|
509
|
+
return value.filter((entry) => typeof entry === "string");
|
|
510
|
+
}
|
|
511
|
+
return typeof value === "string" ? [value] : [];
|
|
512
|
+
}
|
|
513
|
+
function legacyQuestionAnswerRecord(record) {
|
|
514
|
+
return asRecord(record.answer);
|
|
515
|
+
}
|
|
516
|
+
function labelForQuestionAnswerValue(record, value) {
|
|
517
|
+
const option = record.prompt.options.find((candidate) => candidate.value === value || candidate.label === value);
|
|
518
|
+
if (option) {
|
|
519
|
+
return option.label;
|
|
520
|
+
}
|
|
521
|
+
if (value === "other") {
|
|
522
|
+
return record.prompt.otherLabel ?? "Other";
|
|
523
|
+
}
|
|
524
|
+
return value;
|
|
525
|
+
}
|
|
526
|
+
function selectedValuesForQuestion(record) {
|
|
527
|
+
const answer = legacyQuestionAnswerRecord(record);
|
|
528
|
+
if (!answer) {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
const directValues = asStringArray(answer.selectedValues);
|
|
532
|
+
if (directValues.length > 0) {
|
|
533
|
+
return directValues;
|
|
534
|
+
}
|
|
535
|
+
const legacyValues = [
|
|
536
|
+
...asStringArray(answer.selectedValue),
|
|
537
|
+
...asStringArray(answer.selected),
|
|
538
|
+
...asStringArray(answer.selectedOption),
|
|
539
|
+
...asStringArray(answer.answer),
|
|
540
|
+
...asStringArray(answer.value)
|
|
541
|
+
];
|
|
542
|
+
return uniqueStrings(legacyValues);
|
|
543
|
+
}
|
|
544
|
+
function selectedLabelsForQuestion(record, selectedValues) {
|
|
545
|
+
const answer = legacyQuestionAnswerRecord(record);
|
|
546
|
+
const directLabels = asStringArray(answer?.selectedLabels);
|
|
547
|
+
if (directLabels.length > 0) {
|
|
548
|
+
return directLabels;
|
|
549
|
+
}
|
|
550
|
+
return selectedValues.map((value) => labelForQuestionAnswerValue(record, value));
|
|
551
|
+
}
|
|
552
|
+
function legacyAnswerShapeWarnings(questions) {
|
|
553
|
+
return questions.flatMap((record) => {
|
|
554
|
+
if (record.status !== "answered" || !legacyQuestionAnswerRecord(record)) {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
const selectedValues = asStringArray(legacyQuestionAnswerRecord(record)?.selectedValues);
|
|
558
|
+
const selectedLabels = asStringArray(legacyQuestionAnswerRecord(record)?.selectedLabels);
|
|
559
|
+
if (selectedValues.length > 0 && selectedLabels.length > 0) {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
return [{
|
|
563
|
+
questionId: record.id,
|
|
564
|
+
...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
|
|
565
|
+
issue: "Answered question uses a legacy answer shape that is missing selectedValues or selectedLabels.",
|
|
566
|
+
suggestion: "Run `longtable repair-state --cwd <project-path>` to normalize the answer without changing the recorded selection."
|
|
567
|
+
}];
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function numericOtherAnswerWarnings(questions) {
|
|
571
|
+
return questions.flatMap((record) => {
|
|
572
|
+
if (record.status !== "answered" || !selectedValuesForQuestion(record).includes("other")) {
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
const answer = legacyQuestionAnswerRecord(record);
|
|
576
|
+
const raw = typeof answer?.otherText === "string"
|
|
577
|
+
? answer.otherText
|
|
578
|
+
: selectedLabelsForQuestion(record, selectedValuesForQuestion(record))[0] ?? "";
|
|
579
|
+
if (!/^\d+$/.test(raw.trim())) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
const index = Number(raw.trim()) - 1;
|
|
583
|
+
const option = record.prompt.options[index];
|
|
584
|
+
return [{
|
|
585
|
+
questionId: record.id,
|
|
586
|
+
...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
|
|
587
|
+
issue: `Numeric answer "${raw.trim()}" was stored as other text.`,
|
|
588
|
+
...(option ? { suggestion: `Use "${option.value}" (${option.label}) for this checkpoint option.` } : {})
|
|
589
|
+
}];
|
|
590
|
+
});
|
|
591
|
+
}
|
|
500
592
|
function compactLine(value, limit = 160) {
|
|
501
593
|
const compacted = value.replace(/\s+/g, " ").trim();
|
|
502
594
|
return compacted.length > limit ? `${compacted.slice(0, limit - 1)}…` : compacted;
|
|
@@ -854,22 +946,10 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
854
946
|
...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
|
|
855
947
|
timestamp: record.timestamp
|
|
856
948
|
})),
|
|
857
|
-
answerWarnings:
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (!/^\d+$/.test(raw.trim())) {
|
|
862
|
-
return [];
|
|
863
|
-
}
|
|
864
|
-
const index = Number(raw.trim()) - 1;
|
|
865
|
-
const option = record.prompt.options[index];
|
|
866
|
-
return [{
|
|
867
|
-
questionId: record.id,
|
|
868
|
-
...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
|
|
869
|
-
issue: `Numeric answer "${raw.trim()}" was stored as other text.`,
|
|
870
|
-
...(option ? { suggestion: `Use "${option.value}" (${option.label}) for this checkpoint option.` } : {})
|
|
871
|
-
}];
|
|
872
|
-
})
|
|
949
|
+
answerWarnings: [
|
|
950
|
+
...(legacyAnswerShapeWarnings(questions) ?? []),
|
|
951
|
+
...(numericOtherAnswerWarnings(questions) ?? [])
|
|
952
|
+
]
|
|
873
953
|
};
|
|
874
954
|
}
|
|
875
955
|
function buildProjectAgentsMd(project, session) {
|
|
@@ -1070,7 +1150,8 @@ function evidenceRecordsForInvocation(invocation, timestamp) {
|
|
|
1070
1150
|
member.summary ? `Summary: ${member.summary}` : "",
|
|
1071
1151
|
member.claims?.length ? `Claims: ${member.claims.join("; ")}` : "",
|
|
1072
1152
|
member.objections?.length ? `Objections: ${member.objections.join("; ")}` : "",
|
|
1073
|
-
member.openQuestions?.length ? `Open questions: ${member.openQuestions.join("; ")}` : ""
|
|
1153
|
+
member.openQuestions?.length ? `Open questions: ${member.openQuestions.join("; ")}` : "",
|
|
1154
|
+
member.evidenceRefs?.length ? `Evidence refs: ${member.evidenceRefs.join("; ")}` : ""
|
|
1074
1155
|
].filter(Boolean).join("\n"),
|
|
1075
1156
|
linkedInvocationRecordIds: [invocation.id],
|
|
1076
1157
|
linkedQuestionRecordIds: invocation.panelResult.linkedQuestionRecordIds,
|
|
@@ -1125,6 +1206,314 @@ export async function appendInvocationRecordToWorkspace(context, invocation, que
|
|
|
1125
1206
|
await syncCurrentWorkspaceView(context);
|
|
1126
1207
|
return withEvidence;
|
|
1127
1208
|
}
|
|
1209
|
+
function latestPanelInvocation(state) {
|
|
1210
|
+
return (state.invocationLog ?? [])
|
|
1211
|
+
.slice()
|
|
1212
|
+
.reverse()
|
|
1213
|
+
.find((record) => record.intent.kind === "panel" && record.panelResult);
|
|
1214
|
+
}
|
|
1215
|
+
function sanitizeStringArray(value) {
|
|
1216
|
+
if (!Array.isArray(value)) {
|
|
1217
|
+
return undefined;
|
|
1218
|
+
}
|
|
1219
|
+
const normalized = value.filter((entry) => typeof entry === "string");
|
|
1220
|
+
return normalized.length > 0 ? normalized : [];
|
|
1221
|
+
}
|
|
1222
|
+
function isInvocationStatus(value) {
|
|
1223
|
+
return (value === "planned" ||
|
|
1224
|
+
value === "running" ||
|
|
1225
|
+
value === "completed" ||
|
|
1226
|
+
value === "blocked" ||
|
|
1227
|
+
value === "degraded" ||
|
|
1228
|
+
value === "error");
|
|
1229
|
+
}
|
|
1230
|
+
function isInvocationSurface(value) {
|
|
1231
|
+
return (value === "native_parallel" ||
|
|
1232
|
+
value === "native_subagents" ||
|
|
1233
|
+
value === "native_workers" ||
|
|
1234
|
+
value === "generated_skill" ||
|
|
1235
|
+
value === "prompt_alias" ||
|
|
1236
|
+
value === "sequential_fallback" ||
|
|
1237
|
+
value === "file_backed_panel_debate" ||
|
|
1238
|
+
value === "file_backed_debate" ||
|
|
1239
|
+
value === "mcp_transport");
|
|
1240
|
+
}
|
|
1241
|
+
function sanitizePanelMemberResult(member, fallback) {
|
|
1242
|
+
const label = typeof member.label === "string" && member.label.trim()
|
|
1243
|
+
? member.label
|
|
1244
|
+
: fallback?.label ?? member.role;
|
|
1245
|
+
const status = isInvocationStatus(member.status)
|
|
1246
|
+
? member.status
|
|
1247
|
+
: fallback?.status ?? "completed";
|
|
1248
|
+
const claims = sanitizeStringArray(member.claims);
|
|
1249
|
+
const objections = sanitizeStringArray(member.objections);
|
|
1250
|
+
const openQuestions = sanitizeStringArray(member.openQuestions);
|
|
1251
|
+
const evidenceRefs = sanitizeStringArray(member.evidenceRefs);
|
|
1252
|
+
return {
|
|
1253
|
+
role: member.role,
|
|
1254
|
+
label,
|
|
1255
|
+
status,
|
|
1256
|
+
...(typeof member.summary === "string" ? { summary: member.summary } : fallback?.summary ? { summary: fallback.summary } : {}),
|
|
1257
|
+
...(claims !== undefined ? { claims } : fallback?.claims ? { claims: fallback.claims } : {}),
|
|
1258
|
+
...(objections !== undefined ? { objections } : fallback?.objections ? { objections: fallback.objections } : {}),
|
|
1259
|
+
...(openQuestions !== undefined ? { openQuestions } : fallback?.openQuestions ? { openQuestions: fallback.openQuestions } : {}),
|
|
1260
|
+
...(evidenceRefs !== undefined ? { evidenceRefs } : fallback?.evidenceRefs ? { evidenceRefs: fallback.evidenceRefs } : {}),
|
|
1261
|
+
...(typeof member.error === "string" ? { error: member.error } : fallback?.error ? { error: fallback.error } : {})
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function mergePanelMemberResults(existing, incoming = []) {
|
|
1265
|
+
const incomingByRole = new Map(incoming.map((member) => [member.role, member]));
|
|
1266
|
+
const merged = existing.map((member) => {
|
|
1267
|
+
const update = incomingByRole.get(member.role);
|
|
1268
|
+
if (!update) {
|
|
1269
|
+
return sanitizePanelMemberResult(member);
|
|
1270
|
+
}
|
|
1271
|
+
return sanitizePanelMemberResult(update, member);
|
|
1272
|
+
});
|
|
1273
|
+
const existingRoles = new Set(existing.map((member) => member.role));
|
|
1274
|
+
for (const update of incoming) {
|
|
1275
|
+
if (existingRoles.has(update.role)) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
merged.push(sanitizePanelMemberResult(update));
|
|
1279
|
+
}
|
|
1280
|
+
return merged;
|
|
1281
|
+
}
|
|
1282
|
+
function removeEvidenceForInvocation(state, invocationId) {
|
|
1283
|
+
return (state.evidenceRecords ?? []).filter((record) => {
|
|
1284
|
+
const linked = record.linkedInvocationRecordIds ?? [];
|
|
1285
|
+
if (!linked.includes(invocationId)) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
return !(record.sourceKind === "panel" || record.sourceId?.startsWith(`${invocationId}:`));
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
export async function recordPanelResultInWorkspace(options) {
|
|
1292
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
1293
|
+
const targetInvocation = options.result.invocationId
|
|
1294
|
+
? (state.invocationLog ?? []).find((record) => record.id === options.result.invocationId)
|
|
1295
|
+
: latestPanelInvocation(state);
|
|
1296
|
+
if (!targetInvocation) {
|
|
1297
|
+
throw new Error(options.result.invocationId
|
|
1298
|
+
? `No panel invocation found for ${options.result.invocationId}.`
|
|
1299
|
+
: "No panel invocation found to record.");
|
|
1300
|
+
}
|
|
1301
|
+
if (!targetInvocation.panelResult) {
|
|
1302
|
+
throw new Error(`Invocation ${targetInvocation.id} does not have a panel result.`);
|
|
1303
|
+
}
|
|
1304
|
+
if (options.result.status !== undefined && !isInvocationStatus(options.result.status)) {
|
|
1305
|
+
throw new Error(`Invalid panel result status: ${String(options.result.status)}.`);
|
|
1306
|
+
}
|
|
1307
|
+
if (options.result.surface !== undefined && !isInvocationSurface(options.result.surface)) {
|
|
1308
|
+
throw new Error(`Invalid panel result surface: ${String(options.result.surface)}.`);
|
|
1309
|
+
}
|
|
1310
|
+
const timestamp = nowIso();
|
|
1311
|
+
const status = options.result.status ?? "completed";
|
|
1312
|
+
const surface = options.result.surface ?? targetInvocation.panelResult.surface;
|
|
1313
|
+
const updatedInvocation = {
|
|
1314
|
+
...targetInvocation,
|
|
1315
|
+
updatedAt: timestamp,
|
|
1316
|
+
status,
|
|
1317
|
+
surface,
|
|
1318
|
+
panelResult: {
|
|
1319
|
+
...targetInvocation.panelResult,
|
|
1320
|
+
updatedAt: timestamp,
|
|
1321
|
+
status,
|
|
1322
|
+
surface,
|
|
1323
|
+
memberResults: mergePanelMemberResults(targetInvocation.panelResult.memberResults, options.result.memberResults),
|
|
1324
|
+
...(normalizeOptionalString(options.result.synthesis) ? { synthesis: normalizeOptionalString(options.result.synthesis) } : {}),
|
|
1325
|
+
...(normalizeOptionalString(options.result.conflictSummary) ? { conflictSummary: normalizeOptionalString(options.result.conflictSummary) } : {}),
|
|
1326
|
+
...(normalizeOptionalString(options.result.decisionPrompt) ? { decisionPrompt: normalizeOptionalString(options.result.decisionPrompt) } : {})
|
|
1327
|
+
},
|
|
1328
|
+
degradationReason: surface === "native_subagents"
|
|
1329
|
+
? "Provider-native subagent execution was recorded as session-dependent; sequential_fallback remains the required fallback."
|
|
1330
|
+
: surface === "native_workers"
|
|
1331
|
+
? "LongTable-native worker outputs were recorded as structured panel evidence; runtime state remains under .longtable/panel-runs and hidden reasoning/raw tmux logs stay out of handoff."
|
|
1332
|
+
: targetInvocation.degradationReason
|
|
1333
|
+
};
|
|
1334
|
+
const evidenceRecords = evidenceRecordsForInvocation(updatedInvocation, timestamp);
|
|
1335
|
+
const updatedState = {
|
|
1336
|
+
...state,
|
|
1337
|
+
invocationLog: (state.invocationLog ?? []).map((record) => record.id === updatedInvocation.id ? updatedInvocation : record),
|
|
1338
|
+
evidenceRecords: [...removeEvidenceForInvocation(state, updatedInvocation.id), ...evidenceRecords]
|
|
1339
|
+
};
|
|
1340
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updatedState, null, 2), "utf8");
|
|
1341
|
+
await syncCurrentWorkspaceView(options.context);
|
|
1342
|
+
return {
|
|
1343
|
+
invocation: updatedInvocation,
|
|
1344
|
+
evidenceRecords,
|
|
1345
|
+
state: updatedState
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function renderBulletList(values, empty) {
|
|
1349
|
+
return values.length > 0 ? values.map((value) => `- ${value}`) : [`- ${empty}`];
|
|
1350
|
+
}
|
|
1351
|
+
function formatInvocationLine(record) {
|
|
1352
|
+
const roles = record.intent.roles.length > 0 ? record.intent.roles.join(", ") : "auto";
|
|
1353
|
+
return `${record.intent.kind}/${record.intent.mode} via ${record.surface} (${record.status}); roles: ${roles}`;
|
|
1354
|
+
}
|
|
1355
|
+
function renderLatestPanelForHandoff(invocation) {
|
|
1356
|
+
if (!invocation?.panelResult) {
|
|
1357
|
+
return [
|
|
1358
|
+
"## Latest Panel Or Discussion",
|
|
1359
|
+
"- No panel invocation is recorded yet.",
|
|
1360
|
+
"- Start with `lt panel: <what needs review>` or `longtable panel --prompt \"...\" --json`."
|
|
1361
|
+
];
|
|
1362
|
+
}
|
|
1363
|
+
const result = invocation.panelResult;
|
|
1364
|
+
const workerResultGuidance = result.surface === "native_workers"
|
|
1365
|
+
? [
|
|
1366
|
+
"- Native worker note: role-worker outputs have been normalized into this `PanelResult`; use `longtable panel status --run <run_id>` for live worker status when a native worker run id is available.",
|
|
1367
|
+
"- Native worker handoff rule: preserve final summaries, claims, objections, open questions, and evidence references; do not paste hidden reasoning, raw tool traces, or tmux logs into the research handoff."
|
|
1368
|
+
]
|
|
1369
|
+
: [];
|
|
1370
|
+
return [
|
|
1371
|
+
"## Latest Panel Or Discussion",
|
|
1372
|
+
`- Invocation: ${invocation.id}`,
|
|
1373
|
+
`- Record: ${formatInvocationLine(invocation)}`,
|
|
1374
|
+
`- Result status: ${result.status}`,
|
|
1375
|
+
`- Execution surface: ${result.surface}`,
|
|
1376
|
+
...workerResultGuidance,
|
|
1377
|
+
...(invocation.degradationReason ? [`- Fallback note: ${invocation.degradationReason}`] : []),
|
|
1378
|
+
...(result.synthesis ? [`- Synthesis: ${result.synthesis}`] : []),
|
|
1379
|
+
...(result.conflictSummary ? [`- Conflict summary: ${result.conflictSummary}`] : []),
|
|
1380
|
+
...(result.decisionPrompt ? [`- Decision prompt: ${result.decisionPrompt}`] : []),
|
|
1381
|
+
"",
|
|
1382
|
+
"### Role Outputs",
|
|
1383
|
+
...result.memberResults.map((member) => {
|
|
1384
|
+
const details = [
|
|
1385
|
+
member.summary,
|
|
1386
|
+
...(member.claims ?? []).map((claim) => `claim: ${claim}`),
|
|
1387
|
+
...(member.objections ?? []).map((objection) => `objection: ${objection}`),
|
|
1388
|
+
...(member.openQuestions ?? []).map((question) => `open question: ${question}`),
|
|
1389
|
+
...(member.evidenceRefs ?? []).map((ref) => `evidence: ${ref}`),
|
|
1390
|
+
...(member.error ? [`error: ${member.error}`] : [])
|
|
1391
|
+
].filter(Boolean);
|
|
1392
|
+
return `- ${member.label} (${member.role}): ${details.length > 0 ? compactLine(details.join("; "), 220) : member.status}`;
|
|
1393
|
+
}),
|
|
1394
|
+
...(result.memberResults.some((member) => (member.evidenceRefs ?? []).length > 0)
|
|
1395
|
+
? [
|
|
1396
|
+
"",
|
|
1397
|
+
"### Role Evidence References",
|
|
1398
|
+
...result.memberResults.flatMap((member) => (member.evidenceRefs ?? []).map((ref) => `- ${member.label} (${member.role}): ${ref}`))
|
|
1399
|
+
]
|
|
1400
|
+
: []),
|
|
1401
|
+
...(result.status === "planned"
|
|
1402
|
+
? [
|
|
1403
|
+
"",
|
|
1404
|
+
"### Missing Persistence Step",
|
|
1405
|
+
"- This panel is only planned. After the provider returns role outputs, record them with:",
|
|
1406
|
+
` \`longtable panel record --invocation ${invocation.id} --result-file panel-result.json\``
|
|
1407
|
+
]
|
|
1408
|
+
: [])
|
|
1409
|
+
];
|
|
1410
|
+
}
|
|
1411
|
+
function renderWorkflowGuidance(context, latestInvocation) {
|
|
1412
|
+
const cwdFlag = `--cwd "${context.project.projectPath.replaceAll("\"", "\\\"")}"`;
|
|
1413
|
+
const recordCommand = latestInvocation?.panelResult
|
|
1414
|
+
? `longtable panel record ${cwdFlag} --invocation ${latestInvocation.id} --result-file panel-result.json`
|
|
1415
|
+
: `longtable panel ${cwdFlag} --prompt "<panel question>" --json`;
|
|
1416
|
+
return [
|
|
1417
|
+
"## Continuation Workflow",
|
|
1418
|
+
"",
|
|
1419
|
+
"### Provider-Neutral Path",
|
|
1420
|
+
"Use this when OMX is not installed or when the researcher wants a plain CLI/native-agent workflow.",
|
|
1421
|
+
"",
|
|
1422
|
+
"1. Open the project in Codex or Claude Code.",
|
|
1423
|
+
"2. Use `$longtable-start` if no usable Research Specification exists; otherwise use `$longtable-interview` or `lt panel: ...` for the next bounded decision.",
|
|
1424
|
+
"3. When a panel or native worker run produces real role outputs, persist the structured result:",
|
|
1425
|
+
` \`${recordCommand}\``,
|
|
1426
|
+
" Native worker outputs should be final role summaries only: summary, claims, objections, open questions, and evidence refs.",
|
|
1427
|
+
"4. Inspect unincorporated evidence:",
|
|
1428
|
+
` \`longtable spec unincorporated ${cwdFlag}\``,
|
|
1429
|
+
"5. Propose a Research Specification patch before applying a changed research direction:",
|
|
1430
|
+
` \`longtable spec propose ${cwdFlag} --spec-file updated-spec.json --rationale \"Panel/discussion handoff\"\``,
|
|
1431
|
+
"6. Apply only after the researcher confirms the decision:",
|
|
1432
|
+
` \`longtable spec apply ${cwdFlag} --patch-id <spec_patch_id>\``,
|
|
1433
|
+
"",
|
|
1434
|
+
"### Optional OMX Path",
|
|
1435
|
+
"Use this only when OMX is installed. The handoff packet can be pasted into `$ralplan` for a plan/test-spec pass, then `$ralph` can execute the approved work until verification. LongTable should remain the research-state source of truth; OMX is only the execution loop.",
|
|
1436
|
+
"",
|
|
1437
|
+
"Suggested OMX prompt:",
|
|
1438
|
+
"```text",
|
|
1439
|
+
"$ralplan: Use the LongTable handoff below as the research-state contract. Produce a PRD/test-spec style execution plan, preserve unresolved panel disagreements, and do not change the Research Specification without a LongTable checkpoint.",
|
|
1440
|
+
"```",
|
|
1441
|
+
"",
|
|
1442
|
+
"Then, after the plan is accepted:",
|
|
1443
|
+
"```text",
|
|
1444
|
+
"$ralph: Execute the approved LongTable handoff plan. Verify artifacts, then record any panel evidence or spec patch through LongTable commands.",
|
|
1445
|
+
"```"
|
|
1446
|
+
];
|
|
1447
|
+
}
|
|
1448
|
+
export async function createWorkspaceHandoff(options) {
|
|
1449
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
1450
|
+
const createdAt = nowIso();
|
|
1451
|
+
const id = createId("handoff");
|
|
1452
|
+
const locale = normalizeLocale(options.context.session.locale ?? options.context.project.locale);
|
|
1453
|
+
const openQuestions = options.context.session.openQuestions && options.context.session.openQuestions.length > 0
|
|
1454
|
+
? options.context.session.openQuestions
|
|
1455
|
+
: buildOpenQuestions(options.context.session);
|
|
1456
|
+
const nextAction = options.context.session.nextAction ?? buildNextAction(options.context.session);
|
|
1457
|
+
const latestInvocation = latestPanelInvocation(state);
|
|
1458
|
+
const pendingQuestions = (state.questionLog ?? []).filter((record) => record.status === "pending");
|
|
1459
|
+
const unincorporatedEvidence = (state.evidenceRecords ?? []).filter((record) => !record.incorporatedByRevisionId);
|
|
1460
|
+
const proposedPatches = (state.specPatches ?? []).filter((patch) => patch.status === "proposed");
|
|
1461
|
+
const outputPath = options.outputPath
|
|
1462
|
+
? resolve(options.outputPath)
|
|
1463
|
+
: join(options.context.metaDir, "handoffs", `${id}-${slugify(options.context.session.researchSpecification?.title ?? options.context.project.projectName) || "longtable"}.md`);
|
|
1464
|
+
const content = [
|
|
1465
|
+
"# LongTable Handoff",
|
|
1466
|
+
"",
|
|
1467
|
+
`Generated: ${createdAt}`,
|
|
1468
|
+
`Project: ${options.context.project.projectName}`,
|
|
1469
|
+
`Project path: ${options.context.project.projectPath}`,
|
|
1470
|
+
"",
|
|
1471
|
+
"## Current Objective",
|
|
1472
|
+
`- Goal: ${options.context.session.currentGoal}`,
|
|
1473
|
+
...(options.context.session.currentBlocker ? [`- Blocker: ${options.context.session.currentBlocker}`] : []),
|
|
1474
|
+
...(options.context.session.protectedDecision ? [`- Protected decision: ${options.context.session.protectedDecision}`] : []),
|
|
1475
|
+
`- Next action: ${nextAction}`,
|
|
1476
|
+
"",
|
|
1477
|
+
"## Research Specification Status",
|
|
1478
|
+
...(options.context.session.researchSpecification
|
|
1479
|
+
? renderResearchSpecificationSummary(options.context.session.researchSpecification, locale)
|
|
1480
|
+
: [
|
|
1481
|
+
"- No Research Specification is available yet.",
|
|
1482
|
+
"- Start or resume `$longtable-start` before treating the project direction as settled."
|
|
1483
|
+
]),
|
|
1484
|
+
"",
|
|
1485
|
+
...renderLatestPanelForHandoff(latestInvocation),
|
|
1486
|
+
"",
|
|
1487
|
+
"## Pending Researcher Decisions",
|
|
1488
|
+
...renderBulletList(pendingQuestions.map((record) => `${record.id}: ${record.prompt.question} (${formatQuestionOptionValues(record).join("/")})`), "No pending researcher decision questions."),
|
|
1489
|
+
"",
|
|
1490
|
+
"## Unincorporated Evidence",
|
|
1491
|
+
...renderBulletList(unincorporatedEvidence.slice(-10).reverse().map((record) => `${record.id} [${record.sourceKind}]: ${compactLine(record.summary, 180)}`), "No unincorporated evidence records."),
|
|
1492
|
+
"",
|
|
1493
|
+
"## Proposed Specification Patches",
|
|
1494
|
+
...renderBulletList(proposedPatches.map((patch) => `${patch.id}: ${patch.title} (${patch.changes.length} changes)`), "No proposed Research Specification patches."),
|
|
1495
|
+
"",
|
|
1496
|
+
"## Open Questions",
|
|
1497
|
+
...renderBulletList(openQuestions, "No open questions recorded."),
|
|
1498
|
+
"",
|
|
1499
|
+
...renderWorkflowGuidance(options.context, latestInvocation),
|
|
1500
|
+
"",
|
|
1501
|
+
"## Stop Condition",
|
|
1502
|
+
"- Stop when the next research decision is either confirmed by the researcher, preserved as an explicit open tension, or represented as a proposed Research Specification patch waiting for confirmation."
|
|
1503
|
+
].join("\n");
|
|
1504
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
1505
|
+
await writeFile(outputPath, `${content}\n`, "utf8");
|
|
1506
|
+
return {
|
|
1507
|
+
id,
|
|
1508
|
+
createdAt,
|
|
1509
|
+
path: outputPath,
|
|
1510
|
+
content,
|
|
1511
|
+
sourceEvidenceIds: unincorporatedEvidence.map((record) => record.id),
|
|
1512
|
+
pendingQuestionIds: pendingQuestions.map((record) => record.id),
|
|
1513
|
+
proposedPatchIds: proposedPatches.map((patch) => patch.id),
|
|
1514
|
+
...(latestInvocation ? { latestInvocationId: latestInvocation.id } : {})
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1128
1517
|
function createId(prefix) {
|
|
1129
1518
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1130
1519
|
}
|
|
@@ -2933,7 +3322,51 @@ export async function repairWorkspaceStateConsistency(options) {
|
|
|
2933
3322
|
})
|
|
2934
3323
|
};
|
|
2935
3324
|
}
|
|
2936
|
-
|
|
3325
|
+
const timestamp = nowIso();
|
|
3326
|
+
const repairedQuestionLog = (updated.questionLog ?? []).map((record) => {
|
|
3327
|
+
if (record.status !== "answered") {
|
|
3328
|
+
return record;
|
|
3329
|
+
}
|
|
3330
|
+
const answer = legacyQuestionAnswerRecord(record);
|
|
3331
|
+
if (!answer) {
|
|
3332
|
+
return record;
|
|
3333
|
+
}
|
|
3334
|
+
const selectedValues = selectedValuesForQuestion(record);
|
|
3335
|
+
if (selectedValues.length === 0) {
|
|
3336
|
+
return record;
|
|
3337
|
+
}
|
|
3338
|
+
const selectedLabels = selectedLabelsForQuestion(record, selectedValues);
|
|
3339
|
+
const needsRepair = asStringArray(answer.selectedValues).length === 0 ||
|
|
3340
|
+
asStringArray(answer.selectedLabels).length === 0 ||
|
|
3341
|
+
typeof answer.promptId !== "string" ||
|
|
3342
|
+
!QUESTION_SURFACES.has(answer.surface);
|
|
3343
|
+
if (!needsRepair) {
|
|
3344
|
+
return record;
|
|
3345
|
+
}
|
|
3346
|
+
repaired.push(`normalized legacy answer shape for question ${record.id}`);
|
|
3347
|
+
const normalizedAnswer = {
|
|
3348
|
+
...answer,
|
|
3349
|
+
promptId: typeof answer.promptId === "string" ? answer.promptId : record.prompt.id,
|
|
3350
|
+
selectedValues,
|
|
3351
|
+
selectedLabels,
|
|
3352
|
+
...(typeof answer.otherText === "string" && answer.otherText.trim() ? { otherText: answer.otherText } : {}),
|
|
3353
|
+
...(typeof answer.rationale === "string" && answer.rationale.trim() ? { rationale: answer.rationale } : {}),
|
|
3354
|
+
...(answer.provider === "codex" || answer.provider === "claude" ? { provider: answer.provider } : {}),
|
|
3355
|
+
surface: QUESTION_SURFACES.has(answer.surface) ? answer.surface : "numbered"
|
|
3356
|
+
};
|
|
3357
|
+
return {
|
|
3358
|
+
...record,
|
|
3359
|
+
updatedAt: timestamp,
|
|
3360
|
+
answer: normalizedAnswer
|
|
3361
|
+
};
|
|
3362
|
+
});
|
|
3363
|
+
if (repairedQuestionLog.some((record, index) => record !== (updated.questionLog ?? [])[index])) {
|
|
3364
|
+
updated = {
|
|
3365
|
+
...updated,
|
|
3366
|
+
questionLog: repairedQuestionLog
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
if (repaired.length > 0 && !options.dryRun) {
|
|
2937
3370
|
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
2938
3371
|
await syncCurrentWorkspaceView(options.context);
|
|
2939
3372
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
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.57",
|
|
33
|
+
"@longtable/core": "0.1.57",
|
|
34
|
+
"@longtable/memory": "0.1.57",
|
|
35
|
+
"@longtable/provider-claude": "0.1.57",
|
|
36
|
+
"@longtable/provider-codex": "0.1.57",
|
|
37
|
+
"@longtable/setup": "0.1.57"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|