@longtable/cli 0.1.42 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -1
- package/dist/longtable-codex-native-hook.js +45 -46
- package/dist/project-session.d.ts +70 -0
- package/dist/project-session.js +379 -3
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -86,9 +86,18 @@ summary without starting a provider session.
|
|
|
86
86
|
- `CURRENT.md`: human-facing current view regenerated from state
|
|
87
87
|
- `.longtable/project.json`: stable project identity
|
|
88
88
|
- `.longtable/current-session.json`: current session cursor
|
|
89
|
-
- `.longtable/state.json`: layered memory state
|
|
89
|
+
- `.longtable/state.json`: layered memory state, including First Research
|
|
90
|
+
Shape and Research Specification when the interview has produced them
|
|
90
91
|
- `.longtable/sessions/`: historical snapshots
|
|
91
92
|
|
|
93
|
+
`$longtable-interview` first stabilizes a short First Research Shape. When the
|
|
94
|
+
conversation is substantive enough, it should also preserve a Research
|
|
95
|
+
Specification covering scope, construct ontology, theory framing,
|
|
96
|
+
measurement/coding, method options, evidence/access requirements, epistemic
|
|
97
|
+
alignment, protected decisions, open questions, and next actions. `CURRENT.md`
|
|
98
|
+
renders that specification so later agents do not need to reconstruct the full
|
|
99
|
+
interview from memory.
|
|
100
|
+
|
|
92
101
|
## Why This Shape
|
|
93
102
|
|
|
94
103
|
The CLI tries to keep the root simple for novice researchers while preserving enough structure for power users and downstream tooling.
|
|
@@ -140,6 +149,25 @@ That setup writes the MCP configuration and Codex elicitation approval needed
|
|
|
140
149
|
for form-style checkpoint prompts. Without it, LongTable keeps the same
|
|
141
150
|
`QuestionRecord` pending and falls back to numbered text.
|
|
142
151
|
|
|
152
|
+
## Runtime Boundary
|
|
153
|
+
|
|
154
|
+
LongTable is not a replacement wrapper for Codex. Markdown docs and generated
|
|
155
|
+
skills are soft policy; hooks, MCP elicitation, CLI gates, and `.longtable/`
|
|
156
|
+
state are the enforcement layers.
|
|
157
|
+
|
|
158
|
+
LongTable should ask and stop before acting when the next step would change or
|
|
159
|
+
settle one of four high-risk research commitments:
|
|
160
|
+
|
|
161
|
+
1. Research question or scope
|
|
162
|
+
2. Theory frame or construct map
|
|
163
|
+
3. Measurement, coding, or extraction standard
|
|
164
|
+
4. Method design or analysis strategy
|
|
165
|
+
|
|
166
|
+
Low-risk reversible work should continue with visible assumptions instead of a
|
|
167
|
+
hook interruption. If human knowledge, AI inference, and durable project state
|
|
168
|
+
conflict, LongTable should prefer the most explicit durable state; if that state
|
|
169
|
+
is not explicit enough, it should ask the researcher for clarity.
|
|
170
|
+
|
|
143
171
|
Explicit short forms are available when needed:
|
|
144
172
|
|
|
145
173
|
```text
|
|
@@ -188,6 +216,12 @@ Inside a LongTable project workspace, panel planning also appends an
|
|
|
188
216
|
`InvocationRecord` to `.longtable/state.json`, creates a pending follow-up
|
|
189
217
|
`QuestionRecord`, and refreshes `CURRENT.md`.
|
|
190
218
|
|
|
219
|
+
Panel output should remain inspectable. A panel or debate result is expected to
|
|
220
|
+
show the consulted roles, each role's main claim or objection, the disagreement
|
|
221
|
+
map, decision options, a defensible recommendation when one exists, and the
|
|
222
|
+
exact researcher-facing question before a high-stakes decision is treated as
|
|
223
|
+
settled.
|
|
224
|
+
|
|
191
225
|
Default panel roles include:
|
|
192
226
|
|
|
193
227
|
- `reviewer`
|
|
@@ -204,6 +238,12 @@ measurement, theory, method, evidence, authorship, or tacit-assumption risks.
|
|
|
204
238
|
Use `--record` inside a LongTable workspace to store the finding as an
|
|
205
239
|
unconfirmed inferred hypothesis.
|
|
206
240
|
|
|
241
|
+
The Codex hook stays quiet for advisory-only questions. Required hook context is
|
|
242
|
+
reserved for durable Researcher Checkpoints, especially when a prompt would
|
|
243
|
+
change the research question/scope, theory frame, measurement/coding standard,
|
|
244
|
+
method design, or analysis strategy. Low-risk reversible work should proceed
|
|
245
|
+
with visible assumptions rather than a noisy hook interruption.
|
|
246
|
+
|
|
207
247
|
`longtable team` creates a file-backed agent-team review under
|
|
208
248
|
`.longtable/team/<id>/`: independent review, cross-review, and
|
|
209
249
|
synthesis/checkpoint. Use it when roles should inspect each other's concerns
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
|
-
import {
|
|
2
|
+
import { createWorkspaceFollowUpQuestions, loadProjectContextFromDirectory, loadWorkspaceState, pendingQuestionObligations } from "./index.js";
|
|
3
3
|
function safeString(value) {
|
|
4
4
|
return typeof value === "string" ? value : "";
|
|
5
5
|
}
|
|
@@ -115,7 +115,7 @@ function looksLikeLongTableProductOrToolingPrompt(prompt) {
|
|
|
115
115
|
if (!normalized) {
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
|
-
return /\b(longlongtable|hook|checkpoint|mcp|agents?|skills?|ux|interface|setup|install|cli|npm|version|global|release|deploy|git|github|readme|docs?|documentation|workflow|package|router|autocomplete|simulation test)\b/i.test(normalized)
|
|
118
|
+
return /\b(longtable|longlongtable|hook|checkpoint|mcp|agents?|skills?|ux|interface|setup|install|cli|npm|version|global|release|deploy|git|github|readme|docs?|documentation|workflow|package|router|autocomplete|simulation test)\b/i.test(normalized)
|
|
119
119
|
|| /롱테이블|훅|체크포인트|에이전트|스킬|사용성|인터페이스|설치|세팅|글로벌|배포|버전|릴리즈|깃|깃허브|문서화된\s*절차|패키지|라우터|자동완성|시뮬레이션\s*테스트/.test(normalized);
|
|
120
120
|
}
|
|
121
121
|
function looksLikeResearchDomainPrompt(prompt) {
|
|
@@ -123,11 +123,38 @@ function looksLikeResearchDomainPrompt(prompt) {
|
|
|
123
123
|
if (!normalized) {
|
|
124
124
|
return false;
|
|
125
125
|
}
|
|
126
|
-
return /\b(research|study|paper|manuscript|journal|article|method|methodology|measurement|construct|theory|analysis|model|data|participant|sample|scale|survey|instrument|validity|hypothesis|literature|meta[- ]?analysis|gold standard|coding|trust|reliance|calibration)\b/i.test(normalized)
|
|
127
|
-
||
|
|
126
|
+
return /\b(research|research question|research direction|scope|boundary|study|paper|manuscript|journal|article|method|methodology|measurement|construct|theory|framework|analysis|analysis plan|model|data|participant|sample|scale|survey|instrument|validity|hypothesis|literature|meta[- ]?analysis|gold standard|coding|criteria|trust|reliance|calibration)\b/i.test(normalized)
|
|
127
|
+
|| /연구|연구\s*질문|연구\s*방향|범위|경계|논문|원고|저널|방법론|방법|연구\s*설계|측정|구성개념|개념|이론|프레임워크|분석|분석\s*계획|모형|모델|데이터|참가자|표본|샘플|척도|설문|도구|타당도|가설|문헌|메타\s*분석|골드\s*스탠더드|코딩|기준|신뢰|의존|캘리브레이션|교정|보정/.test(normalized);
|
|
128
128
|
}
|
|
129
129
|
function looksLikeResearchCommitmentPrompt(prompt) {
|
|
130
|
-
return looksLikeResearchDomainPrompt(prompt) && looksLikeClosurePrompt(prompt)
|
|
130
|
+
return looksLikeResearchDomainPrompt(prompt) && (looksLikeClosurePrompt(prompt) ||
|
|
131
|
+
/\b(change|revise|update|replace|reframe|modify|alter)\b/i.test(prompt) ||
|
|
132
|
+
/바꾸|변경|수정|교체|전환|재설정/.test(prompt));
|
|
133
|
+
}
|
|
134
|
+
function looksLikeQuestionGenerationPrompt(prompt) {
|
|
135
|
+
return /\b(needed questions?|necessary questions?|question generation|clarifying questions?|ask questions?)\b/i.test(prompt)
|
|
136
|
+
|| /필요한\s*질문|질문을\s*(모두|많이|생성)|질문\s*생성|물어봐|질문해/.test(prompt);
|
|
137
|
+
}
|
|
138
|
+
function looksLikeMultiCommitmentChangePrompt(prompt) {
|
|
139
|
+
const normalized = prompt.trim();
|
|
140
|
+
const actionCue = /\b(change|revise|update|replace|reframe|modify|alter)\b/i.test(normalized)
|
|
141
|
+
|| /바꾸|변경|수정|교체|전환|재설정/.test(normalized);
|
|
142
|
+
if (!actionCue) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const categories = [
|
|
146
|
+
/\b(research question|research direction|scope|boundary|inclusion criteria|exclusion criteria)\b/i.test(normalized)
|
|
147
|
+
|| /연구\s*질문|연구\s*문제|연구\s*방향|범위|경계|포함\s*기준|제외\s*기준/.test(normalized),
|
|
148
|
+
/\b(theory|framework|conceptual model|construct map)\b/i.test(normalized)
|
|
149
|
+
|| /이론|프레임워크|개념\s*모형|구성개념\s*지도|컨스트럭트/.test(normalized),
|
|
150
|
+
/\b(measure|measurement|scale|instrument|coding|coding rule|extraction rule|operationali[sz]ation)\b/i.test(normalized)
|
|
151
|
+
|| /측정|척도|도구|코딩|코딩\s*규칙|코딩\s*기준|추출\s*규칙|추출\s*기준|조작화/.test(normalized),
|
|
152
|
+
/\b(method|methodology|study design|sampling|sample)\b/i.test(normalized)
|
|
153
|
+
|| /방법론|방법|연구\s*설계|표본|샘플링/.test(normalized),
|
|
154
|
+
/\b(analysis plan|analysis method|meta[- ]?analysis|masem|(?:statistical|structural|path|analysis) model|moderator|random[- ]?effects)\b/i.test(normalized)
|
|
155
|
+
|| /분석\s*계획|분석\s*방법|메타\s*분석|분석\s*(?:모형|모델)|통계\s*(?:모형|모델)|구조\s*방정식|경로\s*모형|조절효과|랜덤\s*효과/.test(normalized)
|
|
156
|
+
];
|
|
157
|
+
return categories.filter(Boolean).length >= 2;
|
|
131
158
|
}
|
|
132
159
|
function looksLikeExplicitInterviewPrompt(prompt) {
|
|
133
160
|
const normalized = prompt.trim();
|
|
@@ -156,28 +183,14 @@ function looksLikeResearchStateConfirmationPrompt(prompt) {
|
|
|
156
183
|
function shouldSurfaceInterviewContext(prompt) {
|
|
157
184
|
return looksLikeExplicitInterviewPrompt(prompt) || looksLikeResearchStateConfirmationPrompt(prompt);
|
|
158
185
|
}
|
|
159
|
-
function buildResponseOnlyAdvisoryQuestions(prompt) {
|
|
160
|
-
if (looksLikeLongTableProductOrToolingPrompt(prompt)) {
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
const opportunities = buildQuestionOpportunitySpecs(prompt, {
|
|
164
|
-
includeFallback: false,
|
|
165
|
-
autoOnly: true
|
|
166
|
-
});
|
|
167
|
-
if (opportunities.length === 0) {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
if (!looksLikeResearchDomainPrompt(prompt) && !/\b(needed questions?|necessary questions?|clarifying questions?|question generation|assumptions?|uncertain|not sure|gap|tension|trade[- ]?off)\b/i.test(prompt) && !/필요한\s*질문|질문\s*생성|물어봐|질문해|전제|가정|불확실|모르겠|공백|긴장|상충|균형/.test(prompt)) {
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
173
|
-
return opportunities.slice(0, 3);
|
|
174
|
-
}
|
|
175
186
|
function shouldCreateRequiredQuestionsForPrompt(prompt) {
|
|
176
187
|
return !looksLikeLongTableProductOrToolingPrompt(prompt) && looksLikeResearchCommitmentPrompt(prompt);
|
|
177
188
|
}
|
|
178
189
|
function shouldApplyProtectedDecisionClosure(runtime, prompt) {
|
|
179
190
|
return Boolean(runtime.context.session.protectedDecision) &&
|
|
180
|
-
shouldCreateRequiredQuestionsForPrompt(prompt)
|
|
191
|
+
shouldCreateRequiredQuestionsForPrompt(prompt) &&
|
|
192
|
+
!looksLikeQuestionGenerationPrompt(prompt) &&
|
|
193
|
+
!looksLikeMultiCommitmentChangePrompt(prompt);
|
|
181
194
|
}
|
|
182
195
|
function protectedDecisionClosurePrompt(prompt) {
|
|
183
196
|
return [
|
|
@@ -262,16 +275,6 @@ function buildGeneratedQuestionsContext(questions, created) {
|
|
|
262
275
|
lines.push("Do not choose or record answers for these checkpoints unless the researcher explicitly provides the selections.");
|
|
263
276
|
return lines.join("\n");
|
|
264
277
|
}
|
|
265
|
-
function buildAdvisoryQuestionsContext(questions) {
|
|
266
|
-
const lines = [
|
|
267
|
-
`LongTable surfaced ${questions.length} response-only advisory question${questions.length === 1 ? "" : "s"} for this prompt.`,
|
|
268
|
-
"Use these only if they help the reply. Do not create QuestionRecord entries, call longtable decide, or answer for the researcher unless the prompt explicitly asks to commit a research decision."
|
|
269
|
-
];
|
|
270
|
-
for (const question of questions) {
|
|
271
|
-
lines.push(`- ${question.title}: ${question.question}`);
|
|
272
|
-
}
|
|
273
|
-
return lines.join("\n");
|
|
274
|
-
}
|
|
275
278
|
function buildPendingObligationContext(obligation) {
|
|
276
279
|
return [
|
|
277
280
|
`Pending LongTable research obligation: ${obligation.prompt}`,
|
|
@@ -347,37 +350,33 @@ async function userPromptSubmitContext(runtime, prompt) {
|
|
|
347
350
|
}
|
|
348
351
|
const generatedQuestions = [];
|
|
349
352
|
let createdQuestions = false;
|
|
350
|
-
if (
|
|
351
|
-
const
|
|
353
|
+
if (shouldApplyProtectedDecisionClosure(runtime, prompt)) {
|
|
354
|
+
const protectedGenerated = await createWorkspaceFollowUpQuestions({
|
|
352
355
|
context: runtime.context,
|
|
353
|
-
prompt,
|
|
356
|
+
prompt: protectedDecisionClosurePrompt(prompt),
|
|
354
357
|
provider: "codex",
|
|
355
358
|
required: true,
|
|
356
359
|
auto: true,
|
|
357
360
|
requiredOnly: true
|
|
358
361
|
});
|
|
359
|
-
generatedQuestions.push(...
|
|
360
|
-
createdQuestions = createdQuestions ||
|
|
362
|
+
generatedQuestions.push(...protectedGenerated.questions);
|
|
363
|
+
createdQuestions = createdQuestions || protectedGenerated.created;
|
|
361
364
|
}
|
|
362
|
-
if (
|
|
363
|
-
const
|
|
365
|
+
else if (shouldCreateRequiredQuestionsForPrompt(prompt)) {
|
|
366
|
+
const generated = await createWorkspaceFollowUpQuestions({
|
|
364
367
|
context: runtime.context,
|
|
365
|
-
prompt
|
|
368
|
+
prompt,
|
|
366
369
|
provider: "codex",
|
|
367
370
|
required: true,
|
|
368
371
|
auto: true,
|
|
369
372
|
requiredOnly: true
|
|
370
373
|
});
|
|
371
|
-
generatedQuestions.push(...
|
|
372
|
-
createdQuestions = createdQuestions ||
|
|
374
|
+
generatedQuestions.push(...generated.questions);
|
|
375
|
+
createdQuestions = createdQuestions || generated.created;
|
|
373
376
|
}
|
|
374
377
|
if (generatedQuestions.length > 0) {
|
|
375
378
|
return buildGeneratedQuestionsContext(generatedQuestions, createdQuestions);
|
|
376
379
|
}
|
|
377
|
-
const advisoryQuestions = buildResponseOnlyAdvisoryQuestions(prompt);
|
|
378
|
-
if (advisoryQuestions.length > 0) {
|
|
379
|
-
return buildAdvisoryQuestionsContext(advisoryQuestions);
|
|
380
|
-
}
|
|
381
380
|
return null;
|
|
382
381
|
}
|
|
383
382
|
function preToolUseOutput(runtime, payload) {
|
|
@@ -34,6 +34,58 @@ export interface FirstResearchShape {
|
|
|
34
34
|
sourceHookId?: string;
|
|
35
35
|
confirmedAt?: string;
|
|
36
36
|
}
|
|
37
|
+
export interface ResearchSpecification {
|
|
38
|
+
title: string;
|
|
39
|
+
status?: "draft" | "confirmed" | "deferred";
|
|
40
|
+
createdAt?: string;
|
|
41
|
+
updatedAt?: string;
|
|
42
|
+
sourceHookId?: string;
|
|
43
|
+
researchDirection: {
|
|
44
|
+
question?: string;
|
|
45
|
+
purpose: string;
|
|
46
|
+
scopeBoundary?: string;
|
|
47
|
+
inclusionCriteria?: string[];
|
|
48
|
+
exclusionCriteria?: string[];
|
|
49
|
+
};
|
|
50
|
+
constructOntology: {
|
|
51
|
+
coreConstructs: string[];
|
|
52
|
+
distinctions: string[];
|
|
53
|
+
termsToAvoidCollapsing?: string[];
|
|
54
|
+
};
|
|
55
|
+
theoryAndFraming: {
|
|
56
|
+
anchors: string[];
|
|
57
|
+
alternatives?: string[];
|
|
58
|
+
overreachRisks?: string[];
|
|
59
|
+
};
|
|
60
|
+
measurementCoding: {
|
|
61
|
+
variablesOrConstructs: string[];
|
|
62
|
+
evidenceTypes: string[];
|
|
63
|
+
codingRules: string[];
|
|
64
|
+
openStandards?: string[];
|
|
65
|
+
};
|
|
66
|
+
methodAnalysis: {
|
|
67
|
+
design?: string;
|
|
68
|
+
analysisOptions: string[];
|
|
69
|
+
dataSufficiencyCriteria?: string[];
|
|
70
|
+
unsettledChoices?: string[];
|
|
71
|
+
};
|
|
72
|
+
evidenceAccess: {
|
|
73
|
+
requiredSources?: string[];
|
|
74
|
+
accessRequirements?: string[];
|
|
75
|
+
evidenceStandards?: string[];
|
|
76
|
+
};
|
|
77
|
+
epistemicAlignment: {
|
|
78
|
+
researcherKnowledge?: string[];
|
|
79
|
+
projectStatePriority?: string[];
|
|
80
|
+
aiInferenceLimits?: string[];
|
|
81
|
+
conflictResolutionRule?: string;
|
|
82
|
+
};
|
|
83
|
+
protectedDecisions: string[];
|
|
84
|
+
openQuestions: string[];
|
|
85
|
+
nextActions: string[];
|
|
86
|
+
confidence: "low" | "medium" | "high";
|
|
87
|
+
confirmedAt?: string;
|
|
88
|
+
}
|
|
37
89
|
export interface LongTableInterviewTurn {
|
|
38
90
|
id: string;
|
|
39
91
|
index: number;
|
|
@@ -59,6 +111,7 @@ export interface LongTableHookRun {
|
|
|
59
111
|
provider?: ProviderKind;
|
|
60
112
|
turns?: LongTableInterviewTurn[];
|
|
61
113
|
firstResearchShape?: FirstResearchShape;
|
|
114
|
+
researchSpecification?: ResearchSpecification;
|
|
62
115
|
qualityNotes?: string[];
|
|
63
116
|
rationale?: string[];
|
|
64
117
|
linkedQuestionRecordIds?: string[];
|
|
@@ -67,6 +120,7 @@ export interface LongTableHookRun {
|
|
|
67
120
|
export type LongTableWorkspaceState = ResearchState & {
|
|
68
121
|
hooks?: LongTableHookRun[];
|
|
69
122
|
firstResearchShape?: FirstResearchShape;
|
|
123
|
+
researchSpecification?: ResearchSpecification;
|
|
70
124
|
};
|
|
71
125
|
export interface LongTableProjectRecord {
|
|
72
126
|
schemaVersion: 1;
|
|
@@ -102,6 +156,7 @@ export interface LongTableSessionRecord {
|
|
|
102
156
|
openQuestions?: string[];
|
|
103
157
|
startInterview?: StartInterviewSession;
|
|
104
158
|
firstResearchShape?: FirstResearchShape;
|
|
159
|
+
researchSpecification?: ResearchSpecification;
|
|
105
160
|
requestedPerspectives: string[];
|
|
106
161
|
disagreementPreference: ProjectDisagreementPreference;
|
|
107
162
|
activeModes?: string[];
|
|
@@ -132,6 +187,11 @@ export interface LongTableWorkspaceInspection {
|
|
|
132
187
|
currentBlocker?: string;
|
|
133
188
|
requestedPerspectives: string[];
|
|
134
189
|
disagreementPreference: ProjectDisagreementPreference;
|
|
190
|
+
researchSpecification?: {
|
|
191
|
+
title: string;
|
|
192
|
+
status: "draft" | "confirmed" | "deferred";
|
|
193
|
+
confidence: "low" | "medium" | "high";
|
|
194
|
+
};
|
|
135
195
|
};
|
|
136
196
|
files?: {
|
|
137
197
|
project: string;
|
|
@@ -224,6 +284,16 @@ export declare function summarizeLongTableInterview(options: {
|
|
|
224
284
|
state: LongTableWorkspaceState;
|
|
225
285
|
session: LongTableSessionRecord;
|
|
226
286
|
}>;
|
|
287
|
+
export declare function summarizeLongTableResearchSpecification(options: {
|
|
288
|
+
context: LongTableProjectContext;
|
|
289
|
+
hookId?: string;
|
|
290
|
+
specification: ResearchSpecification;
|
|
291
|
+
}): Promise<{
|
|
292
|
+
hook?: LongTableHookRun;
|
|
293
|
+
specification: ResearchSpecification;
|
|
294
|
+
state: LongTableWorkspaceState;
|
|
295
|
+
session: LongTableSessionRecord;
|
|
296
|
+
}>;
|
|
227
297
|
export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
|
|
228
298
|
export declare function listBlockingWorkspaceObligations(context: LongTableProjectContext): Promise<LongTableQuestionObligation[]>;
|
|
229
299
|
export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
|
package/dist/project-session.js
CHANGED
|
@@ -94,6 +94,9 @@ function buildNextAction(session) {
|
|
|
94
94
|
: "Open with your current goal in one sentence, then ask LongTable for the first concrete research move.";
|
|
95
95
|
}
|
|
96
96
|
function buildResumeHint(session) {
|
|
97
|
+
if (session.researchSpecification) {
|
|
98
|
+
return `I want to continue from the Research Specification: ${session.researchSpecification.title}.`;
|
|
99
|
+
}
|
|
97
100
|
if (session.firstResearchShape) {
|
|
98
101
|
return `I want to continue from the First Research Shape: ${session.firstResearchShape.handle}.`;
|
|
99
102
|
}
|
|
@@ -106,6 +109,51 @@ function buildResumeHint(session) {
|
|
|
106
109
|
? `I want to continue ${session.currentGoal}. The unresolved blocker is ${session.currentBlocker}.`
|
|
107
110
|
: `I want to continue ${session.currentGoal}.`;
|
|
108
111
|
}
|
|
112
|
+
function renderResearchSpecificationSummary(specification, locale) {
|
|
113
|
+
const korean = locale === "ko";
|
|
114
|
+
const lines = [
|
|
115
|
+
"",
|
|
116
|
+
korean ? "## Research Specification" : "## Research Specification",
|
|
117
|
+
`- ${korean ? "제목" : "Title"}: ${specification.title}`,
|
|
118
|
+
`- ${korean ? "상태" : "Status"}: ${specification.confirmedAt ? "confirmed" : specification.status ?? "draft"}`,
|
|
119
|
+
`- ${korean ? "신뢰도" : "Confidence"}: ${specification.confidence}`
|
|
120
|
+
];
|
|
121
|
+
if (specification.researchDirection.question) {
|
|
122
|
+
lines.push(`- ${korean ? "연구 질문" : "Question"}: ${specification.researchDirection.question}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push(`- ${korean ? "목적" : "Purpose"}: ${specification.researchDirection.purpose}`);
|
|
125
|
+
if (specification.researchDirection.scopeBoundary) {
|
|
126
|
+
lines.push(`- ${korean ? "범위 경계" : "Scope boundary"}: ${specification.researchDirection.scopeBoundary}`);
|
|
127
|
+
}
|
|
128
|
+
if (specification.constructOntology.coreConstructs.length > 0) {
|
|
129
|
+
lines.push(`- ${korean ? "핵심 construct" : "Core constructs"}: ${specification.constructOntology.coreConstructs.join("; ")}`);
|
|
130
|
+
}
|
|
131
|
+
if (specification.constructOntology.distinctions.length > 0) {
|
|
132
|
+
lines.push(`- ${korean ? "구분해야 할 차이" : "Key distinctions"}: ${specification.constructOntology.distinctions.join("; ")}`);
|
|
133
|
+
}
|
|
134
|
+
if (specification.theoryAndFraming.anchors.length > 0) {
|
|
135
|
+
lines.push(`- ${korean ? "이론 앵커" : "Theory anchors"}: ${specification.theoryAndFraming.anchors.join("; ")}`);
|
|
136
|
+
}
|
|
137
|
+
if (specification.measurementCoding.codingRules.length > 0) {
|
|
138
|
+
lines.push(`- ${korean ? "코딩 규칙" : "Coding rules"}: ${specification.measurementCoding.codingRules.join("; ")}`);
|
|
139
|
+
}
|
|
140
|
+
if (specification.methodAnalysis.analysisOptions.length > 0) {
|
|
141
|
+
lines.push(`- ${korean ? "분석 옵션" : "Analysis options"}: ${specification.methodAnalysis.analysisOptions.join("; ")}`);
|
|
142
|
+
}
|
|
143
|
+
if (specification.epistemicAlignment.conflictResolutionRule) {
|
|
144
|
+
lines.push(`- ${korean ? "충돌 조정 규칙" : "Conflict rule"}: ${specification.epistemicAlignment.conflictResolutionRule}`);
|
|
145
|
+
}
|
|
146
|
+
if (specification.protectedDecisions.length > 0) {
|
|
147
|
+
lines.push(...specification.protectedDecisions.map((decision) => `- ${korean ? "보호할 결정" : "Protected decision"}: ${decision}`));
|
|
148
|
+
}
|
|
149
|
+
if (specification.openQuestions.length > 0) {
|
|
150
|
+
lines.push(...specification.openQuestions.map((question) => `- ${korean ? "열린 질문" : "Open question"}: ${question}`));
|
|
151
|
+
}
|
|
152
|
+
if (specification.nextActions.length > 0) {
|
|
153
|
+
lines.push(...specification.nextActions.map((action) => `- ${korean ? "다음 행동" : "Next action"}: ${action}`));
|
|
154
|
+
}
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
109
157
|
function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
|
|
110
158
|
const locale = normalizeLocale(session.locale ?? project.locale);
|
|
111
159
|
const openQuestions = session.openQuestions && session.openQuestions.length > 0
|
|
@@ -129,6 +177,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
129
177
|
...(session.gapRisk ? [`- 공백/암묵지 위험: ${session.gapRisk}`] : []),
|
|
130
178
|
...(session.protectedDecision ? [`- 보호할 결정: ${session.protectedDecision}`] : []),
|
|
131
179
|
...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
|
|
180
|
+
...(session.researchSpecification ? [`- Research Specification: ${session.researchSpecification.title}`] : []),
|
|
132
181
|
...(session.startInterview ? [`- start interview: ${session.startInterview.summary}`] : []),
|
|
133
182
|
`- 다음 액션: ${nextAction}`,
|
|
134
183
|
`- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
@@ -179,6 +228,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
179
228
|
...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
|
|
180
229
|
]
|
|
181
230
|
: []),
|
|
231
|
+
...(session.researchSpecification ? renderResearchSpecificationSummary(session.researchSpecification, locale) : []),
|
|
182
232
|
"",
|
|
183
233
|
"## 빠른 시작",
|
|
184
234
|
"- 이 디렉토리에서 `codex`를 엽니다.",
|
|
@@ -202,6 +252,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
202
252
|
...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
|
|
203
253
|
...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
|
|
204
254
|
...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
|
|
255
|
+
...(session.researchSpecification ? [`- Research Specification: ${session.researchSpecification.title}`] : []),
|
|
205
256
|
...(session.startInterview ? [`- Start interview: ${session.startInterview.summary}`] : []),
|
|
206
257
|
`- Next action: ${nextAction}`,
|
|
207
258
|
`- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
@@ -252,6 +303,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
252
303
|
...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
|
|
253
304
|
]
|
|
254
305
|
: []),
|
|
306
|
+
...(session.researchSpecification ? renderResearchSpecificationSummary(session.researchSpecification, locale) : []),
|
|
255
307
|
"",
|
|
256
308
|
"## Quick Start",
|
|
257
309
|
"- Open `codex` in this directory.",
|
|
@@ -272,6 +324,7 @@ async function loadResearchState(stateFilePath) {
|
|
|
272
324
|
workingState: parsed.workingState ?? {},
|
|
273
325
|
hooks: parsed.hooks ?? [],
|
|
274
326
|
...(parsed.firstResearchShape ? { firstResearchShape: parsed.firstResearchShape } : {}),
|
|
327
|
+
...(parsed.researchSpecification ? { researchSpecification: parsed.researchSpecification } : {}),
|
|
275
328
|
questionObligations: parsed.questionObligations ?? [],
|
|
276
329
|
inferredHypotheses: parsed.inferredHypotheses ?? [],
|
|
277
330
|
openTensions: parsed.openTensions ?? [],
|
|
@@ -332,7 +385,18 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
332
385
|
currentGoal: context.session.currentGoal,
|
|
333
386
|
...(context.session.currentBlocker ? { currentBlocker: context.session.currentBlocker } : {}),
|
|
334
387
|
requestedPerspectives: context.session.requestedPerspectives,
|
|
335
|
-
disagreementPreference: context.session.disagreementPreference
|
|
388
|
+
disagreementPreference: context.session.disagreementPreference,
|
|
389
|
+
...(context.session.researchSpecification
|
|
390
|
+
? {
|
|
391
|
+
researchSpecification: {
|
|
392
|
+
title: context.session.researchSpecification.title,
|
|
393
|
+
status: context.session.researchSpecification.confirmedAt
|
|
394
|
+
? "confirmed"
|
|
395
|
+
: context.session.researchSpecification.status ?? "draft",
|
|
396
|
+
confidence: context.session.researchSpecification.confidence
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
: {})
|
|
336
400
|
},
|
|
337
401
|
files: {
|
|
338
402
|
project: context.projectFilePath,
|
|
@@ -422,6 +486,7 @@ function buildProjectAgentsMd(project, session) {
|
|
|
422
486
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
423
487
|
"- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, record turns when MCP is available, and avoid early reader/reviewer or theory/method/measurement classification.",
|
|
424
488
|
"- Do not summarize `$longtable-interview` because a fixed number of turns has passed; wait for content-based readiness around research object, focal uncertainty, boundary, evidence/material, protected decision, and next action.",
|
|
489
|
+
"- After the First Research Shape, create a Research Specification when the interview has enough detail to preserve scope, construct ontology, theory framing, coding rules, method options, evidence/access requirements, epistemic alignment, protected decisions, open questions, and next actions.",
|
|
425
490
|
"- Do not let unrelated pending Researcher Checkpoints interrupt `$longtable-interview`; mention them only as separate unresolved checkpoints unless the researcher is confirming, saving, or recording a research decision.",
|
|
426
491
|
"- Use structured options only at the final First Research Shape confirmation or at true checkpoint boundaries.",
|
|
427
492
|
"- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
|
|
@@ -441,6 +506,7 @@ function buildProjectAgentsMd(project, session) {
|
|
|
441
506
|
...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
|
|
442
507
|
...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
|
|
443
508
|
...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
|
|
509
|
+
...(session.researchSpecification ? [`- Research Specification: ${session.researchSpecification.title}`] : []),
|
|
444
510
|
...(session.startInterview ? [`- Start interview summary: ${session.startInterview.summary}`] : []),
|
|
445
511
|
`- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
446
512
|
`- Disagreement visibility: ${session.disagreementPreference}`,
|
|
@@ -473,6 +539,10 @@ function buildStateSeed(project, session, setup) {
|
|
|
473
539
|
state.firstResearchShape = session.firstResearchShape;
|
|
474
540
|
state.workingState.firstResearchShape = session.firstResearchShape;
|
|
475
541
|
}
|
|
542
|
+
if (session.researchSpecification) {
|
|
543
|
+
state.researchSpecification = session.researchSpecification;
|
|
544
|
+
state.workingState.researchSpecification = session.researchSpecification;
|
|
545
|
+
}
|
|
476
546
|
if (session.currentBlocker) {
|
|
477
547
|
state.openTensions.push(session.currentBlocker);
|
|
478
548
|
}
|
|
@@ -543,7 +613,16 @@ async function removeLegacyRootFiles(projectPath) {
|
|
|
543
613
|
}
|
|
544
614
|
export async function syncCurrentWorkspaceView(context) {
|
|
545
615
|
const state = await loadResearchState(context.stateFilePath);
|
|
546
|
-
const
|
|
616
|
+
const session = {
|
|
617
|
+
...context.session,
|
|
618
|
+
...(context.session.firstResearchShape ?? state.firstResearchShape
|
|
619
|
+
? { firstResearchShape: context.session.firstResearchShape ?? state.firstResearchShape }
|
|
620
|
+
: {}),
|
|
621
|
+
...(context.session.researchSpecification ?? state.researchSpecification
|
|
622
|
+
? { researchSpecification: context.session.researchSpecification ?? state.researchSpecification }
|
|
623
|
+
: {})
|
|
624
|
+
};
|
|
625
|
+
const body = buildCurrentGuide(context.project, session, recentInvocationRecords(state), recentPendingQuestions(state), recentPendingObligations(state));
|
|
547
626
|
await writeFile(context.currentFilePath, body, "utf8");
|
|
548
627
|
return context.currentFilePath;
|
|
549
628
|
}
|
|
@@ -772,6 +851,122 @@ export async function summarizeLongTableInterview(options) {
|
|
|
772
851
|
await syncCurrentWorkspaceView(options.context);
|
|
773
852
|
return { hook, shape, state: updated, session };
|
|
774
853
|
}
|
|
854
|
+
function normalizeStringArray(values) {
|
|
855
|
+
return (values ?? []).map((value) => value.trim()).filter(Boolean);
|
|
856
|
+
}
|
|
857
|
+
function normalizeOptionalString(value) {
|
|
858
|
+
const trimmed = value?.trim();
|
|
859
|
+
return trimmed && trimmed.length > 0 ? trimmed : undefined;
|
|
860
|
+
}
|
|
861
|
+
function normalizeResearchSpecification(input, sourceHookId, timestamp) {
|
|
862
|
+
const title = input.title.trim();
|
|
863
|
+
if (!title) {
|
|
864
|
+
throw new Error("Research Specification title is required.");
|
|
865
|
+
}
|
|
866
|
+
const purpose = input.researchDirection.purpose.trim();
|
|
867
|
+
if (!purpose) {
|
|
868
|
+
throw new Error("Research Specification researchDirection.purpose is required.");
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
title,
|
|
872
|
+
status: input.confirmedAt ? "confirmed" : input.status ?? "draft",
|
|
873
|
+
createdAt: input.createdAt ?? timestamp,
|
|
874
|
+
updatedAt: timestamp,
|
|
875
|
+
...(sourceHookId ? { sourceHookId } : {}),
|
|
876
|
+
researchDirection: {
|
|
877
|
+
...(normalizeOptionalString(input.researchDirection.question) ? { question: normalizeOptionalString(input.researchDirection.question) } : {}),
|
|
878
|
+
purpose,
|
|
879
|
+
...(normalizeOptionalString(input.researchDirection.scopeBoundary) ? { scopeBoundary: normalizeOptionalString(input.researchDirection.scopeBoundary) } : {}),
|
|
880
|
+
inclusionCriteria: normalizeStringArray(input.researchDirection.inclusionCriteria),
|
|
881
|
+
exclusionCriteria: normalizeStringArray(input.researchDirection.exclusionCriteria)
|
|
882
|
+
},
|
|
883
|
+
constructOntology: {
|
|
884
|
+
coreConstructs: normalizeStringArray(input.constructOntology.coreConstructs),
|
|
885
|
+
distinctions: normalizeStringArray(input.constructOntology.distinctions),
|
|
886
|
+
termsToAvoidCollapsing: normalizeStringArray(input.constructOntology.termsToAvoidCollapsing)
|
|
887
|
+
},
|
|
888
|
+
theoryAndFraming: {
|
|
889
|
+
anchors: normalizeStringArray(input.theoryAndFraming.anchors),
|
|
890
|
+
alternatives: normalizeStringArray(input.theoryAndFraming.alternatives),
|
|
891
|
+
overreachRisks: normalizeStringArray(input.theoryAndFraming.overreachRisks)
|
|
892
|
+
},
|
|
893
|
+
measurementCoding: {
|
|
894
|
+
variablesOrConstructs: normalizeStringArray(input.measurementCoding.variablesOrConstructs),
|
|
895
|
+
evidenceTypes: normalizeStringArray(input.measurementCoding.evidenceTypes),
|
|
896
|
+
codingRules: normalizeStringArray(input.measurementCoding.codingRules),
|
|
897
|
+
openStandards: normalizeStringArray(input.measurementCoding.openStandards)
|
|
898
|
+
},
|
|
899
|
+
methodAnalysis: {
|
|
900
|
+
...(normalizeOptionalString(input.methodAnalysis.design) ? { design: normalizeOptionalString(input.methodAnalysis.design) } : {}),
|
|
901
|
+
analysisOptions: normalizeStringArray(input.methodAnalysis.analysisOptions),
|
|
902
|
+
dataSufficiencyCriteria: normalizeStringArray(input.methodAnalysis.dataSufficiencyCriteria),
|
|
903
|
+
unsettledChoices: normalizeStringArray(input.methodAnalysis.unsettledChoices)
|
|
904
|
+
},
|
|
905
|
+
evidenceAccess: {
|
|
906
|
+
requiredSources: normalizeStringArray(input.evidenceAccess.requiredSources),
|
|
907
|
+
accessRequirements: normalizeStringArray(input.evidenceAccess.accessRequirements),
|
|
908
|
+
evidenceStandards: normalizeStringArray(input.evidenceAccess.evidenceStandards)
|
|
909
|
+
},
|
|
910
|
+
epistemicAlignment: {
|
|
911
|
+
researcherKnowledge: normalizeStringArray(input.epistemicAlignment.researcherKnowledge),
|
|
912
|
+
projectStatePriority: normalizeStringArray(input.epistemicAlignment.projectStatePriority),
|
|
913
|
+
aiInferenceLimits: normalizeStringArray(input.epistemicAlignment.aiInferenceLimits),
|
|
914
|
+
...(normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule)
|
|
915
|
+
? { conflictResolutionRule: normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule) }
|
|
916
|
+
: {})
|
|
917
|
+
},
|
|
918
|
+
protectedDecisions: normalizeStringArray(input.protectedDecisions),
|
|
919
|
+
openQuestions: normalizeStringArray(input.openQuestions),
|
|
920
|
+
nextActions: normalizeStringArray(input.nextActions),
|
|
921
|
+
confidence: input.confidence,
|
|
922
|
+
...(input.confirmedAt ? { confirmedAt: input.confirmedAt } : {})
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
export async function summarizeLongTableResearchSpecification(options) {
|
|
926
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
927
|
+
const sourceHookId = options.hookId
|
|
928
|
+
?? options.specification.sourceHookId
|
|
929
|
+
?? state.firstResearchShape?.sourceHookId;
|
|
930
|
+
const existing = sourceHookId
|
|
931
|
+
? (state.hooks ?? []).find((hook) => hook.id === sourceHookId)
|
|
932
|
+
: activeInterviewHook(state);
|
|
933
|
+
const timestamp = nowIso();
|
|
934
|
+
const specification = normalizeResearchSpecification(options.specification, existing?.id ?? sourceHookId, timestamp);
|
|
935
|
+
const hook = existing
|
|
936
|
+
? {
|
|
937
|
+
...existing,
|
|
938
|
+
status: "ready_to_confirm",
|
|
939
|
+
updatedAt: timestamp,
|
|
940
|
+
researchSpecification: specification
|
|
941
|
+
}
|
|
942
|
+
: undefined;
|
|
943
|
+
const session = {
|
|
944
|
+
...options.context.session,
|
|
945
|
+
lastUpdatedAt: timestamp,
|
|
946
|
+
researchSpecification: specification,
|
|
947
|
+
resumeHint: `I want to continue from the Research Specification: ${specification.title}.`
|
|
948
|
+
};
|
|
949
|
+
options.context.session = session;
|
|
950
|
+
let updated = hook ? upsertHook(state, hook) : state;
|
|
951
|
+
updated.researchSpecification = specification;
|
|
952
|
+
updated.workingState = {
|
|
953
|
+
...updated.workingState,
|
|
954
|
+
researchSpecification: specification
|
|
955
|
+
};
|
|
956
|
+
updated.narrativeTraces.push({
|
|
957
|
+
id: createId("narrative_trace"),
|
|
958
|
+
timestamp,
|
|
959
|
+
source: "$longtable-interview",
|
|
960
|
+
traceType: "judgment",
|
|
961
|
+
summary: `Research Specification draft: ${specification.title}.`,
|
|
962
|
+
visibility: "explicit",
|
|
963
|
+
importance: specification.confidence
|
|
964
|
+
});
|
|
965
|
+
await writeFile(options.context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
966
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
967
|
+
await syncCurrentWorkspaceView(options.context);
|
|
968
|
+
return { hook, specification, state: updated, session };
|
|
969
|
+
}
|
|
775
970
|
function findQuestionForDecision(state, questionId) {
|
|
776
971
|
const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
|
|
777
972
|
if (questionId) {
|
|
@@ -1001,6 +1196,29 @@ function optionsForCheckpointTrigger(family, checkpointKey) {
|
|
|
1001
1196
|
function includesAny(prompt, patterns) {
|
|
1002
1197
|
return patterns.some((pattern) => pattern.test(prompt));
|
|
1003
1198
|
}
|
|
1199
|
+
function questionPriority(spec) {
|
|
1200
|
+
const byKey = {
|
|
1201
|
+
protected_decision_closure: 100,
|
|
1202
|
+
research_direction_change_commitment: 96,
|
|
1203
|
+
construct_boundary_commitment: 94,
|
|
1204
|
+
measurement_coding_standard: 92,
|
|
1205
|
+
analysis_strategy_commitment: 90,
|
|
1206
|
+
theory_frame_commitment: 88,
|
|
1207
|
+
method_design_commitment: 86,
|
|
1208
|
+
research_scope_boundary: 84,
|
|
1209
|
+
epistemic_alignment_boundary: 82,
|
|
1210
|
+
value_conflict_boundary: 80,
|
|
1211
|
+
source_authority: 70,
|
|
1212
|
+
knowledge_gap_probe: 68,
|
|
1213
|
+
tacit_assumption_probe: 66,
|
|
1214
|
+
needed_question_policy: 60,
|
|
1215
|
+
philosophical_checkpoint_boundary: 58,
|
|
1216
|
+
harness_question_harness: 55
|
|
1217
|
+
};
|
|
1218
|
+
const confidenceWeight = spec.confidence === "high" ? 10 : spec.confidence === "medium" ? 5 : 0;
|
|
1219
|
+
const requiredWeight = spec.kind === "research_commitment" || spec.required ? 20 : 0;
|
|
1220
|
+
return (byKey[spec.key] ?? 0) + confidenceWeight + requiredWeight;
|
|
1221
|
+
}
|
|
1004
1222
|
function followUpQuestionOptions(first, second, third, fourth) {
|
|
1005
1223
|
return [first, second, third, ...(fourth ? [fourth] : [])];
|
|
1006
1224
|
}
|
|
@@ -1018,6 +1236,158 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
|
1018
1236
|
});
|
|
1019
1237
|
}
|
|
1020
1238
|
}
|
|
1239
|
+
const decisionActionCue = includesAny(normalized, [
|
|
1240
|
+
/\b(final|finalize|commit|decide|settle|freeze|lock|record|apply|incorporate|change|revise|update|replace|reframe|modify|alter)\b/i,
|
|
1241
|
+
/최종|확정|결정|고정|기록|반영|바꾸|변경|수정|교체|전환|재설정/
|
|
1242
|
+
]);
|
|
1243
|
+
const scopeCue = includesAny(normalized, [
|
|
1244
|
+
/\bresearch question\b/i,
|
|
1245
|
+
/\bresearch direction\b/i,
|
|
1246
|
+
/\bscope\b/i,
|
|
1247
|
+
/\bboundary\b/i,
|
|
1248
|
+
/\binclusion criteria\b/i,
|
|
1249
|
+
/\bexclusion criteria\b/i,
|
|
1250
|
+
/연구\s*질문|연구\s*문제|연구\s*방향|범위|경계|포함\s*기준|제외\s*기준/
|
|
1251
|
+
]);
|
|
1252
|
+
const theoryCue = includesAny(normalized, [
|
|
1253
|
+
/\btheory\b/i,
|
|
1254
|
+
/\bframework\b/i,
|
|
1255
|
+
/\bconceptual model\b/i,
|
|
1256
|
+
/\bconstruct map\b/i,
|
|
1257
|
+
/이론|프레임워크|개념\s*모형|구성개념\s*지도|컨스트럭트/
|
|
1258
|
+
]);
|
|
1259
|
+
const measurementCodingCue = includesAny(normalized, [
|
|
1260
|
+
/\bmeasure\b/i,
|
|
1261
|
+
/\bmeasurement\b/i,
|
|
1262
|
+
/\bscale\b/i,
|
|
1263
|
+
/\binstrument\b/i,
|
|
1264
|
+
/\bcoding\b/i,
|
|
1265
|
+
/\bcoding rule\b/i,
|
|
1266
|
+
/\bextraction rule\b/i,
|
|
1267
|
+
/\boperationali[sz]ation\b/i,
|
|
1268
|
+
/측정|척도|도구|코딩|코딩\s*규칙|코딩\s*기준|추출\s*규칙|추출\s*기준|조작화/
|
|
1269
|
+
]);
|
|
1270
|
+
const methodCue = includesAny(normalized, [
|
|
1271
|
+
/\bmethod\b/i,
|
|
1272
|
+
/\bmethodology\b/i,
|
|
1273
|
+
/\bstudy design\b/i,
|
|
1274
|
+
/\bsampling\b/i,
|
|
1275
|
+
/\bsample\b/i,
|
|
1276
|
+
/방법론|방법|연구\s*설계|표본|샘플링/
|
|
1277
|
+
]);
|
|
1278
|
+
const analysisCue = includesAny(normalized, [
|
|
1279
|
+
/\banalysis plan\b/i,
|
|
1280
|
+
/\banalysis method\b/i,
|
|
1281
|
+
/\bmeta[- ]?analysis\b/i,
|
|
1282
|
+
/\bmasem\b/i,
|
|
1283
|
+
/\b(statistical|structural|path|analysis) model\b/i,
|
|
1284
|
+
/\bmoderator\b/i,
|
|
1285
|
+
/\brandom[- ]?effects\b/i,
|
|
1286
|
+
/분석\s*계획|분석\s*방법|메타\s*분석|분석\s*(?:모형|모델)|통계\s*(?:모형|모델)|구조\s*방정식|경로\s*모형|조절효과|랜덤\s*효과/
|
|
1287
|
+
]);
|
|
1288
|
+
const decisionFamilyCount = [scopeCue, theoryCue, measurementCodingCue, methodCue, analysisCue]
|
|
1289
|
+
.filter(Boolean).length;
|
|
1290
|
+
if (decisionActionCue && decisionFamilyCount >= 2) {
|
|
1291
|
+
push({
|
|
1292
|
+
key: "research_direction_change_commitment",
|
|
1293
|
+
kind: "research_commitment",
|
|
1294
|
+
title: "Research direction change",
|
|
1295
|
+
question: "Which high-risk research commitment should LongTable clarify first before changing the project direction?",
|
|
1296
|
+
whyNow: "The prompt touches multiple protected research commitments; proceeding silently would let LongTable choose the priority order for the researcher.",
|
|
1297
|
+
options: followUpQuestionOptions({ value: "scope_first", label: "Scope or research question first", description: "Clarify what the study includes, excludes, or asks before changing downstream work.", recommended: true }, { value: "theory_first", label: "Theory or construct frame first", description: "Clarify the conceptual frame before changing methods or analysis." }, { value: "method_analysis_first", label: "Method or analysis first", description: "Clarify design, data, or analysis implications before changing the artifact." }, { value: "defer", label: "Keep the direction open", description: "Do not change the research direction until the commitment order is explicit." }),
|
|
1298
|
+
confidence: "high",
|
|
1299
|
+
autoEligible: true,
|
|
1300
|
+
required: true,
|
|
1301
|
+
cues: ["research_direction", "multi_commitment_change"]
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
if (decisionActionCue && scopeCue) {
|
|
1305
|
+
push({
|
|
1306
|
+
key: "research_scope_boundary",
|
|
1307
|
+
kind: "research_commitment",
|
|
1308
|
+
title: "Research scope boundary",
|
|
1309
|
+
question: "What scope boundary should LongTable keep explicit before treating the research direction as changed?",
|
|
1310
|
+
whyNow: "Scope changes can redefine the population, domain, corpus, or research question before the researcher has explicitly chosen the boundary.",
|
|
1311
|
+
options: followUpQuestionOptions({ value: "revise_scope", label: "Revise the scope", description: "Change the inclusion boundary before downstream work continues.", recommended: true }, { value: "compare_boundaries", label: "Compare boundaries first", description: "Keep candidate scopes visible before choosing one." }, { value: "proceed_with_scope_assumption", label: "Proceed with scope assumption", description: "Continue only after stating the assumed boundary." }, { value: "defer", label: "Keep scope open", description: "Do not settle the scope yet." }),
|
|
1312
|
+
confidence: "high",
|
|
1313
|
+
autoEligible: true,
|
|
1314
|
+
cues: ["research_scope", "boundary"]
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
if (decisionActionCue && theoryCue) {
|
|
1318
|
+
push({
|
|
1319
|
+
key: "theory_frame_commitment",
|
|
1320
|
+
kind: "research_commitment",
|
|
1321
|
+
title: "Theory frame",
|
|
1322
|
+
question: "Which theory or construct frame should LongTable treat as the candidate commitment before revising the model?",
|
|
1323
|
+
whyNow: "Theory-frame changes can make later measurement, coding, and analysis choices look settled when they are only inferred.",
|
|
1324
|
+
options: followUpQuestionOptions({ value: "compare_theories", label: "Compare theory frames first", description: "Keep competing theoretical anchors visible before choosing one.", recommended: true }, { value: "revise_theory", label: "Revise the frame", description: "Change the framework and record the conceptual tradeoff." }, { value: "use_current_frame", label: "Use current frame tentatively", description: "Proceed but mark the theory choice as provisional." }, { value: "defer", label: "Keep theory open", description: "Do not settle the theory frame yet." }),
|
|
1325
|
+
confidence: "high",
|
|
1326
|
+
autoEligible: true,
|
|
1327
|
+
cues: ["theory", "framework", "construct"]
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
if (decisionActionCue && measurementCodingCue) {
|
|
1331
|
+
push({
|
|
1332
|
+
key: "measurement_coding_standard",
|
|
1333
|
+
kind: "research_commitment",
|
|
1334
|
+
title: "Measurement and coding standard",
|
|
1335
|
+
question: "What measurement or coding rule should LongTable ask you to fix before extracting or revising data?",
|
|
1336
|
+
whyNow: "Measurement and coding rules decide what counts as evidence; changing them silently can make later synthesis non-reproducible.",
|
|
1337
|
+
options: followUpQuestionOptions({ value: "define_construct_rule", label: "Define construct rule first", description: "Clarify what counts as the construct or variable before extraction.", recommended: true }, { value: "define_extraction_rule", label: "Define extraction rule first", description: "Clarify correlation, path, beta, or qualitative evidence extraction rules." }, { value: "pilot_code", label: "Pilot the coding rule", description: "Test the rule on a small sample before committing." }, { value: "defer", label: "Keep coding open", description: "Do not settle measurement or coding yet." }),
|
|
1338
|
+
confidence: "high",
|
|
1339
|
+
autoEligible: true,
|
|
1340
|
+
cues: ["measurement", "coding", "extraction"]
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
if (decisionActionCue && methodCue) {
|
|
1344
|
+
push({
|
|
1345
|
+
key: "method_design_commitment",
|
|
1346
|
+
kind: "research_commitment",
|
|
1347
|
+
title: "Method design",
|
|
1348
|
+
question: "Which method-design choice should LongTable keep explicit before changing the study plan?",
|
|
1349
|
+
whyNow: "Method changes affect the defensible claim, sample, evidence standard, and ethics boundary.",
|
|
1350
|
+
options: followUpQuestionOptions({ value: "revise_design", label: "Revise design first", description: "Change the design or sample boundary before proceeding.", recommended: true }, { value: "check_feasibility", label: "Check feasibility first", description: "Inspect whether data, evidence, and access support the method." }, { value: "proceed_with_method_assumption", label: "Proceed with method assumption", description: "Continue only after stating the assumed method." }, { value: "defer", label: "Keep method open", description: "Do not commit the method yet." }),
|
|
1351
|
+
confidence: "high",
|
|
1352
|
+
autoEligible: true,
|
|
1353
|
+
cues: ["method", "study_design", "sample"]
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
if (decisionActionCue && analysisCue) {
|
|
1357
|
+
push({
|
|
1358
|
+
key: "analysis_strategy_commitment",
|
|
1359
|
+
kind: "research_commitment",
|
|
1360
|
+
title: "Analysis strategy",
|
|
1361
|
+
question: "What analysis strategy should LongTable treat as unsettled before revising or running the synthesis?",
|
|
1362
|
+
whyNow: "Analysis choices determine what effect sizes, moderators, and interpretations become defensible.",
|
|
1363
|
+
options: followUpQuestionOptions({ value: "choose_analysis_family", label: "Choose analysis family first", description: "Clarify MASEM, family-level meta-analysis, moderator analysis, or narrative synthesis.", recommended: true }, { value: "check_data_sufficiency", label: "Check data sufficiency first", description: "Inspect whether primary quantitative effects support the analysis." }, { value: "proceed_with_analysis_assumption", label: "Proceed with assumption", description: "Continue only after stating the analysis assumption." }, { value: "defer", label: "Keep analysis open", description: "Do not settle the analysis plan yet." }),
|
|
1364
|
+
confidence: "high",
|
|
1365
|
+
autoEligible: true,
|
|
1366
|
+
cues: ["analysis", "model", "meta_analysis"]
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
if (decisionActionCue && includesAny(normalized, [
|
|
1370
|
+
/\bconflict\b/i,
|
|
1371
|
+
/\bcontradict/i,
|
|
1372
|
+
/\bontology\b/i,
|
|
1373
|
+
/\bepistem/i,
|
|
1374
|
+
/\bhuman knowledge\b/i,
|
|
1375
|
+
/\bai knowledge\b/i,
|
|
1376
|
+
/\balignment\b/i,
|
|
1377
|
+
/충돌|상충|모순|존재론|인식론|지식|인간의\s*지식|ai의\s*지식|정렬|방향성/
|
|
1378
|
+
])) {
|
|
1379
|
+
push({
|
|
1380
|
+
key: "epistemic_alignment_boundary",
|
|
1381
|
+
kind: "value_conflict",
|
|
1382
|
+
title: "Knowledge alignment",
|
|
1383
|
+
question: "When researcher knowledge, AI inference, and project state conflict, what should LongTable privilege before acting?",
|
|
1384
|
+
whyNow: "The prompt asks LongTable to mediate knowledge conflict; that mediation should be visible rather than hidden inside an implementation choice.",
|
|
1385
|
+
options: followUpQuestionOptions({ value: "ask_researcher", label: "Ask researcher clarity first", description: "Pause when human meaning or priority is underspecified.", recommended: true }, { value: "inspect_project_state", label: "Inspect project state first", description: "Use durable files and prior decisions before inferring intent." }, { value: "proceed_with_trace", label: "Proceed with explicit trace", description: "Continue only after naming the conflict and assumption." }, { value: "defer", label: "Keep conflict open", description: "Do not collapse the conflict into one answer yet." }),
|
|
1386
|
+
confidence: "high",
|
|
1387
|
+
autoEligible: true,
|
|
1388
|
+
cues: ["knowledge_conflict", "epistemic_alignment"]
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1021
1391
|
if (includesAny(normalized, [
|
|
1022
1392
|
/\blongtable\b/,
|
|
1023
1393
|
/\bharness\b/,
|
|
@@ -1251,7 +1621,13 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
|
1251
1621
|
if (options.requiredOnly === true) {
|
|
1252
1622
|
selected = selected.filter((spec) => spec.kind === "research_commitment");
|
|
1253
1623
|
}
|
|
1254
|
-
|
|
1624
|
+
if (normalized.includes("protected decision closure pressure")) {
|
|
1625
|
+
selected = selected.filter((spec) => spec.key === "protected_decision_closure");
|
|
1626
|
+
}
|
|
1627
|
+
else if (options.requiredOnly === true && selected.some((spec) => spec.key === "research_direction_change_commitment")) {
|
|
1628
|
+
selected = selected.filter((spec) => spec.key === "research_direction_change_commitment");
|
|
1629
|
+
}
|
|
1630
|
+
return selected.sort((a, b) => questionPriority(b) - questionPriority(a));
|
|
1255
1631
|
}
|
|
1256
1632
|
function buildFollowUpQuestionSpecs(prompt) {
|
|
1257
1633
|
return buildQuestionOpportunitySpecs(prompt);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
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.44",
|
|
33
|
+
"@longtable/core": "0.1.44",
|
|
34
|
+
"@longtable/memory": "0.1.44",
|
|
35
|
+
"@longtable/provider-claude": "0.1.44",
|
|
36
|
+
"@longtable/provider-codex": "0.1.44",
|
|
37
|
+
"@longtable/setup": "0.1.44"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|