@longtable/cli 0.1.53 → 0.1.55

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,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 : "";
@@ -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.firstResearchShape
242
- ? `First research shape: ${compactContextValue(state.firstResearchShape.handle, 96)}.`
243
- : `Current goal: ${compactContextValue(context.session.currentGoal, 120)}.`;
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 is either summarized into a First Research Shape or explicitly cleared."
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) {
@@ -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;
@@ -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
- if (!session.firstResearchShape && !session.researchSpecification) {
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 (!session.researchSpecification) {
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
- const status = session.researchSpecification.confirmedAt
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
- ? "- 다음 프로토콜: 명세를 업데이트한 뒤 `confirm_research_specification`으로 다시 preview 확인을 받아야 합니다."
201
- : "- Next protocol: update the specification, then return to `confirm_research_specification` for another preview confirmation."
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"),
@@ -655,6 +636,18 @@ function changeSummaryForRevision(changes) {
655
636
  }
656
637
  return changes.slice(0, 12).map((change) => change.summary);
657
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
+ }
658
651
  export function applyResearchSpecificationAuditUpdate(state, options) {
659
652
  const previous = state.researchSpecification;
660
653
  const incomingEvidenceIds = mergeStringLists(options.patch?.sourceEvidenceIds, options.specification.sourceEvidenceIds, options.sourceEvidenceIds);
@@ -767,6 +760,12 @@ function summarizeWorkspaceInspection(context, state) {
767
760
  const answeredQuestions = questions.filter((record) => record.status === "answered");
768
761
  const pendingObligations = visiblePendingObligations(state);
769
762
  const hardStop = collectHardStopBlockers(state);
763
+ const researchSpecificationReadiness = evaluateResearchSpecificationReadiness({
764
+ firstResearchShape: state.firstResearchShape ?? context.session.firstResearchShape,
765
+ researchSpecification: state.researchSpecification ?? context.session.researchSpecification,
766
+ questionLog: state.questionLog,
767
+ questionObligations: state.questionObligations
768
+ });
770
769
  return {
771
770
  found: true,
772
771
  rootPath: context.project.projectPath,
@@ -815,6 +814,7 @@ function summarizeWorkspaceInspection(context, state) {
815
814
  specRevisions: (state.specRevisions ?? []).length
816
815
  },
817
816
  hardStop,
817
+ researchSpecificationReadiness,
818
818
  recentInvocations: recentInvocationRecords(state, 5).map((record) => ({
819
819
  id: record.id,
820
820
  kind: record.intent.kind,
@@ -2735,7 +2735,56 @@ export async function answerWorkspaceQuestion(options) {
2735
2735
  invocationLog: (state.invocationLog ?? []).map((record) => updateInvocationWithDecision(record, question.id, decision.id))
2736
2736
  };
2737
2737
  const withDecision = appendDecisionToResearchState(withQuestion, decision);
2738
- const updated = resolveQuestionObligationByQuestionId(withDecision, question.id, decision.id);
2738
+ let updated = resolveQuestionObligationByQuestionId(withDecision, question.id, decision.id);
2739
+ if (question.prompt.checkpointKey === "research_specification_confirmation") {
2740
+ const specification = updated.researchSpecification ?? options.context.session.researchSpecification;
2741
+ const selectedAnswer = answer.selectedValues[0];
2742
+ if (specification) {
2743
+ const nextStatus = researchSpecificationAnswerStatus(selectedAnswer);
2744
+ const confirmedSpecification = {
2745
+ ...specification,
2746
+ status: nextStatus,
2747
+ updatedAt: timestamp,
2748
+ ...(nextStatus === "confirmed" ? { confirmedAt: specification.confirmedAt ?? timestamp } : {})
2749
+ };
2750
+ const withHookStatus = {
2751
+ ...updated,
2752
+ hooks: (updated.hooks ?? []).map((hook) => {
2753
+ if (hook.id !== confirmedSpecification.sourceHookId) {
2754
+ return hook;
2755
+ }
2756
+ return {
2757
+ ...hook,
2758
+ status: nextStatus === "draft" ? "active" : nextStatus,
2759
+ updatedAt: timestamp,
2760
+ researchSpecification: confirmedSpecification,
2761
+ linkedQuestionRecordIds: mergeStringLists(hook.linkedQuestionRecordIds, [question.id]),
2762
+ linkedDecisionRecordIds: mergeStringLists(hook.linkedDecisionRecordIds, [decision.id])
2763
+ };
2764
+ })
2765
+ };
2766
+ const sourceEvidenceIds = (withHookStatus.evidenceRecords ?? [])
2767
+ .filter((record) => record.sourceHookId && record.sourceHookId === confirmedSpecification.sourceHookId)
2768
+ .map((record) => record.id);
2769
+ updated = applyResearchSpecificationAuditUpdate(withHookStatus, {
2770
+ specification: confirmedSpecification,
2771
+ timestamp,
2772
+ source: "decision",
2773
+ title: `Research Specification confirmation: ${confirmedSpecification.title}`,
2774
+ rationale: `Research Specification confirmation answer: ${selectedAnswer}`,
2775
+ sourceEvidenceIds,
2776
+ questionRecordId: question.id,
2777
+ decisionRecordId: decision.id,
2778
+ createDecisionRecord: false
2779
+ }).state;
2780
+ options.context.session = {
2781
+ ...options.context.session,
2782
+ researchSpecification: confirmedSpecification,
2783
+ lastUpdatedAt: timestamp
2784
+ };
2785
+ await writeFile(options.context.sessionFilePath, JSON.stringify(options.context.session, null, 2), "utf8");
2786
+ }
2787
+ }
2739
2788
  await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
2740
2789
  await syncCurrentWorkspaceView(options.context);
2741
2790
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
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.53",
33
- "@longtable/core": "0.1.53",
34
- "@longtable/memory": "0.1.53",
35
- "@longtable/provider-claude": "0.1.53",
36
- "@longtable/provider-codex": "0.1.53",
37
- "@longtable/setup": "0.1.53"
32
+ "@longtable/checkpoints": "0.1.55",
33
+ "@longtable/core": "0.1.55",
34
+ "@longtable/memory": "0.1.55",
35
+ "@longtable/provider-claude": "0.1.55",
36
+ "@longtable/provider-codex": "0.1.55",
37
+ "@longtable/setup": "0.1.55"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",