@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.
|
|
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) {
|
|
@@ -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"),
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.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",
|