@longtable/cli 0.1.31 → 0.1.33
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 +16 -13
- package/dist/cli.js +265 -36
- package/dist/codex-hooks.d.ts +22 -0
- package/dist/codex-hooks.js +240 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/longtable-codex-native-hook.d.ts +4 -0
- package/dist/longtable-codex-native-hook.js +314 -0
- package/dist/project-session.d.ts +108 -3
- package/dist/project-session.js +420 -23
- package/dist/prompt-aliases.js +5 -5
- package/dist/question-obligations.d.ts +22 -0
- package/dist/question-obligations.js +112 -0
- package/package.json +7 -7
package/dist/project-session.js
CHANGED
|
@@ -4,6 +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 { ensureRequiredQuestionObligation, pendingQuestionObligations, resolveQuestionObligationByQuestionId } from "./question-obligations.js";
|
|
7
8
|
const CURRENT_FILE_NAME = "CURRENT.md";
|
|
8
9
|
const LEGACY_ROOT_FILES = ["LONGTABLE.md", "START-HERE.md", "NEXT-STEPS.md", "SESSION-SNAPSHOT.md"];
|
|
9
10
|
function nowIso() {
|
|
@@ -54,6 +55,9 @@ function resolveUserLocale() {
|
|
|
54
55
|
return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale);
|
|
55
56
|
}
|
|
56
57
|
function buildFirstQuestion(session) {
|
|
58
|
+
if (session.firstResearchShape?.openQuestions?.[0]) {
|
|
59
|
+
return session.firstResearchShape.openQuestions[0];
|
|
60
|
+
}
|
|
57
61
|
return session.currentBlocker
|
|
58
62
|
? `Where does "${session.currentBlocker}" show up most concretely in the scene, material, or evidence?`
|
|
59
63
|
: `What scene, case, text, data, or draft would make "${session.currentGoal}" easiest to inspect first?`;
|
|
@@ -63,20 +67,23 @@ function buildOpenQuestions(session) {
|
|
|
63
67
|
if (session.startInterview) {
|
|
64
68
|
return [
|
|
65
69
|
firstQuestion,
|
|
66
|
-
`What
|
|
70
|
+
`What still feels hardest to name or make concrete in "${session.currentGoal}"?`
|
|
67
71
|
];
|
|
68
72
|
}
|
|
69
73
|
return session.currentBlocker
|
|
70
74
|
? [
|
|
71
75
|
firstQuestion,
|
|
72
|
-
`What would
|
|
76
|
+
`What would give "${session.currentBlocker}" a usable first research handle without forcing a final research question yet?`
|
|
73
77
|
]
|
|
74
78
|
: [
|
|
75
79
|
firstQuestion,
|
|
76
|
-
`What would
|
|
80
|
+
`What would give this project a usable first research handle without pretending the question is settled?`
|
|
77
81
|
];
|
|
78
82
|
}
|
|
79
83
|
function buildNextAction(session) {
|
|
84
|
+
if (session.firstResearchShape) {
|
|
85
|
+
return session.firstResearchShape.nextAction;
|
|
86
|
+
}
|
|
80
87
|
if (session.startInterview) {
|
|
81
88
|
return session.currentBlocker
|
|
82
89
|
? `Begin from the start-interview brief, then make "${session.currentBlocker}" concrete with one scene, source, case, or dataset.`
|
|
@@ -87,6 +94,9 @@ function buildNextAction(session) {
|
|
|
87
94
|
: "Open with your current goal in one sentence, then ask LongTable for the first concrete research move.";
|
|
88
95
|
}
|
|
89
96
|
function buildResumeHint(session) {
|
|
97
|
+
if (session.firstResearchShape) {
|
|
98
|
+
return `I want to continue from the First Research Shape: ${session.firstResearchShape.handle}.`;
|
|
99
|
+
}
|
|
90
100
|
if (session.startInterview) {
|
|
91
101
|
return session.currentBlocker
|
|
92
102
|
? `I want to continue from the LongTable start interview. The first unresolved issue is ${session.currentBlocker}.`
|
|
@@ -96,7 +106,7 @@ function buildResumeHint(session) {
|
|
|
96
106
|
? `I want to continue ${session.currentGoal}. The unresolved blocker is ${session.currentBlocker}.`
|
|
97
107
|
: `I want to continue ${session.currentGoal}.`;
|
|
98
108
|
}
|
|
99
|
-
function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = []) {
|
|
109
|
+
function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
|
|
100
110
|
const locale = normalizeLocale(session.locale ?? project.locale);
|
|
101
111
|
const openQuestions = session.openQuestions && session.openQuestions.length > 0
|
|
102
112
|
? session.openQuestions
|
|
@@ -118,6 +128,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
118
128
|
...(session.researchObject ? [`- 연구 객체: ${session.researchObject}`] : []),
|
|
119
129
|
...(session.gapRisk ? [`- 공백/암묵지 위험: ${session.gapRisk}`] : []),
|
|
120
130
|
...(session.protectedDecision ? [`- 보호할 결정: ${session.protectedDecision}`] : []),
|
|
131
|
+
...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
|
|
121
132
|
...(session.startInterview ? [`- start interview: ${session.startInterview.summary}`] : []),
|
|
122
133
|
`- 다음 액션: ${nextAction}`,
|
|
123
134
|
`- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
@@ -146,13 +157,32 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
146
157
|
"- 답변 기록: `longtable decide --question <id> --answer <value>`"
|
|
147
158
|
]
|
|
148
159
|
: []),
|
|
160
|
+
...(pendingObligations.length > 0
|
|
161
|
+
? [
|
|
162
|
+
"",
|
|
163
|
+
"## 대기 중인 연구 의무",
|
|
164
|
+
...pendingObligations.map((obligation) => {
|
|
165
|
+
const linked = obligation.questionId ? ` [question: ${obligation.questionId}]` : "";
|
|
166
|
+
return `- ${obligation.prompt}${linked}: ${obligation.reason}`;
|
|
167
|
+
})
|
|
168
|
+
]
|
|
169
|
+
: []),
|
|
149
170
|
"",
|
|
150
171
|
"## 다시 시작 문장",
|
|
151
172
|
`- "${resumeHint}"`,
|
|
173
|
+
...(session.firstResearchShape
|
|
174
|
+
? [
|
|
175
|
+
"",
|
|
176
|
+
"## First Research Shape",
|
|
177
|
+
`- Handle: ${session.firstResearchShape.handle}`,
|
|
178
|
+
`- Confidence: ${session.firstResearchShape.confidence}`,
|
|
179
|
+
...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
|
|
180
|
+
]
|
|
181
|
+
: []),
|
|
152
182
|
"",
|
|
153
183
|
"## 빠른 시작",
|
|
154
184
|
"- 이 디렉토리에서 `codex`를 엽니다.",
|
|
155
|
-
`- 첫 메시지는 보통 \`${suggestedPrompt}\` 정도면 충분합니다.`,
|
|
185
|
+
`- 첫 메시지는 보통 \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\` 정도면 충분합니다.`,
|
|
156
186
|
"",
|
|
157
187
|
"## 증거 규칙",
|
|
158
188
|
"- 외부 사실이나 현재 정보는 source를 붙이거나 inference로 낮춥니다."
|
|
@@ -171,6 +201,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
171
201
|
...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
|
|
172
202
|
...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
|
|
173
203
|
...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
|
|
204
|
+
...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
|
|
174
205
|
...(session.startInterview ? [`- Start interview: ${session.startInterview.summary}`] : []),
|
|
175
206
|
`- Next action: ${nextAction}`,
|
|
176
207
|
`- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
@@ -199,13 +230,32 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
199
230
|
"- Record an answer: `longtable decide --question <id> --answer <value>`"
|
|
200
231
|
]
|
|
201
232
|
: []),
|
|
233
|
+
...(pendingObligations.length > 0
|
|
234
|
+
? [
|
|
235
|
+
"",
|
|
236
|
+
"## Pending Research Obligations",
|
|
237
|
+
...pendingObligations.map((obligation) => {
|
|
238
|
+
const linked = obligation.questionId ? ` [question: ${obligation.questionId}]` : "";
|
|
239
|
+
return `- ${obligation.prompt}${linked}: ${obligation.reason}`;
|
|
240
|
+
})
|
|
241
|
+
]
|
|
242
|
+
: []),
|
|
202
243
|
"",
|
|
203
244
|
"## Restart Prompt",
|
|
204
245
|
`- "${resumeHint}"`,
|
|
246
|
+
...(session.firstResearchShape
|
|
247
|
+
? [
|
|
248
|
+
"",
|
|
249
|
+
"## First Research Shape",
|
|
250
|
+
`- Handle: ${session.firstResearchShape.handle}`,
|
|
251
|
+
`- Confidence: ${session.firstResearchShape.confidence}`,
|
|
252
|
+
...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
|
|
253
|
+
]
|
|
254
|
+
: []),
|
|
205
255
|
"",
|
|
206
256
|
"## Quick Start",
|
|
207
257
|
"- Open `codex` in this directory.",
|
|
208
|
-
`- A good first message is usually \`${suggestedPrompt}\`.`,
|
|
258
|
+
`- A good first message is usually \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\`.`,
|
|
209
259
|
"",
|
|
210
260
|
"## Evidence Rule",
|
|
211
261
|
"- External or current claims should carry a source link or be labeled as inference."
|
|
@@ -220,6 +270,9 @@ async function loadResearchState(stateFilePath) {
|
|
|
220
270
|
...parsed,
|
|
221
271
|
explicitState: parsed.explicitState ?? {},
|
|
222
272
|
workingState: parsed.workingState ?? {},
|
|
273
|
+
hooks: parsed.hooks ?? [],
|
|
274
|
+
...(parsed.firstResearchShape ? { firstResearchShape: parsed.firstResearchShape } : {}),
|
|
275
|
+
questionObligations: parsed.questionObligations ?? [],
|
|
223
276
|
inferredHypotheses: parsed.inferredHypotheses ?? [],
|
|
224
277
|
openTensions: parsed.openTensions ?? [],
|
|
225
278
|
decisionLog: parsed.decisionLog ?? [],
|
|
@@ -242,6 +295,17 @@ function recentPendingQuestions(state, limit = 3) {
|
|
|
242
295
|
.slice(-limit)
|
|
243
296
|
.reverse();
|
|
244
297
|
}
|
|
298
|
+
function visiblePendingObligations(state) {
|
|
299
|
+
const pendingQuestionIds = new Set((state.questionLog ?? [])
|
|
300
|
+
.filter((record) => record.status === "pending")
|
|
301
|
+
.map((record) => record.id));
|
|
302
|
+
return pendingQuestionObligations(state).filter((obligation) => !obligation.questionId || !pendingQuestionIds.has(obligation.questionId));
|
|
303
|
+
}
|
|
304
|
+
function recentPendingObligations(state, limit = 3) {
|
|
305
|
+
return visiblePendingObligations(state)
|
|
306
|
+
.slice(-limit)
|
|
307
|
+
.reverse();
|
|
308
|
+
}
|
|
245
309
|
function formatQuestionOptionValues(record) {
|
|
246
310
|
const values = record.prompt.options.map((option) => option.value);
|
|
247
311
|
if (record.prompt.allowOther) {
|
|
@@ -253,6 +317,7 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
253
317
|
const questions = state.questionLog ?? [];
|
|
254
318
|
const pendingQuestions = questions.filter((record) => record.status === "pending");
|
|
255
319
|
const answeredQuestions = questions.filter((record) => record.status === "answered");
|
|
320
|
+
const pendingObligations = visiblePendingObligations(state);
|
|
256
321
|
return {
|
|
257
322
|
found: true,
|
|
258
323
|
rootPath: context.project.projectPath,
|
|
@@ -279,6 +344,7 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
279
344
|
invocations: (state.invocationLog ?? []).length,
|
|
280
345
|
questions: questions.length,
|
|
281
346
|
pendingQuestions: pendingQuestions.length,
|
|
347
|
+
pendingObligations: pendingObligations.length,
|
|
282
348
|
answeredQuestions: answeredQuestions.length,
|
|
283
349
|
decisions: (state.decisionLog ?? []).length
|
|
284
350
|
},
|
|
@@ -299,6 +365,13 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
299
365
|
options: formatQuestionOptionValues(record),
|
|
300
366
|
required: record.prompt.required
|
|
301
367
|
})),
|
|
368
|
+
pendingObligations: pendingObligations.slice(-5).reverse().map((obligation) => ({
|
|
369
|
+
id: obligation.id,
|
|
370
|
+
kind: obligation.kind,
|
|
371
|
+
prompt: obligation.prompt,
|
|
372
|
+
reason: obligation.reason,
|
|
373
|
+
...(obligation.questionId ? { questionId: obligation.questionId } : {})
|
|
374
|
+
})),
|
|
302
375
|
recentDecisions: (state.decisionLog ?? []).slice(-5).reverse().map((record) => ({
|
|
303
376
|
id: record.id,
|
|
304
377
|
checkpointKey: record.checkpointKey,
|
|
@@ -339,13 +412,16 @@ function buildProjectAgentsMd(project, session) {
|
|
|
339
412
|
"- Treat `AGENTS.md` as runtime guidance, not as the researcher-facing resume artifact.",
|
|
340
413
|
"",
|
|
341
414
|
"## Invocation Rules",
|
|
415
|
+
"- If the user message starts with `$longtable-interview`, run the LongTable interview flow before generic research advice.",
|
|
342
416
|
"- If the user message starts with `lt `, `longtable `, `long table `, or `롱테이블 ` followed by a directive and `:`, treat it as an explicit LongTable invocation.",
|
|
343
|
-
"- Supported explicit directives are: explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
|
|
417
|
+
"- Supported explicit directives are: interview, explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
|
|
344
418
|
"- For explicit LongTable invocations, do not begin by scanning the workspace. Use the current session files first and answer as LongTable immediately.",
|
|
345
419
|
"- For general research requests in this workspace, prefer LongTable behavior before generic coding behavior.",
|
|
346
420
|
"",
|
|
347
421
|
"## Research Behavior",
|
|
348
422
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
423
|
+
"- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, and avoid early reader/reviewer or theory/method/measurement classification.",
|
|
424
|
+
"- Use structured options only at the final First Research Shape confirmation or at true checkpoint boundaries.",
|
|
349
425
|
"- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
|
|
350
426
|
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
351
427
|
...(session.disagreementPreference === "always_visible"
|
|
@@ -362,6 +438,7 @@ function buildProjectAgentsMd(project, session) {
|
|
|
362
438
|
...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
|
|
363
439
|
...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
|
|
364
440
|
...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
|
|
441
|
+
...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
|
|
365
442
|
...(session.startInterview ? [`- Start interview summary: ${session.startInterview.summary}`] : []),
|
|
366
443
|
`- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
367
444
|
`- Disagreement visibility: ${session.disagreementPreference}`,
|
|
@@ -390,6 +467,10 @@ function buildStateSeed(project, session, setup) {
|
|
|
390
467
|
...(session.startInterview ? { startInterview: session.startInterview } : {}),
|
|
391
468
|
...(session.resumeHint ? { resumeHint: session.resumeHint } : {})
|
|
392
469
|
};
|
|
470
|
+
if (session.firstResearchShape) {
|
|
471
|
+
state.firstResearchShape = session.firstResearchShape;
|
|
472
|
+
state.workingState.firstResearchShape = session.firstResearchShape;
|
|
473
|
+
}
|
|
393
474
|
if (session.currentBlocker) {
|
|
394
475
|
state.openTensions.push(session.currentBlocker);
|
|
395
476
|
}
|
|
@@ -460,7 +541,7 @@ async function removeLegacyRootFiles(projectPath) {
|
|
|
460
541
|
}
|
|
461
542
|
export async function syncCurrentWorkspaceView(context) {
|
|
462
543
|
const state = await loadResearchState(context.stateFilePath);
|
|
463
|
-
const body = buildCurrentGuide(context.project, context.session, recentInvocationRecords(state), recentPendingQuestions(state));
|
|
544
|
+
const body = buildCurrentGuide(context.project, context.session, recentInvocationRecords(state), recentPendingQuestions(state), recentPendingObligations(state));
|
|
464
545
|
await writeFile(context.currentFilePath, body, "utf8");
|
|
465
546
|
return context.currentFilePath;
|
|
466
547
|
}
|
|
@@ -477,6 +558,200 @@ export async function appendInvocationRecordToWorkspace(context, invocation, que
|
|
|
477
558
|
function createId(prefix) {
|
|
478
559
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
479
560
|
}
|
|
561
|
+
function normalizeInterviewQuality(answer, quality) {
|
|
562
|
+
if (quality) {
|
|
563
|
+
return quality;
|
|
564
|
+
}
|
|
565
|
+
const trimmed = answer.trim();
|
|
566
|
+
const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
|
|
567
|
+
if (trimmed.length < 12 || wordCount < 3) {
|
|
568
|
+
return "thin";
|
|
569
|
+
}
|
|
570
|
+
if (trimmed.length > 80 || wordCount >= 12) {
|
|
571
|
+
return "rich";
|
|
572
|
+
}
|
|
573
|
+
return "usable";
|
|
574
|
+
}
|
|
575
|
+
function defaultFollowUpQuestion(answer) {
|
|
576
|
+
const trimmed = answer.trim();
|
|
577
|
+
if (trimmed.length < 12) {
|
|
578
|
+
return "Say one more sentence about where this problem appears or why it matters before LongTable tries to classify it.";
|
|
579
|
+
}
|
|
580
|
+
return "What concrete scene, case, material, text, dataset, or decision would make that problem easier to inspect first?";
|
|
581
|
+
}
|
|
582
|
+
function depthForInterview(turns = []) {
|
|
583
|
+
const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
|
|
584
|
+
if (usableTurns >= 3) {
|
|
585
|
+
return "ready_to_summarize";
|
|
586
|
+
}
|
|
587
|
+
if (usableTurns >= 1) {
|
|
588
|
+
return "forming_first_handle";
|
|
589
|
+
}
|
|
590
|
+
return "gathering_context";
|
|
591
|
+
}
|
|
592
|
+
function activeInterviewHook(state, hookId) {
|
|
593
|
+
const hooks = state.hooks ?? [];
|
|
594
|
+
if (hookId) {
|
|
595
|
+
return hooks.find((hook) => hook.id === hookId);
|
|
596
|
+
}
|
|
597
|
+
return [...hooks].reverse().find((hook) => hook.kind === "longtable_interview" &&
|
|
598
|
+
(hook.status === "pending" || hook.status === "active" || hook.status === "ready_to_confirm"));
|
|
599
|
+
}
|
|
600
|
+
function upsertHook(state, hook) {
|
|
601
|
+
const hooks = state.hooks ?? [];
|
|
602
|
+
const existingIndex = hooks.findIndex((candidate) => candidate.id === hook.id);
|
|
603
|
+
const nextHooks = existingIndex >= 0
|
|
604
|
+
? hooks.map((candidate) => candidate.id === hook.id ? hook : candidate)
|
|
605
|
+
: [...hooks, hook];
|
|
606
|
+
return {
|
|
607
|
+
...state,
|
|
608
|
+
hooks: nextHooks
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
export async function beginLongTableInterview(options) {
|
|
612
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
613
|
+
const existing = activeInterviewHook(state);
|
|
614
|
+
if (existing) {
|
|
615
|
+
return { hook: existing, state };
|
|
616
|
+
}
|
|
617
|
+
const timestamp = nowIso();
|
|
618
|
+
const hook = {
|
|
619
|
+
id: createId("hook_interview"),
|
|
620
|
+
kind: "longtable_interview",
|
|
621
|
+
status: "active",
|
|
622
|
+
createdAt: timestamp,
|
|
623
|
+
updatedAt: timestamp,
|
|
624
|
+
targetOutcome: "first_research_handle",
|
|
625
|
+
depth: "gathering_context",
|
|
626
|
+
provider: options.provider,
|
|
627
|
+
turns: [],
|
|
628
|
+
qualityNotes: [],
|
|
629
|
+
rationale: [
|
|
630
|
+
"Official LongTable research start surface is provider-native `$longtable-interview`, not the CLI start questionnaire.",
|
|
631
|
+
"The hook keeps early research ambiguity open until a first research handle can be summarized."
|
|
632
|
+
]
|
|
633
|
+
};
|
|
634
|
+
let updated = upsertHook(state, hook);
|
|
635
|
+
updated.workingState = {
|
|
636
|
+
...updated.workingState,
|
|
637
|
+
activeInterviewHookId: hook.id,
|
|
638
|
+
interviewSurface: "$longtable-interview",
|
|
639
|
+
...(options.openingQuestion ? { interviewOpeningQuestion: options.openingQuestion } : {}),
|
|
640
|
+
...(options.seedAnswer ? { interviewSeedAnswer: options.seedAnswer } : {})
|
|
641
|
+
};
|
|
642
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
643
|
+
await syncCurrentWorkspaceView(options.context);
|
|
644
|
+
return { hook, state: updated };
|
|
645
|
+
}
|
|
646
|
+
export async function appendLongTableInterviewTurn(options) {
|
|
647
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
648
|
+
const existing = activeInterviewHook(state, options.hookId);
|
|
649
|
+
if (!existing) {
|
|
650
|
+
throw new Error("No active LongTable interview hook was found. Run begin_interview first.");
|
|
651
|
+
}
|
|
652
|
+
const quality = normalizeInterviewQuality(options.answer, options.quality);
|
|
653
|
+
const needsFollowUp = options.needsFollowUp ?? quality === "thin";
|
|
654
|
+
const followUpQuestion = needsFollowUp
|
|
655
|
+
? options.followUpQuestion ?? defaultFollowUpQuestion(options.answer)
|
|
656
|
+
: options.followUpQuestion;
|
|
657
|
+
const timestamp = nowIso();
|
|
658
|
+
const turns = existing.turns ?? [];
|
|
659
|
+
const turn = {
|
|
660
|
+
id: createId("interview_turn"),
|
|
661
|
+
index: turns.length + 1,
|
|
662
|
+
createdAt: timestamp,
|
|
663
|
+
question: options.question.trim(),
|
|
664
|
+
answer: options.answer.trim(),
|
|
665
|
+
...(options.reflection?.trim() ? { reflection: options.reflection.trim() } : {}),
|
|
666
|
+
quality,
|
|
667
|
+
needsFollowUp,
|
|
668
|
+
...(followUpQuestion?.trim() ? { followUpQuestion: followUpQuestion.trim() } : {}),
|
|
669
|
+
...(options.rationale && options.rationale.length > 0 ? { rationale: options.rationale } : {})
|
|
670
|
+
};
|
|
671
|
+
const nextTurns = [...turns, turn];
|
|
672
|
+
const depth = depthForInterview(nextTurns);
|
|
673
|
+
const hook = {
|
|
674
|
+
...existing,
|
|
675
|
+
status: depth === "ready_to_summarize" ? "ready_to_confirm" : "active",
|
|
676
|
+
updatedAt: timestamp,
|
|
677
|
+
depth,
|
|
678
|
+
turns: nextTurns,
|
|
679
|
+
qualityNotes: [
|
|
680
|
+
...(existing.qualityNotes ?? []),
|
|
681
|
+
...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : [])
|
|
682
|
+
]
|
|
683
|
+
};
|
|
684
|
+
const updated = upsertHook(state, hook);
|
|
685
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
686
|
+
await syncCurrentWorkspaceView(options.context);
|
|
687
|
+
return { hook, turn, state: updated };
|
|
688
|
+
}
|
|
689
|
+
export async function summarizeLongTableInterview(options) {
|
|
690
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
691
|
+
const existing = activeInterviewHook(state, options.hookId);
|
|
692
|
+
if (!existing) {
|
|
693
|
+
throw new Error("No active LongTable interview hook was found. Run begin_interview first.");
|
|
694
|
+
}
|
|
695
|
+
const timestamp = nowIso();
|
|
696
|
+
const shape = {
|
|
697
|
+
...options.shape,
|
|
698
|
+
handle: options.shape.handle.trim(),
|
|
699
|
+
currentGoal: options.shape.currentGoal.trim(),
|
|
700
|
+
openQuestions: options.shape.openQuestions.map((question) => question.trim()).filter(Boolean),
|
|
701
|
+
nextAction: options.shape.nextAction.trim(),
|
|
702
|
+
sourceHookId: existing.id
|
|
703
|
+
};
|
|
704
|
+
const hook = {
|
|
705
|
+
...existing,
|
|
706
|
+
status: "ready_to_confirm",
|
|
707
|
+
updatedAt: timestamp,
|
|
708
|
+
depth: "ready_to_summarize",
|
|
709
|
+
firstResearchShape: shape
|
|
710
|
+
};
|
|
711
|
+
const session = {
|
|
712
|
+
...options.context.session,
|
|
713
|
+
lastUpdatedAt: timestamp,
|
|
714
|
+
currentGoal: shape.currentGoal,
|
|
715
|
+
...(shape.currentBlocker ? { currentBlocker: shape.currentBlocker } : {}),
|
|
716
|
+
...(shape.researchObject ? { researchObject: shape.researchObject } : {}),
|
|
717
|
+
...(shape.gapRisk ? { gapRisk: shape.gapRisk } : {}),
|
|
718
|
+
...(shape.protectedDecision ? { protectedDecision: shape.protectedDecision } : {}),
|
|
719
|
+
nextAction: shape.nextAction,
|
|
720
|
+
openQuestions: shape.openQuestions,
|
|
721
|
+
firstResearchShape: shape,
|
|
722
|
+
resumeHint: `I want to continue from the First Research Shape: ${shape.handle}.`
|
|
723
|
+
};
|
|
724
|
+
options.context.session = session;
|
|
725
|
+
let updated = upsertHook(state, hook);
|
|
726
|
+
updated.firstResearchShape = shape;
|
|
727
|
+
updated.workingState = {
|
|
728
|
+
...updated.workingState,
|
|
729
|
+
currentGoal: shape.currentGoal,
|
|
730
|
+
...(shape.currentBlocker ? { currentBlocker: shape.currentBlocker } : {}),
|
|
731
|
+
...(shape.researchObject ? { researchObject: shape.researchObject } : {}),
|
|
732
|
+
...(shape.gapRisk ? { gapRisk: shape.gapRisk } : {}),
|
|
733
|
+
...(shape.protectedDecision ? { protectedDecision: shape.protectedDecision } : {}),
|
|
734
|
+
openQuestions: shape.openQuestions,
|
|
735
|
+
nextAction: shape.nextAction,
|
|
736
|
+
firstResearchShape: shape
|
|
737
|
+
};
|
|
738
|
+
if (shape.currentBlocker && !updated.openTensions.includes(shape.currentBlocker)) {
|
|
739
|
+
updated.openTensions.push(shape.currentBlocker);
|
|
740
|
+
}
|
|
741
|
+
updated.narrativeTraces.push({
|
|
742
|
+
id: createId("narrative_trace"),
|
|
743
|
+
timestamp,
|
|
744
|
+
source: "$longtable-interview",
|
|
745
|
+
traceType: "judgment",
|
|
746
|
+
summary: `First Research Shape: ${shape.handle}.`,
|
|
747
|
+
visibility: "explicit",
|
|
748
|
+
importance: shape.confidence
|
|
749
|
+
});
|
|
750
|
+
await writeFile(options.context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
751
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
752
|
+
await syncCurrentWorkspaceView(options.context);
|
|
753
|
+
return { hook, shape, state: updated, session };
|
|
754
|
+
}
|
|
480
755
|
function findQuestionForDecision(state, questionId) {
|
|
481
756
|
const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
|
|
482
757
|
if (questionId) {
|
|
@@ -484,6 +759,9 @@ function findQuestionForDecision(state, questionId) {
|
|
|
484
759
|
}
|
|
485
760
|
return pending.at(-1) ?? null;
|
|
486
761
|
}
|
|
762
|
+
function findPendingQuestionForClear(state, questionId) {
|
|
763
|
+
return (state.questionLog ?? []).find((record) => record.id === questionId && record.status === "pending") ?? null;
|
|
764
|
+
}
|
|
487
765
|
function pendingRequiredQuestions(state) {
|
|
488
766
|
return (state.questionLog ?? []).filter((record) => record.status === "pending" && record.prompt.required);
|
|
489
767
|
}
|
|
@@ -491,19 +769,36 @@ export async function listBlockingWorkspaceQuestions(context) {
|
|
|
491
769
|
const state = await loadResearchState(context.stateFilePath);
|
|
492
770
|
return pendingRequiredQuestions(state);
|
|
493
771
|
}
|
|
772
|
+
export async function listBlockingWorkspaceObligations(context) {
|
|
773
|
+
const state = await loadResearchState(context.stateFilePath);
|
|
774
|
+
return pendingQuestionObligations(state);
|
|
775
|
+
}
|
|
494
776
|
export async function assertWorkspaceNotBlocked(context) {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
777
|
+
const [blockingQuestions, blockingObligations] = await Promise.all([
|
|
778
|
+
listBlockingWorkspaceQuestions(context),
|
|
779
|
+
listBlockingWorkspaceObligations(context)
|
|
780
|
+
]);
|
|
781
|
+
if (blockingQuestions.length > 0) {
|
|
782
|
+
const first = blockingQuestions[0];
|
|
783
|
+
const options = formatQuestionOptionValues(first).join("/");
|
|
784
|
+
throw new Error([
|
|
785
|
+
`LongTable is blocked by a required Researcher Checkpoint: ${first.id}`,
|
|
786
|
+
first.prompt.question,
|
|
787
|
+
`Options: ${options}`,
|
|
788
|
+
`Record an answer with: longtable decide --question ${first.id} --answer <value>`
|
|
789
|
+
].join("\n"));
|
|
790
|
+
}
|
|
791
|
+
if (blockingObligations.length > 0) {
|
|
792
|
+
const first = blockingObligations[0];
|
|
793
|
+
throw new Error([
|
|
794
|
+
`LongTable is blocked by a pending research obligation: ${first.id}`,
|
|
795
|
+
first.prompt,
|
|
796
|
+
first.reason,
|
|
797
|
+
...(first.questionId
|
|
798
|
+
? [`If a question was already issued, answer it with: longtable decide --question ${first.questionId} --answer <value>`]
|
|
799
|
+
: ["Resume the LongTable interview and answer the next researcher-facing checkpoint before proceeding."])
|
|
800
|
+
].join("\n"));
|
|
801
|
+
}
|
|
507
802
|
}
|
|
508
803
|
function questionTitleForCheckpoint(family, checkpointKey) {
|
|
509
804
|
if (checkpointKey === "knowledge_gap_probe") {
|
|
@@ -847,9 +1142,10 @@ export async function createWorkspaceQuestion(options) {
|
|
|
847
1142
|
}
|
|
848
1143
|
};
|
|
849
1144
|
const updated = appendQuestionRecords(state, [question]);
|
|
850
|
-
|
|
1145
|
+
const withObligation = ensureRequiredQuestionObligation(updated, question);
|
|
1146
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(withObligation, null, 2), "utf8");
|
|
851
1147
|
await syncCurrentWorkspaceView(options.context);
|
|
852
|
-
return { question, state:
|
|
1148
|
+
return { question, state: withObligation };
|
|
853
1149
|
}
|
|
854
1150
|
function updateInvocationWithDecision(invocation, questionId, decisionId) {
|
|
855
1151
|
if (!invocation.panelResult?.linkedQuestionRecordIds.includes(questionId)) {
|
|
@@ -981,7 +1277,8 @@ export async function answerWorkspaceQuestion(options) {
|
|
|
981
1277
|
questionLog: (state.questionLog ?? []).map((record) => record.id === question.id ? answeredQuestion : record),
|
|
982
1278
|
invocationLog: (state.invocationLog ?? []).map((record) => updateInvocationWithDecision(record, question.id, decision.id))
|
|
983
1279
|
};
|
|
984
|
-
const
|
|
1280
|
+
const withDecision = appendDecisionToResearchState(withQuestion, decision);
|
|
1281
|
+
const updated = resolveQuestionObligationByQuestionId(withDecision, question.id, decision.id);
|
|
985
1282
|
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
986
1283
|
await syncCurrentWorkspaceView(options.context);
|
|
987
1284
|
return {
|
|
@@ -990,6 +1287,105 @@ export async function answerWorkspaceQuestion(options) {
|
|
|
990
1287
|
state: updated
|
|
991
1288
|
};
|
|
992
1289
|
}
|
|
1290
|
+
export async function clearWorkspaceQuestion(options) {
|
|
1291
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
1292
|
+
const question = findPendingQuestionForClear(state, options.questionId);
|
|
1293
|
+
if (!question) {
|
|
1294
|
+
throw new Error(`No pending LongTable question found for ${options.questionId}.`);
|
|
1295
|
+
}
|
|
1296
|
+
const timestamp = nowIso();
|
|
1297
|
+
const clearedQuestion = {
|
|
1298
|
+
...question,
|
|
1299
|
+
updatedAt: timestamp,
|
|
1300
|
+
status: "cleared",
|
|
1301
|
+
clearedReason: options.reason.trim()
|
|
1302
|
+
};
|
|
1303
|
+
const withQuestion = {
|
|
1304
|
+
...state,
|
|
1305
|
+
questionLog: (state.questionLog ?? []).map((record) => record.id === question.id ? clearedQuestion : record)
|
|
1306
|
+
};
|
|
1307
|
+
const updated = resolveQuestionObligationByQuestionId(withQuestion, question.id, undefined, "cleared");
|
|
1308
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
1309
|
+
await syncCurrentWorkspaceView(options.context);
|
|
1310
|
+
return {
|
|
1311
|
+
question: clearedQuestion,
|
|
1312
|
+
state: updated
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
export async function repairWorkspaceStateConsistency(options) {
|
|
1316
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
1317
|
+
const repaired = [];
|
|
1318
|
+
const hooks = state.hooks ?? [];
|
|
1319
|
+
const hookMatchedByHandle = state.firstResearchShape?.confirmedAt
|
|
1320
|
+
? hooks.find((hook) => hook.kind === "longtable_interview" &&
|
|
1321
|
+
hook.firstResearchShape?.handle === state.firstResearchShape?.handle)
|
|
1322
|
+
: undefined;
|
|
1323
|
+
const confirmedShape = state.firstResearchShape?.confirmedAt
|
|
1324
|
+
? {
|
|
1325
|
+
...state.firstResearchShape,
|
|
1326
|
+
...(state.firstResearchShape.sourceHookId
|
|
1327
|
+
? {}
|
|
1328
|
+
: hookMatchedByHandle?.id
|
|
1329
|
+
? { sourceHookId: hookMatchedByHandle.id }
|
|
1330
|
+
: {})
|
|
1331
|
+
}
|
|
1332
|
+
: undefined;
|
|
1333
|
+
let updated = state;
|
|
1334
|
+
if (confirmedShape && confirmedShape.sourceHookId && !state.firstResearchShape?.sourceHookId) {
|
|
1335
|
+
repaired.push(`restored sourceHookId ${confirmedShape.sourceHookId} on confirmed first research shape`);
|
|
1336
|
+
updated = {
|
|
1337
|
+
...updated,
|
|
1338
|
+
firstResearchShape: confirmedShape,
|
|
1339
|
+
workingState: {
|
|
1340
|
+
...updated.workingState,
|
|
1341
|
+
firstResearchShape: confirmedShape
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
if (confirmedShape?.sourceHookId) {
|
|
1346
|
+
const hooks = (updated.hooks ?? []).map((hook) => {
|
|
1347
|
+
if (hook.id === confirmedShape.sourceHookId &&
|
|
1348
|
+
hook.kind === "longtable_interview" &&
|
|
1349
|
+
hook.status !== "confirmed") {
|
|
1350
|
+
repaired.push(`confirmed interview hook ${hook.id}`);
|
|
1351
|
+
return {
|
|
1352
|
+
...hook,
|
|
1353
|
+
status: "confirmed",
|
|
1354
|
+
updatedAt: nowIso(),
|
|
1355
|
+
firstResearchShape: confirmedShape
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
return hook;
|
|
1359
|
+
});
|
|
1360
|
+
updated = {
|
|
1361
|
+
...updated,
|
|
1362
|
+
hooks
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (confirmedShape?.sourceHookId) {
|
|
1366
|
+
updated = {
|
|
1367
|
+
...updated,
|
|
1368
|
+
questionObligations: (updated.questionObligations ?? []).map((obligation) => {
|
|
1369
|
+
if (obligation.kind === "first_research_shape_confirmation" &&
|
|
1370
|
+
obligation.status === "pending" &&
|
|
1371
|
+
obligation.sourceHookId === confirmedShape.sourceHookId) {
|
|
1372
|
+
repaired.push(`cleared first research shape obligation ${obligation.id}`);
|
|
1373
|
+
return {
|
|
1374
|
+
...obligation,
|
|
1375
|
+
status: "satisfied",
|
|
1376
|
+
updatedAt: nowIso()
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
return obligation;
|
|
1380
|
+
})
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
if (repaired.length > 0) {
|
|
1384
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
1385
|
+
await syncCurrentWorkspaceView(options.context);
|
|
1386
|
+
}
|
|
1387
|
+
return { state: updated, repaired };
|
|
1388
|
+
}
|
|
993
1389
|
export async function createOrUpdateProjectWorkspace(options) {
|
|
994
1390
|
const projectPath = resolve(options.projectPath);
|
|
995
1391
|
const metaDir = resolveMetaDir(projectPath);
|
|
@@ -1175,6 +1571,7 @@ export function renderProjectWorkspaceSummary(context) {
|
|
|
1175
1571
|
`Goal: ${context.session.currentGoal}`,
|
|
1176
1572
|
...(context.session.currentBlocker ? [`Blocker: ${context.session.currentBlocker}`] : []),
|
|
1177
1573
|
...(context.session.researchObject ? [`Working object: ${context.session.researchObject}`] : []),
|
|
1574
|
+
...(context.session.firstResearchShape ? [`First Research Shape: ${context.session.firstResearchShape.handle}`] : []),
|
|
1178
1575
|
...(context.session.startInterview ? [`Start interview: ${context.session.startInterview.summary}`] : []),
|
|
1179
1576
|
"└───────────────────────────────────────────────────────┘",
|
|
1180
1577
|
"",
|
package/dist/prompt-aliases.js
CHANGED
|
@@ -29,18 +29,18 @@ function promptSpec() {
|
|
|
29
29
|
argumentHint: "[project context or current uncertainty]",
|
|
30
30
|
body: [
|
|
31
31
|
"You are LongTable setup guidance inside Codex.",
|
|
32
|
-
"Treat `longtable init` as deprecated. Prefer `longtable setup --provider codex` for runtime permissions, then
|
|
32
|
+
"Treat `longtable init` as deprecated. Prefer `longtable setup --provider codex` for runtime permissions, then `$longtable-interview` inside Codex for the project interview.",
|
|
33
33
|
"Ask exactly one setup question at a time.",
|
|
34
34
|
"Use numbered choices and include a concise Why / What you get / Tradeoff for each option.",
|
|
35
35
|
"Do not move to the next question until the researcher answers the current one.",
|
|
36
|
-
"Runtime setup covers only: provider, install scope, runtime surfaces, intervention strength, and whether to
|
|
36
|
+
"Runtime setup covers only: provider, install scope, runtime surfaces, intervention strength, and whether to show interview launch steps.",
|
|
37
37
|
"Do not ask for field, career stage, experience level, authorship signal, weakest domain, or panel preference during runtime setup.",
|
|
38
|
-
"Project
|
|
38
|
+
"Project interview covers: the first research handle, early uncertainty, first inspectable material, and final structured confirmation.",
|
|
39
39
|
"After collecting runtime answers, summarize the proposed setup and output the exact `longtable setup --provider codex ...` command.",
|
|
40
|
-
"If the researcher is ready to create a project workspace,
|
|
40
|
+
"If the researcher is ready to create a project workspace, tell them to open Codex in the research folder and invoke `$longtable-interview`.",
|
|
41
41
|
"If the researcher asks you to stay inside Codex, keep the conversation in numbered form and do not prematurely close.",
|
|
42
42
|
"Frame setup as permission and intervention calibration, not a researcher profile interview.",
|
|
43
|
-
"Do not pretend setup is the full project-start interview. The project-start interview happens in
|
|
43
|
+
"Do not pretend setup is the full project-start interview. The project-start interview happens in `$longtable-interview`.",
|
|
44
44
|
"Treat any slash-command arguments as context for why setup is being done now."
|
|
45
45
|
]
|
|
46
46
|
},
|