@longtable/cli 0.1.54 → 0.1.56

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.
@@ -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;
@@ -636,6 +636,18 @@ function changeSummaryForRevision(changes) {
636
636
  }
637
637
  return changes.slice(0, 12).map((change) => change.summary);
638
638
  }
639
+ function researchSpecificationAnswerConfirms(answer) {
640
+ return answer === "confirm_specification";
641
+ }
642
+ function researchSpecificationAnswerStatus(answer) {
643
+ if (researchSpecificationAnswerConfirms(answer)) {
644
+ return "confirmed";
645
+ }
646
+ if (answer === "keep_open") {
647
+ return "deferred";
648
+ }
649
+ return "draft";
650
+ }
639
651
  export function applyResearchSpecificationAuditUpdate(state, options) {
640
652
  const previous = state.researchSpecification;
641
653
  const incomingEvidenceIds = mergeStringLists(options.patch?.sourceEvidenceIds, options.specification.sourceEvidenceIds, options.sourceEvidenceIds);
@@ -1058,7 +1070,8 @@ function evidenceRecordsForInvocation(invocation, timestamp) {
1058
1070
  member.summary ? `Summary: ${member.summary}` : "",
1059
1071
  member.claims?.length ? `Claims: ${member.claims.join("; ")}` : "",
1060
1072
  member.objections?.length ? `Objections: ${member.objections.join("; ")}` : "",
1061
- member.openQuestions?.length ? `Open questions: ${member.openQuestions.join("; ")}` : ""
1073
+ member.openQuestions?.length ? `Open questions: ${member.openQuestions.join("; ")}` : "",
1074
+ member.evidenceRefs?.length ? `Evidence refs: ${member.evidenceRefs.join("; ")}` : ""
1062
1075
  ].filter(Boolean).join("\n"),
1063
1076
  linkedInvocationRecordIds: [invocation.id],
1064
1077
  linkedQuestionRecordIds: invocation.panelResult.linkedQuestionRecordIds,
@@ -1113,6 +1126,314 @@ export async function appendInvocationRecordToWorkspace(context, invocation, que
1113
1126
  await syncCurrentWorkspaceView(context);
1114
1127
  return withEvidence;
1115
1128
  }
1129
+ function latestPanelInvocation(state) {
1130
+ return (state.invocationLog ?? [])
1131
+ .slice()
1132
+ .reverse()
1133
+ .find((record) => record.intent.kind === "panel" && record.panelResult);
1134
+ }
1135
+ function sanitizeStringArray(value) {
1136
+ if (!Array.isArray(value)) {
1137
+ return undefined;
1138
+ }
1139
+ const normalized = value.filter((entry) => typeof entry === "string");
1140
+ return normalized.length > 0 ? normalized : [];
1141
+ }
1142
+ function isInvocationStatus(value) {
1143
+ return (value === "planned" ||
1144
+ value === "running" ||
1145
+ value === "completed" ||
1146
+ value === "blocked" ||
1147
+ value === "degraded" ||
1148
+ value === "error");
1149
+ }
1150
+ function isInvocationSurface(value) {
1151
+ return (value === "native_parallel" ||
1152
+ value === "native_subagents" ||
1153
+ value === "native_workers" ||
1154
+ value === "generated_skill" ||
1155
+ value === "prompt_alias" ||
1156
+ value === "sequential_fallback" ||
1157
+ value === "file_backed_panel_debate" ||
1158
+ value === "file_backed_debate" ||
1159
+ value === "mcp_transport");
1160
+ }
1161
+ function sanitizePanelMemberResult(member, fallback) {
1162
+ const label = typeof member.label === "string" && member.label.trim()
1163
+ ? member.label
1164
+ : fallback?.label ?? member.role;
1165
+ const status = isInvocationStatus(member.status)
1166
+ ? member.status
1167
+ : fallback?.status ?? "completed";
1168
+ const claims = sanitizeStringArray(member.claims);
1169
+ const objections = sanitizeStringArray(member.objections);
1170
+ const openQuestions = sanitizeStringArray(member.openQuestions);
1171
+ const evidenceRefs = sanitizeStringArray(member.evidenceRefs);
1172
+ return {
1173
+ role: member.role,
1174
+ label,
1175
+ status,
1176
+ ...(typeof member.summary === "string" ? { summary: member.summary } : fallback?.summary ? { summary: fallback.summary } : {}),
1177
+ ...(claims !== undefined ? { claims } : fallback?.claims ? { claims: fallback.claims } : {}),
1178
+ ...(objections !== undefined ? { objections } : fallback?.objections ? { objections: fallback.objections } : {}),
1179
+ ...(openQuestions !== undefined ? { openQuestions } : fallback?.openQuestions ? { openQuestions: fallback.openQuestions } : {}),
1180
+ ...(evidenceRefs !== undefined ? { evidenceRefs } : fallback?.evidenceRefs ? { evidenceRefs: fallback.evidenceRefs } : {}),
1181
+ ...(typeof member.error === "string" ? { error: member.error } : fallback?.error ? { error: fallback.error } : {})
1182
+ };
1183
+ }
1184
+ function mergePanelMemberResults(existing, incoming = []) {
1185
+ const incomingByRole = new Map(incoming.map((member) => [member.role, member]));
1186
+ const merged = existing.map((member) => {
1187
+ const update = incomingByRole.get(member.role);
1188
+ if (!update) {
1189
+ return sanitizePanelMemberResult(member);
1190
+ }
1191
+ return sanitizePanelMemberResult(update, member);
1192
+ });
1193
+ const existingRoles = new Set(existing.map((member) => member.role));
1194
+ for (const update of incoming) {
1195
+ if (existingRoles.has(update.role)) {
1196
+ continue;
1197
+ }
1198
+ merged.push(sanitizePanelMemberResult(update));
1199
+ }
1200
+ return merged;
1201
+ }
1202
+ function removeEvidenceForInvocation(state, invocationId) {
1203
+ return (state.evidenceRecords ?? []).filter((record) => {
1204
+ const linked = record.linkedInvocationRecordIds ?? [];
1205
+ if (!linked.includes(invocationId)) {
1206
+ return true;
1207
+ }
1208
+ return !(record.sourceKind === "panel" || record.sourceId?.startsWith(`${invocationId}:`));
1209
+ });
1210
+ }
1211
+ export async function recordPanelResultInWorkspace(options) {
1212
+ const state = await loadResearchState(options.context.stateFilePath);
1213
+ const targetInvocation = options.result.invocationId
1214
+ ? (state.invocationLog ?? []).find((record) => record.id === options.result.invocationId)
1215
+ : latestPanelInvocation(state);
1216
+ if (!targetInvocation) {
1217
+ throw new Error(options.result.invocationId
1218
+ ? `No panel invocation found for ${options.result.invocationId}.`
1219
+ : "No panel invocation found to record.");
1220
+ }
1221
+ if (!targetInvocation.panelResult) {
1222
+ throw new Error(`Invocation ${targetInvocation.id} does not have a panel result.`);
1223
+ }
1224
+ if (options.result.status !== undefined && !isInvocationStatus(options.result.status)) {
1225
+ throw new Error(`Invalid panel result status: ${String(options.result.status)}.`);
1226
+ }
1227
+ if (options.result.surface !== undefined && !isInvocationSurface(options.result.surface)) {
1228
+ throw new Error(`Invalid panel result surface: ${String(options.result.surface)}.`);
1229
+ }
1230
+ const timestamp = nowIso();
1231
+ const status = options.result.status ?? "completed";
1232
+ const surface = options.result.surface ?? targetInvocation.panelResult.surface;
1233
+ const updatedInvocation = {
1234
+ ...targetInvocation,
1235
+ updatedAt: timestamp,
1236
+ status,
1237
+ surface,
1238
+ panelResult: {
1239
+ ...targetInvocation.panelResult,
1240
+ updatedAt: timestamp,
1241
+ status,
1242
+ surface,
1243
+ memberResults: mergePanelMemberResults(targetInvocation.panelResult.memberResults, options.result.memberResults),
1244
+ ...(normalizeOptionalString(options.result.synthesis) ? { synthesis: normalizeOptionalString(options.result.synthesis) } : {}),
1245
+ ...(normalizeOptionalString(options.result.conflictSummary) ? { conflictSummary: normalizeOptionalString(options.result.conflictSummary) } : {}),
1246
+ ...(normalizeOptionalString(options.result.decisionPrompt) ? { decisionPrompt: normalizeOptionalString(options.result.decisionPrompt) } : {})
1247
+ },
1248
+ degradationReason: surface === "native_subagents"
1249
+ ? "Provider-native subagent execution was recorded as session-dependent; sequential_fallback remains the required fallback."
1250
+ : surface === "native_workers"
1251
+ ? "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."
1252
+ : targetInvocation.degradationReason
1253
+ };
1254
+ const evidenceRecords = evidenceRecordsForInvocation(updatedInvocation, timestamp);
1255
+ const updatedState = {
1256
+ ...state,
1257
+ invocationLog: (state.invocationLog ?? []).map((record) => record.id === updatedInvocation.id ? updatedInvocation : record),
1258
+ evidenceRecords: [...removeEvidenceForInvocation(state, updatedInvocation.id), ...evidenceRecords]
1259
+ };
1260
+ await writeFile(options.context.stateFilePath, JSON.stringify(updatedState, null, 2), "utf8");
1261
+ await syncCurrentWorkspaceView(options.context);
1262
+ return {
1263
+ invocation: updatedInvocation,
1264
+ evidenceRecords,
1265
+ state: updatedState
1266
+ };
1267
+ }
1268
+ function renderBulletList(values, empty) {
1269
+ return values.length > 0 ? values.map((value) => `- ${value}`) : [`- ${empty}`];
1270
+ }
1271
+ function formatInvocationLine(record) {
1272
+ const roles = record.intent.roles.length > 0 ? record.intent.roles.join(", ") : "auto";
1273
+ return `${record.intent.kind}/${record.intent.mode} via ${record.surface} (${record.status}); roles: ${roles}`;
1274
+ }
1275
+ function renderLatestPanelForHandoff(invocation) {
1276
+ if (!invocation?.panelResult) {
1277
+ return [
1278
+ "## Latest Panel Or Discussion",
1279
+ "- No panel invocation is recorded yet.",
1280
+ "- Start with `lt panel: <what needs review>` or `longtable panel --prompt \"...\" --json`."
1281
+ ];
1282
+ }
1283
+ const result = invocation.panelResult;
1284
+ const workerResultGuidance = result.surface === "native_workers"
1285
+ ? [
1286
+ "- 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.",
1287
+ "- 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."
1288
+ ]
1289
+ : [];
1290
+ return [
1291
+ "## Latest Panel Or Discussion",
1292
+ `- Invocation: ${invocation.id}`,
1293
+ `- Record: ${formatInvocationLine(invocation)}`,
1294
+ `- Result status: ${result.status}`,
1295
+ `- Execution surface: ${result.surface}`,
1296
+ ...workerResultGuidance,
1297
+ ...(invocation.degradationReason ? [`- Fallback note: ${invocation.degradationReason}`] : []),
1298
+ ...(result.synthesis ? [`- Synthesis: ${result.synthesis}`] : []),
1299
+ ...(result.conflictSummary ? [`- Conflict summary: ${result.conflictSummary}`] : []),
1300
+ ...(result.decisionPrompt ? [`- Decision prompt: ${result.decisionPrompt}`] : []),
1301
+ "",
1302
+ "### Role Outputs",
1303
+ ...result.memberResults.map((member) => {
1304
+ const details = [
1305
+ member.summary,
1306
+ ...(member.claims ?? []).map((claim) => `claim: ${claim}`),
1307
+ ...(member.objections ?? []).map((objection) => `objection: ${objection}`),
1308
+ ...(member.openQuestions ?? []).map((question) => `open question: ${question}`),
1309
+ ...(member.evidenceRefs ?? []).map((ref) => `evidence: ${ref}`),
1310
+ ...(member.error ? [`error: ${member.error}`] : [])
1311
+ ].filter(Boolean);
1312
+ return `- ${member.label} (${member.role}): ${details.length > 0 ? compactLine(details.join("; "), 220) : member.status}`;
1313
+ }),
1314
+ ...(result.memberResults.some((member) => (member.evidenceRefs ?? []).length > 0)
1315
+ ? [
1316
+ "",
1317
+ "### Role Evidence References",
1318
+ ...result.memberResults.flatMap((member) => (member.evidenceRefs ?? []).map((ref) => `- ${member.label} (${member.role}): ${ref}`))
1319
+ ]
1320
+ : []),
1321
+ ...(result.status === "planned"
1322
+ ? [
1323
+ "",
1324
+ "### Missing Persistence Step",
1325
+ "- This panel is only planned. After the provider returns role outputs, record them with:",
1326
+ ` \`longtable panel record --invocation ${invocation.id} --result-file panel-result.json\``
1327
+ ]
1328
+ : [])
1329
+ ];
1330
+ }
1331
+ function renderWorkflowGuidance(context, latestInvocation) {
1332
+ const cwdFlag = `--cwd "${context.project.projectPath.replaceAll("\"", "\\\"")}"`;
1333
+ const recordCommand = latestInvocation?.panelResult
1334
+ ? `longtable panel record ${cwdFlag} --invocation ${latestInvocation.id} --result-file panel-result.json`
1335
+ : `longtable panel ${cwdFlag} --prompt "<panel question>" --json`;
1336
+ return [
1337
+ "## Continuation Workflow",
1338
+ "",
1339
+ "### Provider-Neutral Path",
1340
+ "Use this when OMX is not installed or when the researcher wants a plain CLI/native-agent workflow.",
1341
+ "",
1342
+ "1. Open the project in Codex or Claude Code.",
1343
+ "2. Use `$longtable-start` if no usable Research Specification exists; otherwise use `$longtable-interview` or `lt panel: ...` for the next bounded decision.",
1344
+ "3. When a panel or native worker run produces real role outputs, persist the structured result:",
1345
+ ` \`${recordCommand}\``,
1346
+ " Native worker outputs should be final role summaries only: summary, claims, objections, open questions, and evidence refs.",
1347
+ "4. Inspect unincorporated evidence:",
1348
+ ` \`longtable spec unincorporated ${cwdFlag}\``,
1349
+ "5. Propose a Research Specification patch before applying a changed research direction:",
1350
+ ` \`longtable spec propose ${cwdFlag} --spec-file updated-spec.json --rationale \"Panel/discussion handoff\"\``,
1351
+ "6. Apply only after the researcher confirms the decision:",
1352
+ ` \`longtable spec apply ${cwdFlag} --patch-id <spec_patch_id>\``,
1353
+ "",
1354
+ "### Optional OMX Path",
1355
+ "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.",
1356
+ "",
1357
+ "Suggested OMX prompt:",
1358
+ "```text",
1359
+ "$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.",
1360
+ "```",
1361
+ "",
1362
+ "Then, after the plan is accepted:",
1363
+ "```text",
1364
+ "$ralph: Execute the approved LongTable handoff plan. Verify artifacts, then record any panel evidence or spec patch through LongTable commands.",
1365
+ "```"
1366
+ ];
1367
+ }
1368
+ export async function createWorkspaceHandoff(options) {
1369
+ const state = await loadResearchState(options.context.stateFilePath);
1370
+ const createdAt = nowIso();
1371
+ const id = createId("handoff");
1372
+ const locale = normalizeLocale(options.context.session.locale ?? options.context.project.locale);
1373
+ const openQuestions = options.context.session.openQuestions && options.context.session.openQuestions.length > 0
1374
+ ? options.context.session.openQuestions
1375
+ : buildOpenQuestions(options.context.session);
1376
+ const nextAction = options.context.session.nextAction ?? buildNextAction(options.context.session);
1377
+ const latestInvocation = latestPanelInvocation(state);
1378
+ const pendingQuestions = (state.questionLog ?? []).filter((record) => record.status === "pending");
1379
+ const unincorporatedEvidence = (state.evidenceRecords ?? []).filter((record) => !record.incorporatedByRevisionId);
1380
+ const proposedPatches = (state.specPatches ?? []).filter((patch) => patch.status === "proposed");
1381
+ const outputPath = options.outputPath
1382
+ ? resolve(options.outputPath)
1383
+ : join(options.context.metaDir, "handoffs", `${id}-${slugify(options.context.session.researchSpecification?.title ?? options.context.project.projectName) || "longtable"}.md`);
1384
+ const content = [
1385
+ "# LongTable Handoff",
1386
+ "",
1387
+ `Generated: ${createdAt}`,
1388
+ `Project: ${options.context.project.projectName}`,
1389
+ `Project path: ${options.context.project.projectPath}`,
1390
+ "",
1391
+ "## Current Objective",
1392
+ `- Goal: ${options.context.session.currentGoal}`,
1393
+ ...(options.context.session.currentBlocker ? [`- Blocker: ${options.context.session.currentBlocker}`] : []),
1394
+ ...(options.context.session.protectedDecision ? [`- Protected decision: ${options.context.session.protectedDecision}`] : []),
1395
+ `- Next action: ${nextAction}`,
1396
+ "",
1397
+ "## Research Specification Status",
1398
+ ...(options.context.session.researchSpecification
1399
+ ? renderResearchSpecificationSummary(options.context.session.researchSpecification, locale)
1400
+ : [
1401
+ "- No Research Specification is available yet.",
1402
+ "- Start or resume `$longtable-start` before treating the project direction as settled."
1403
+ ]),
1404
+ "",
1405
+ ...renderLatestPanelForHandoff(latestInvocation),
1406
+ "",
1407
+ "## Pending Researcher Decisions",
1408
+ ...renderBulletList(pendingQuestions.map((record) => `${record.id}: ${record.prompt.question} (${formatQuestionOptionValues(record).join("/")})`), "No pending researcher decision questions."),
1409
+ "",
1410
+ "## Unincorporated Evidence",
1411
+ ...renderBulletList(unincorporatedEvidence.slice(-10).reverse().map((record) => `${record.id} [${record.sourceKind}]: ${compactLine(record.summary, 180)}`), "No unincorporated evidence records."),
1412
+ "",
1413
+ "## Proposed Specification Patches",
1414
+ ...renderBulletList(proposedPatches.map((patch) => `${patch.id}: ${patch.title} (${patch.changes.length} changes)`), "No proposed Research Specification patches."),
1415
+ "",
1416
+ "## Open Questions",
1417
+ ...renderBulletList(openQuestions, "No open questions recorded."),
1418
+ "",
1419
+ ...renderWorkflowGuidance(options.context, latestInvocation),
1420
+ "",
1421
+ "## Stop Condition",
1422
+ "- 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."
1423
+ ].join("\n");
1424
+ await mkdir(dirname(outputPath), { recursive: true });
1425
+ await writeFile(outputPath, `${content}\n`, "utf8");
1426
+ return {
1427
+ id,
1428
+ createdAt,
1429
+ path: outputPath,
1430
+ content,
1431
+ sourceEvidenceIds: unincorporatedEvidence.map((record) => record.id),
1432
+ pendingQuestionIds: pendingQuestions.map((record) => record.id),
1433
+ proposedPatchIds: proposedPatches.map((patch) => patch.id),
1434
+ ...(latestInvocation ? { latestInvocationId: latestInvocation.id } : {})
1435
+ };
1436
+ }
1116
1437
  function createId(prefix) {
1117
1438
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1118
1439
  }
@@ -2723,7 +3044,56 @@ export async function answerWorkspaceQuestion(options) {
2723
3044
  invocationLog: (state.invocationLog ?? []).map((record) => updateInvocationWithDecision(record, question.id, decision.id))
2724
3045
  };
2725
3046
  const withDecision = appendDecisionToResearchState(withQuestion, decision);
2726
- const updated = resolveQuestionObligationByQuestionId(withDecision, question.id, decision.id);
3047
+ let updated = resolveQuestionObligationByQuestionId(withDecision, question.id, decision.id);
3048
+ if (question.prompt.checkpointKey === "research_specification_confirmation") {
3049
+ const specification = updated.researchSpecification ?? options.context.session.researchSpecification;
3050
+ const selectedAnswer = answer.selectedValues[0];
3051
+ if (specification) {
3052
+ const nextStatus = researchSpecificationAnswerStatus(selectedAnswer);
3053
+ const confirmedSpecification = {
3054
+ ...specification,
3055
+ status: nextStatus,
3056
+ updatedAt: timestamp,
3057
+ ...(nextStatus === "confirmed" ? { confirmedAt: specification.confirmedAt ?? timestamp } : {})
3058
+ };
3059
+ const withHookStatus = {
3060
+ ...updated,
3061
+ hooks: (updated.hooks ?? []).map((hook) => {
3062
+ if (hook.id !== confirmedSpecification.sourceHookId) {
3063
+ return hook;
3064
+ }
3065
+ return {
3066
+ ...hook,
3067
+ status: nextStatus === "draft" ? "active" : nextStatus,
3068
+ updatedAt: timestamp,
3069
+ researchSpecification: confirmedSpecification,
3070
+ linkedQuestionRecordIds: mergeStringLists(hook.linkedQuestionRecordIds, [question.id]),
3071
+ linkedDecisionRecordIds: mergeStringLists(hook.linkedDecisionRecordIds, [decision.id])
3072
+ };
3073
+ })
3074
+ };
3075
+ const sourceEvidenceIds = (withHookStatus.evidenceRecords ?? [])
3076
+ .filter((record) => record.sourceHookId && record.sourceHookId === confirmedSpecification.sourceHookId)
3077
+ .map((record) => record.id);
3078
+ updated = applyResearchSpecificationAuditUpdate(withHookStatus, {
3079
+ specification: confirmedSpecification,
3080
+ timestamp,
3081
+ source: "decision",
3082
+ title: `Research Specification confirmation: ${confirmedSpecification.title}`,
3083
+ rationale: `Research Specification confirmation answer: ${selectedAnswer}`,
3084
+ sourceEvidenceIds,
3085
+ questionRecordId: question.id,
3086
+ decisionRecordId: decision.id,
3087
+ createDecisionRecord: false
3088
+ }).state;
3089
+ options.context.session = {
3090
+ ...options.context.session,
3091
+ researchSpecification: confirmedSpecification,
3092
+ lastUpdatedAt: timestamp
3093
+ };
3094
+ await writeFile(options.context.sessionFilePath, JSON.stringify(options.context.session, null, 2), "utf8");
3095
+ }
3096
+ }
2727
3097
  await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
2728
3098
  await syncCurrentWorkspaceView(options.context);
2729
3099
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
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.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"
32
+ "@longtable/checkpoints": "0.1.56",
33
+ "@longtable/core": "0.1.56",
34
+ "@longtable/memory": "0.1.56",
35
+ "@longtable/provider-claude": "0.1.56",
36
+ "@longtable/provider-codex": "0.1.56",
37
+ "@longtable/setup": "0.1.56"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",