@longtable/cli 0.1.8 → 0.1.10
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 +121 -76
- package/dist/cli.js +711 -66
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/panel.d.ts +40 -0
- package/dist/panel.js +268 -0
- package/dist/persona-router.d.ts +1 -1
- package/dist/persona-router.js +6 -6
- package/dist/personas.d.ts +5 -0
- package/dist/personas.js +44 -8
- package/dist/project-session.d.ts +94 -2
- package/dist/project-session.js +593 -150
- package/dist/prompt-aliases.js +34 -34
- package/package.json +13 -7
package/dist/project-session.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { createEmptyResearchState } from "@
|
|
5
|
+
import { appendDecisionRecord as appendDecisionToResearchState, appendInvocationRecord as appendInvocationToResearchState, appendQuestionRecords, createEmptyResearchState } from "@longtable/memory";
|
|
6
|
+
import { classifyCheckpointTrigger } from "@longtable/checkpoints";
|
|
7
|
+
const CURRENT_FILE_NAME = "CURRENT.md";
|
|
8
|
+
const LEGACY_ROOT_FILES = ["LONGTABLE.md", "START-HERE.md", "NEXT-STEPS.md", "SESSION-SNAPSHOT.md"];
|
|
5
9
|
function nowIso() {
|
|
6
10
|
return new Date().toISOString();
|
|
7
11
|
}
|
|
@@ -16,160 +20,303 @@ function slugify(input) {
|
|
|
16
20
|
function resolveMetaDir(projectPath) {
|
|
17
21
|
return join(projectPath, ".longtable");
|
|
18
22
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
"# Long Table Workspace",
|
|
22
|
-
"",
|
|
23
|
-
"This directory is a Long Table research workspace.",
|
|
24
|
-
"",
|
|
25
|
-
"## Current project",
|
|
26
|
-
`- Project: ${project.projectName}`,
|
|
27
|
-
`- Goal right now: ${session.currentGoal}`,
|
|
28
|
-
...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
|
|
29
|
-
`- Disagreement visibility: ${session.disagreementPreference}`,
|
|
30
|
-
`- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
31
|
-
"",
|
|
32
|
-
"## How Codex should behave here",
|
|
33
|
-
"- Treat this as a researcher-facing Long Table session, not a generic coding task.",
|
|
34
|
-
"- Ask clarifying or tension questions before closing too early.",
|
|
35
|
-
"- If you foreground specific roles, disclose them with `Long Table consulted: ...`.",
|
|
36
|
-
"- Keep one accountable synthesis, but keep disagreement visible by default when it matters.",
|
|
37
|
-
"- For factual, current, or external claims, attach source links or local file references whenever possible.",
|
|
38
|
-
"- If you cannot source a statement, label it as an inference or estimate.",
|
|
39
|
-
"- Do not expose internal tool logs or process commentary in researcher-facing answers.",
|
|
40
|
-
"",
|
|
41
|
-
"## Session files",
|
|
42
|
-
"- `.longtable/project.json` contains project-level metadata.",
|
|
43
|
-
"- `.longtable/current-session.json` contains the current session goal and blocker."
|
|
44
|
-
];
|
|
45
|
-
return lines.join("\n");
|
|
23
|
+
function resolveStateFilePath(projectPath) {
|
|
24
|
+
return join(resolveMetaDir(projectPath), "state.json");
|
|
46
25
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`- Current goal: ${session.currentGoal}`,
|
|
77
|
-
...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
|
|
78
|
-
`- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
79
|
-
`- Disagreement visibility: ${session.disagreementPreference}`,
|
|
80
|
-
"",
|
|
81
|
-
"## Files",
|
|
82
|
-
"- `AGENTS.md` tells Codex how to behave in this workspace.",
|
|
83
|
-
"- `.longtable/current-session.json` stores the current session goal and blocker.",
|
|
84
|
-
"- `.longtable/project.json` stores the project-level record.",
|
|
85
|
-
"- `NEXT-STEPS.md` stores the immediate next actions.",
|
|
86
|
-
"- `SESSION-SNAPSHOT.md` stores the short resume snapshot."
|
|
87
|
-
];
|
|
88
|
-
return lines.join("\n");
|
|
26
|
+
function resolveCurrentFilePath(projectPath) {
|
|
27
|
+
return join(projectPath, CURRENT_FILE_NAME);
|
|
28
|
+
}
|
|
29
|
+
function normalizeLocale(locale) {
|
|
30
|
+
if ((locale ?? "").toLowerCase().startsWith("ko")) {
|
|
31
|
+
return "ko";
|
|
32
|
+
}
|
|
33
|
+
return "en";
|
|
34
|
+
}
|
|
35
|
+
function resolveUserLocale() {
|
|
36
|
+
const envLocale = process.env.LC_ALL ??
|
|
37
|
+
process.env.LC_MESSAGES ??
|
|
38
|
+
process.env.LANG ??
|
|
39
|
+
"";
|
|
40
|
+
if (envLocale && !["c", "c.utf-8", "posix"].includes(envLocale.toLowerCase())) {
|
|
41
|
+
return normalizeLocale(envLocale);
|
|
42
|
+
}
|
|
43
|
+
if (process.platform === "darwin") {
|
|
44
|
+
try {
|
|
45
|
+
const appleLocale = execSync("defaults read -g AppleLocale", { encoding: "utf8" }).trim();
|
|
46
|
+
if (appleLocale) {
|
|
47
|
+
return normalizeLocale(appleLocale);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore and fall back to Intl below.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale);
|
|
89
55
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
56
|
+
function buildFirstQuestion(session) {
|
|
57
|
+
return session.currentBlocker
|
|
92
58
|
? `What would reduce the uncertainty around "${session.currentBlocker}" first?`
|
|
93
59
|
: `What is the first concrete question that would move "${session.currentGoal}" forward?`;
|
|
60
|
+
}
|
|
61
|
+
function buildOpenQuestions(session) {
|
|
62
|
+
const firstQuestion = buildFirstQuestion(session);
|
|
63
|
+
return session.currentBlocker
|
|
64
|
+
? [
|
|
65
|
+
firstQuestion,
|
|
66
|
+
`What evidence would let you decide whether "${session.currentBlocker}" is a knowledge gap, a coding rule gap, or a data gap?`
|
|
67
|
+
]
|
|
68
|
+
: [
|
|
69
|
+
firstQuestion,
|
|
70
|
+
`What would count as a good first outcome for "${session.currentGoal}" in this session?`
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
function buildNextAction(session) {
|
|
74
|
+
return session.currentBlocker
|
|
75
|
+
? `Open with the blocker, then ask LongTable to surface the first high-leverage uncertainty around "${session.currentBlocker}".`
|
|
76
|
+
: "Open with your current goal in one sentence, then ask LongTable for the first concrete research move.";
|
|
77
|
+
}
|
|
78
|
+
function buildResumeHint(session) {
|
|
79
|
+
return session.currentBlocker
|
|
80
|
+
? `I want to continue ${session.currentGoal}. The unresolved blocker is ${session.currentBlocker}.`
|
|
81
|
+
: `I want to continue ${session.currentGoal}.`;
|
|
82
|
+
}
|
|
83
|
+
function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = []) {
|
|
84
|
+
const locale = normalizeLocale(session.locale ?? project.locale);
|
|
85
|
+
const openQuestions = session.openQuestions && session.openQuestions.length > 0
|
|
86
|
+
? session.openQuestions
|
|
87
|
+
: buildOpenQuestions(session);
|
|
88
|
+
const nextAction = session.nextAction ?? buildNextAction(session);
|
|
89
|
+
const resumeHint = session.resumeHint ?? buildResumeHint(session);
|
|
90
|
+
const suggestedPrompt = `lt explore: ${openQuestions[0]}`;
|
|
91
|
+
if (locale === "ko") {
|
|
92
|
+
return [
|
|
93
|
+
"# CURRENT",
|
|
94
|
+
"",
|
|
95
|
+
`Project: ${project.projectName}`,
|
|
96
|
+
"",
|
|
97
|
+
"이 파일은 `.longtable/current-session.json`과 `.longtable/state.json`에서 재생성되는 현재 작업 뷰입니다.",
|
|
98
|
+
"",
|
|
99
|
+
"## 지금 초점",
|
|
100
|
+
`- 현재 목표: ${session.currentGoal}`,
|
|
101
|
+
...(session.currentBlocker ? [`- 현재 blocker: ${session.currentBlocker}`] : []),
|
|
102
|
+
`- 다음 액션: ${nextAction}`,
|
|
103
|
+
`- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
104
|
+
`- disagreement: ${session.disagreementPreference}`,
|
|
105
|
+
"",
|
|
106
|
+
"## 열린 질문",
|
|
107
|
+
...openQuestions.map((question) => `- ${question}`),
|
|
108
|
+
...(recentInvocations.length > 0
|
|
109
|
+
? [
|
|
110
|
+
"",
|
|
111
|
+
"## 최근 LongTable 호출",
|
|
112
|
+
...recentInvocations.map((record) => {
|
|
113
|
+
const roles = record.intent.roles.length > 0 ? record.intent.roles.join(", ") : "auto";
|
|
114
|
+
return `- ${record.intent.kind}/${record.intent.mode} via ${record.surface}: ${roles}`;
|
|
115
|
+
})
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
118
|
+
...(pendingQuestions.length > 0
|
|
119
|
+
? [
|
|
120
|
+
"",
|
|
121
|
+
"## 대기 중인 결정 질문",
|
|
122
|
+
...pendingQuestions.map((record) => {
|
|
123
|
+
const options = formatQuestionOptionValues(record).join("/");
|
|
124
|
+
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
125
|
+
}),
|
|
126
|
+
"- 답변 기록: `longtable decide --question <id> --answer <value>`"
|
|
127
|
+
]
|
|
128
|
+
: []),
|
|
129
|
+
"",
|
|
130
|
+
"## 다시 시작 문장",
|
|
131
|
+
`- "${resumeHint}"`,
|
|
132
|
+
"",
|
|
133
|
+
"## 빠른 시작",
|
|
134
|
+
"- 이 디렉토리에서 `codex`를 엽니다.",
|
|
135
|
+
`- 첫 메시지는 보통 \`${suggestedPrompt}\` 정도면 충분합니다.`,
|
|
136
|
+
"",
|
|
137
|
+
"## 증거 규칙",
|
|
138
|
+
"- 외부 사실이나 현재 정보는 source를 붙이거나 inference로 낮춥니다."
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
94
141
|
return [
|
|
95
|
-
"#
|
|
142
|
+
"# CURRENT",
|
|
96
143
|
"",
|
|
97
144
|
`Project: ${project.projectName}`,
|
|
98
145
|
"",
|
|
99
|
-
"
|
|
100
|
-
"1. Open Codex in this directory.",
|
|
101
|
-
"2. State your current goal in one sentence.",
|
|
102
|
-
"3. Ask Long Table to surface disagreement before closing on a plan.",
|
|
103
|
-
"",
|
|
104
|
-
"## Suggested openings",
|
|
105
|
-
`- ${session.currentBlocker ? `"I want to work on ${session.currentGoal}. My current blocker is ${session.currentBlocker}."` : `"I want to work on ${session.currentGoal}."`}`,
|
|
106
|
-
`- "lt explore: ${firstQuestion}"`,
|
|
107
|
-
`- "lt panel: Show me the strongest disagreement before I commit."`,
|
|
146
|
+
"This file is regenerated from `.longtable/current-session.json` and `.longtable/state.json`.",
|
|
108
147
|
"",
|
|
109
|
-
"##
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
"## Focus Now",
|
|
149
|
+
`- Current goal: ${session.currentGoal}`,
|
|
150
|
+
...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
|
|
151
|
+
`- Next action: ${nextAction}`,
|
|
152
|
+
`- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
153
|
+
`- Disagreement: ${session.disagreementPreference}`,
|
|
112
154
|
"",
|
|
113
|
-
"##
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
155
|
+
"## Open Questions",
|
|
156
|
+
...openQuestions.map((question) => `- ${question}`),
|
|
157
|
+
...(recentInvocations.length > 0
|
|
158
|
+
? [
|
|
159
|
+
"",
|
|
160
|
+
"## Recent LongTable Invocations",
|
|
161
|
+
...recentInvocations.map((record) => {
|
|
162
|
+
const roles = record.intent.roles.length > 0 ? record.intent.roles.join(", ") : "auto";
|
|
163
|
+
return `- ${record.intent.kind}/${record.intent.mode} via ${record.surface}: ${roles}`;
|
|
164
|
+
})
|
|
165
|
+
]
|
|
166
|
+
: []),
|
|
167
|
+
...(pendingQuestions.length > 0
|
|
168
|
+
? [
|
|
169
|
+
"",
|
|
170
|
+
"## Pending Decision Questions",
|
|
171
|
+
...pendingQuestions.map((record) => {
|
|
172
|
+
const options = formatQuestionOptionValues(record).join("/");
|
|
173
|
+
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
174
|
+
}),
|
|
175
|
+
"- Record an answer: `longtable decide --question <id> --answer <value>`"
|
|
176
|
+
]
|
|
177
|
+
: []),
|
|
120
178
|
"",
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
...(session.currentBlocker ? [`Current blocker: ${session.currentBlocker}`] : []),
|
|
124
|
-
`Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
125
|
-
`Disagreement visibility: ${session.disagreementPreference}`,
|
|
179
|
+
"## Restart Prompt",
|
|
180
|
+
`- "${resumeHint}"`,
|
|
126
181
|
"",
|
|
127
|
-
"##
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
: `"I want to continue ${session.currentGoal}."`,
|
|
182
|
+
"## Quick Start",
|
|
183
|
+
"- Open `codex` in this directory.",
|
|
184
|
+
`- A good first message is usually \`${suggestedPrompt}\`.`,
|
|
131
185
|
"",
|
|
132
|
-
"##
|
|
133
|
-
"-
|
|
134
|
-
"- If factual or external claims matter, Long Table should cite sources or mark them as inference."
|
|
186
|
+
"## Evidence Rule",
|
|
187
|
+
"- External or current claims should carry a source link or be labeled as inference."
|
|
135
188
|
].join("\n");
|
|
136
189
|
}
|
|
190
|
+
async function loadResearchState(stateFilePath) {
|
|
191
|
+
if (!existsSync(stateFilePath)) {
|
|
192
|
+
return createEmptyResearchState();
|
|
193
|
+
}
|
|
194
|
+
const parsed = JSON.parse(await readFile(stateFilePath, "utf8"));
|
|
195
|
+
return {
|
|
196
|
+
explicitState: parsed.explicitState ?? {},
|
|
197
|
+
workingState: parsed.workingState ?? {},
|
|
198
|
+
inferredHypotheses: parsed.inferredHypotheses ?? [],
|
|
199
|
+
openTensions: parsed.openTensions ?? [],
|
|
200
|
+
decisionLog: parsed.decisionLog ?? [],
|
|
201
|
+
invocationLog: parsed.invocationLog ?? [],
|
|
202
|
+
questionLog: parsed.questionLog ?? [],
|
|
203
|
+
artifactRecords: parsed.artifactRecords ?? [],
|
|
204
|
+
narrativeTraces: parsed.narrativeTraces ?? [],
|
|
205
|
+
...(parsed.studyContract ? { studyContract: parsed.studyContract } : {})
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function recentInvocationRecords(state, limit = 3) {
|
|
209
|
+
return (state.invocationLog ?? []).slice(-limit).reverse();
|
|
210
|
+
}
|
|
211
|
+
function recentPendingQuestions(state, limit = 3) {
|
|
212
|
+
return (state.questionLog ?? [])
|
|
213
|
+
.filter((record) => record.status === "pending")
|
|
214
|
+
.slice(-limit)
|
|
215
|
+
.reverse();
|
|
216
|
+
}
|
|
217
|
+
function formatQuestionOptionValues(record) {
|
|
218
|
+
const values = record.prompt.options.map((option) => option.value);
|
|
219
|
+
if (record.prompt.allowOther) {
|
|
220
|
+
values.push(record.prompt.otherLabel ? `other:${record.prompt.otherLabel}` : "other");
|
|
221
|
+
}
|
|
222
|
+
return values;
|
|
223
|
+
}
|
|
224
|
+
function summarizeWorkspaceInspection(context, state) {
|
|
225
|
+
const questions = state.questionLog ?? [];
|
|
226
|
+
const pendingQuestions = questions.filter((record) => record.status === "pending");
|
|
227
|
+
const answeredQuestions = questions.filter((record) => record.status === "answered");
|
|
228
|
+
return {
|
|
229
|
+
found: true,
|
|
230
|
+
rootPath: context.project.projectPath,
|
|
231
|
+
project: {
|
|
232
|
+
name: context.project.projectName,
|
|
233
|
+
path: context.project.projectPath,
|
|
234
|
+
field: context.project.globalSetupSummary.field,
|
|
235
|
+
careerStage: context.project.globalSetupSummary.careerStage,
|
|
236
|
+
checkpointIntensity: context.project.globalSetupSummary.checkpointIntensity
|
|
237
|
+
},
|
|
238
|
+
session: {
|
|
239
|
+
currentGoal: context.session.currentGoal,
|
|
240
|
+
...(context.session.currentBlocker ? { currentBlocker: context.session.currentBlocker } : {}),
|
|
241
|
+
requestedPerspectives: context.session.requestedPerspectives,
|
|
242
|
+
disagreementPreference: context.session.disagreementPreference
|
|
243
|
+
},
|
|
244
|
+
files: {
|
|
245
|
+
project: context.projectFilePath,
|
|
246
|
+
session: context.sessionFilePath,
|
|
247
|
+
state: context.stateFilePath,
|
|
248
|
+
current: context.currentFilePath
|
|
249
|
+
},
|
|
250
|
+
counts: {
|
|
251
|
+
invocations: (state.invocationLog ?? []).length,
|
|
252
|
+
questions: questions.length,
|
|
253
|
+
pendingQuestions: pendingQuestions.length,
|
|
254
|
+
answeredQuestions: answeredQuestions.length,
|
|
255
|
+
decisions: (state.decisionLog ?? []).length
|
|
256
|
+
},
|
|
257
|
+
recentInvocations: recentInvocationRecords(state, 5).map((record) => ({
|
|
258
|
+
id: record.id,
|
|
259
|
+
kind: record.intent.kind,
|
|
260
|
+
mode: record.intent.mode,
|
|
261
|
+
surface: record.surface,
|
|
262
|
+
status: record.status,
|
|
263
|
+
roles: record.intent.roles,
|
|
264
|
+
linkedQuestions: record.panelResult?.linkedQuestionRecordIds.length ?? 0,
|
|
265
|
+
linkedDecisions: record.panelResult?.linkedDecisionRecordIds.length ?? 0
|
|
266
|
+
})),
|
|
267
|
+
pendingQuestions: pendingQuestions.slice(-5).reverse().map((record) => ({
|
|
268
|
+
id: record.id,
|
|
269
|
+
title: record.prompt.title,
|
|
270
|
+
question: record.prompt.question,
|
|
271
|
+
options: formatQuestionOptionValues(record),
|
|
272
|
+
required: record.prompt.required
|
|
273
|
+
})),
|
|
274
|
+
recentDecisions: (state.decisionLog ?? []).slice(-5).reverse().map((record) => ({
|
|
275
|
+
id: record.id,
|
|
276
|
+
checkpointKey: record.checkpointKey,
|
|
277
|
+
summary: record.summary,
|
|
278
|
+
...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
|
|
279
|
+
timestamp: record.timestamp
|
|
280
|
+
}))
|
|
281
|
+
};
|
|
282
|
+
}
|
|
137
283
|
function buildProjectAgentsMd(project, session) {
|
|
138
284
|
return [
|
|
139
285
|
"# AGENTS.md",
|
|
140
286
|
"",
|
|
141
|
-
"This directory is a
|
|
142
|
-
"",
|
|
143
|
-
"## Purpose",
|
|
144
|
-
`- Project name: ${project.projectName}`,
|
|
145
|
-
`- Current goal: ${session.currentGoal}`,
|
|
146
|
-
...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
|
|
147
|
-
`- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
148
|
-
`- Disagreement visibility: ${session.disagreementPreference}`,
|
|
287
|
+
"This directory is a LongTable research workspace.",
|
|
149
288
|
"",
|
|
150
|
-
"##
|
|
289
|
+
"## Runtime Contract",
|
|
151
290
|
"- Treat researcher interaction as the primary task.",
|
|
152
|
-
"-
|
|
291
|
+
"- Read `.longtable/current-session.json` before giving substantial guidance.",
|
|
292
|
+
"- Use `.longtable/project.json` as stable project context.",
|
|
293
|
+
"- Use `.longtable/state.json` as layered working memory.",
|
|
294
|
+
"- Prefer `currentGoal`, `currentBlocker`, `nextAction`, and `openQuestions` over generic assumptions.",
|
|
295
|
+
"- Treat `AGENTS.md` as runtime guidance, not as the researcher-facing resume artifact.",
|
|
296
|
+
"",
|
|
297
|
+
"## Invocation Rules",
|
|
298
|
+
"- If the user message starts with `lt `, `longtable `, `long table `, or `롱테이블 ` followed by a directive and `:`, treat it as an explicit LongTable invocation.",
|
|
153
299
|
"- Supported explicit directives are: explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
|
|
154
|
-
"- For explicit
|
|
155
|
-
"- For general research requests in this workspace, prefer
|
|
300
|
+
"- For explicit LongTable invocations, do not begin by scanning the workspace. Use the current session files first and answer as LongTable immediately.",
|
|
301
|
+
"- For general research requests in this workspace, prefer LongTable behavior before generic coding behavior.",
|
|
302
|
+
"",
|
|
303
|
+
"## Research Behavior",
|
|
156
304
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
157
|
-
"- If you foreground role perspectives, disclose them with `
|
|
305
|
+
"- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
|
|
158
306
|
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
159
|
-
"- For factual, current, or external claims, provide source links or file references when possible.",
|
|
160
|
-
"- If a statement cannot be sourced, label it as an inference or estimate instead of presenting it as a fact.",
|
|
161
307
|
...(session.disagreementPreference === "always_visible"
|
|
162
|
-
? ["-
|
|
308
|
+
? ["- Panel disagreement should be visible by default rather than hidden behind a single synthesis."]
|
|
163
309
|
: []),
|
|
310
|
+
"- For factual, current, or external claims, provide source links or file references when possible.",
|
|
311
|
+
"- If a statement cannot be sourced, label it as an inference or estimate instead of presenting it as a fact.",
|
|
164
312
|
"- Do not expose internal tool logs, file-search traces, or process commentary in the researcher-facing answer.",
|
|
165
313
|
"",
|
|
166
|
-
"## Session memory",
|
|
167
|
-
"- Read `.longtable/current-session.json` before giving substantial guidance.",
|
|
168
|
-
"- Use `.longtable/project.json` as the project-level context.",
|
|
169
|
-
"- Prefer the current goal and blocker over generic assumptions.",
|
|
170
|
-
"- Do not recursively search unrelated files unless the researcher explicitly asks for file analysis or code changes.",
|
|
171
|
-
"",
|
|
172
314
|
"## Scope",
|
|
315
|
+
`- Project: ${project.projectName}`,
|
|
316
|
+
`- Current goal: ${session.currentGoal}`,
|
|
317
|
+
...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
|
|
318
|
+
`- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
319
|
+
`- Disagreement visibility: ${session.disagreementPreference}`,
|
|
173
320
|
"- These instructions apply to this directory and its children."
|
|
174
321
|
].join("\n");
|
|
175
322
|
}
|
|
@@ -180,12 +327,18 @@ function buildStateSeed(project, session, setup) {
|
|
|
180
327
|
careerStage: setup.profileSeed.careerStage,
|
|
181
328
|
experienceLevel: setup.profileSeed.experienceLevel,
|
|
182
329
|
projectName: project.projectName,
|
|
183
|
-
currentGoal: session.currentGoal,
|
|
184
330
|
disagreementPreference: session.disagreementPreference,
|
|
185
331
|
requestedPerspectives: session.requestedPerspectives
|
|
186
332
|
};
|
|
333
|
+
state.workingState = {
|
|
334
|
+
currentGoal: session.currentGoal,
|
|
335
|
+
...(session.currentBlocker ? { currentBlocker: session.currentBlocker } : {}),
|
|
336
|
+
...(session.nextAction ? { nextAction: session.nextAction } : {}),
|
|
337
|
+
openQuestions: session.openQuestions ?? [],
|
|
338
|
+
activeModes: session.activeModes ?? [],
|
|
339
|
+
...(session.resumeHint ? { resumeHint: session.resumeHint } : {})
|
|
340
|
+
};
|
|
187
341
|
if (session.currentBlocker) {
|
|
188
|
-
state.explicitState.currentBlocker = session.currentBlocker;
|
|
189
342
|
state.openTensions.push(session.currentBlocker);
|
|
190
343
|
}
|
|
191
344
|
if (setup.profileSeed.humanAuthorshipSignal) {
|
|
@@ -213,24 +366,259 @@ function buildStateSeed(project, session, setup) {
|
|
|
213
366
|
}
|
|
214
367
|
return JSON.stringify(state, null, 2);
|
|
215
368
|
}
|
|
369
|
+
async function removeLegacyRootFiles(projectPath) {
|
|
370
|
+
await Promise.all(LEGACY_ROOT_FILES.map((file) => rm(join(projectPath, file), { force: true })));
|
|
371
|
+
}
|
|
372
|
+
export async function syncCurrentWorkspaceView(context) {
|
|
373
|
+
const state = await loadResearchState(context.stateFilePath);
|
|
374
|
+
const body = buildCurrentGuide(context.project, context.session, recentInvocationRecords(state), recentPendingQuestions(state));
|
|
375
|
+
await writeFile(context.currentFilePath, body, "utf8");
|
|
376
|
+
return context.currentFilePath;
|
|
377
|
+
}
|
|
378
|
+
export async function appendInvocationRecordToWorkspace(context, invocation, questions = []) {
|
|
379
|
+
const state = await loadResearchState(context.stateFilePath);
|
|
380
|
+
const withInvocation = appendInvocationToResearchState(state, invocation);
|
|
381
|
+
const updated = questions.length > 0
|
|
382
|
+
? appendQuestionRecords(withInvocation, questions)
|
|
383
|
+
: withInvocation;
|
|
384
|
+
await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
385
|
+
await syncCurrentWorkspaceView(context);
|
|
386
|
+
return updated;
|
|
387
|
+
}
|
|
388
|
+
function createId(prefix) {
|
|
389
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
390
|
+
}
|
|
391
|
+
function findQuestionForDecision(state, questionId) {
|
|
392
|
+
const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
|
|
393
|
+
if (questionId) {
|
|
394
|
+
return pending.find((record) => record.id === questionId) ?? null;
|
|
395
|
+
}
|
|
396
|
+
return pending.at(-1) ?? null;
|
|
397
|
+
}
|
|
398
|
+
function pendingRequiredQuestions(state) {
|
|
399
|
+
return (state.questionLog ?? []).filter((record) => record.status === "pending" && record.prompt.required);
|
|
400
|
+
}
|
|
401
|
+
export async function listBlockingWorkspaceQuestions(context) {
|
|
402
|
+
const state = await loadResearchState(context.stateFilePath);
|
|
403
|
+
return pendingRequiredQuestions(state);
|
|
404
|
+
}
|
|
405
|
+
export async function assertWorkspaceNotBlocked(context) {
|
|
406
|
+
const blocking = await listBlockingWorkspaceQuestions(context);
|
|
407
|
+
if (blocking.length === 0) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const first = blocking[0];
|
|
411
|
+
const options = formatQuestionOptionValues(first).join("/");
|
|
412
|
+
throw new Error([
|
|
413
|
+
`LongTable is blocked by a required Researcher Checkpoint: ${first.id}`,
|
|
414
|
+
first.prompt.question,
|
|
415
|
+
`Options: ${options}`,
|
|
416
|
+
`Record an answer with: longtable decide --question ${first.id} --answer <value>`
|
|
417
|
+
].join("\n"));
|
|
418
|
+
}
|
|
419
|
+
function questionTitleForCheckpoint(family) {
|
|
420
|
+
switch (family) {
|
|
421
|
+
case "meta_decision":
|
|
422
|
+
return "Meta-decision checkpoint";
|
|
423
|
+
case "submission":
|
|
424
|
+
return "Submission checkpoint";
|
|
425
|
+
case "commitment":
|
|
426
|
+
return "Research commitment checkpoint";
|
|
427
|
+
case "evidence":
|
|
428
|
+
return "Evidence checkpoint";
|
|
429
|
+
case "authorship":
|
|
430
|
+
return "Authorship checkpoint";
|
|
431
|
+
case "review":
|
|
432
|
+
return "Review checkpoint";
|
|
433
|
+
case "exploration":
|
|
434
|
+
return "Exploration checkpoint";
|
|
435
|
+
default:
|
|
436
|
+
return "Researcher Checkpoint";
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function questionTextForCheckpoint(family, prompt) {
|
|
440
|
+
switch (family) {
|
|
441
|
+
case "meta_decision":
|
|
442
|
+
return "What should LongTable do before treating this platform decision as settled?";
|
|
443
|
+
case "submission":
|
|
444
|
+
return "What must happen before this work can move toward external release or submission?";
|
|
445
|
+
case "commitment":
|
|
446
|
+
return "What should LongTable treat as the human research commitment here?";
|
|
447
|
+
case "evidence":
|
|
448
|
+
return "How should LongTable handle the evidence risk before using this claim?";
|
|
449
|
+
case "authorship":
|
|
450
|
+
return "What should LongTable preserve before changing the researcher's voice or authorship trace?";
|
|
451
|
+
case "exploration":
|
|
452
|
+
return "What ambiguity should LongTable keep open before recommending a direction?";
|
|
453
|
+
default:
|
|
454
|
+
return `What should LongTable decide before proceeding with: ${prompt}`;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function optionsForCheckpointFamily(family) {
|
|
458
|
+
if (family === "evidence") {
|
|
459
|
+
return [
|
|
460
|
+
{ value: "verify", label: "Verify evidence first", description: "Check whether the source supports the specific claim." },
|
|
461
|
+
{ value: "limit", label: "Limit the claim", description: "Keep the point but narrow it to what the evidence can support." },
|
|
462
|
+
{ value: "rewrite", label: "Rewrite without the claim", description: "Avoid relying on the uncertain citation or claim." },
|
|
463
|
+
{ value: "defer", label: "Keep evidence risk open", description: "Do not settle the claim yet." }
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
if (family === "meta_decision") {
|
|
467
|
+
return [
|
|
468
|
+
{ value: "revise", label: "Revise the platform decision", description: "Change the term, policy, or README positioning before treating it as settled." },
|
|
469
|
+
{ value: "evidence", label: "Gather implementation evidence first", description: "Inspect behavior or docs before committing the platform decision." },
|
|
470
|
+
{ value: "proceed", label: "Proceed with current decision", description: "Accept the current platform framing and continue." },
|
|
471
|
+
{ value: "defer", label: "Keep the decision open", description: "Do not make this platform language authoritative yet." }
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
if (family === "submission") {
|
|
475
|
+
return [
|
|
476
|
+
{ value: "review", label: "Review risk first", description: "Check claims, ethics, venue fit, or study contract before external release." },
|
|
477
|
+
{ value: "evidence", label: "Verify evidence first", description: "Confirm source support before submission or public sharing." },
|
|
478
|
+
{ value: "proceed", label: "Proceed toward submission", description: "Accept the remaining risk and continue." },
|
|
479
|
+
{ value: "defer", label: "Do not submit yet", description: "Keep the submission decision open." }
|
|
480
|
+
];
|
|
481
|
+
}
|
|
482
|
+
return [
|
|
483
|
+
{ value: "revise", label: "Revise before proceeding", description: "Change the framing, design, or draft before treating this as settled." },
|
|
484
|
+
{ value: "evidence", label: "Gather or verify evidence first", description: "Do not proceed until the relevant evidence is checked." },
|
|
485
|
+
{ value: "proceed", label: "Proceed with current direction", description: "Accept the risk profile and continue." },
|
|
486
|
+
{ value: "defer", label: "Keep this open", description: "Do not commit yet; keep the issue visible as an open tension." }
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
export async function createWorkspaceQuestion(options) {
|
|
490
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
491
|
+
const trigger = classifyCheckpointTrigger(options.prompt, {
|
|
492
|
+
unresolvedTensions: state.openTensions ?? [],
|
|
493
|
+
studyContract: state.studyContract
|
|
494
|
+
});
|
|
495
|
+
const createdAt = nowIso();
|
|
496
|
+
const question = {
|
|
497
|
+
id: createId("question_record"),
|
|
498
|
+
createdAt,
|
|
499
|
+
updatedAt: createdAt,
|
|
500
|
+
status: "pending",
|
|
501
|
+
prompt: {
|
|
502
|
+
id: createId("question_prompt"),
|
|
503
|
+
checkpointKey: trigger.signal.checkpointKey,
|
|
504
|
+
title: options.title ?? questionTitleForCheckpoint(trigger.family),
|
|
505
|
+
question: options.question ?? questionTextForCheckpoint(trigger.family, options.prompt),
|
|
506
|
+
type: "single_choice",
|
|
507
|
+
options: optionsForCheckpointFamily(trigger.family),
|
|
508
|
+
allowOther: true,
|
|
509
|
+
otherLabel: "Other decision",
|
|
510
|
+
required: options.required ?? trigger.requiresQuestionBeforeClosure,
|
|
511
|
+
source: "checkpoint",
|
|
512
|
+
rationale: [
|
|
513
|
+
...trigger.rationale,
|
|
514
|
+
`Trigger family: ${trigger.family}.`,
|
|
515
|
+
`Trigger confidence: ${trigger.confidence}.`,
|
|
516
|
+
`Original prompt: ${options.prompt}`
|
|
517
|
+
],
|
|
518
|
+
preferredSurfaces: options.provider === "claude"
|
|
519
|
+
? ["native_structured", "numbered"]
|
|
520
|
+
: ["numbered", "native_structured"]
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
const updated = appendQuestionRecords(state, [question]);
|
|
524
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
525
|
+
await syncCurrentWorkspaceView(options.context);
|
|
526
|
+
return { question, state: updated };
|
|
527
|
+
}
|
|
528
|
+
function updateInvocationWithDecision(invocation, questionId, decisionId) {
|
|
529
|
+
if (!invocation.panelResult?.linkedQuestionRecordIds.includes(questionId)) {
|
|
530
|
+
return invocation;
|
|
531
|
+
}
|
|
532
|
+
const linkedDecisionRecordIds = invocation.panelResult.linkedDecisionRecordIds.includes(decisionId)
|
|
533
|
+
? invocation.panelResult.linkedDecisionRecordIds
|
|
534
|
+
: [...invocation.panelResult.linkedDecisionRecordIds, decisionId];
|
|
535
|
+
return {
|
|
536
|
+
...invocation,
|
|
537
|
+
updatedAt: nowIso(),
|
|
538
|
+
panelResult: {
|
|
539
|
+
...invocation.panelResult,
|
|
540
|
+
updatedAt: nowIso(),
|
|
541
|
+
linkedDecisionRecordIds
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
export async function answerWorkspaceQuestion(options) {
|
|
546
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
547
|
+
const question = findQuestionForDecision(state, options.questionId);
|
|
548
|
+
if (!question) {
|
|
549
|
+
throw new Error(options.questionId ? `No pending LongTable question found for ${options.questionId}.` : "No pending LongTable question was found.");
|
|
550
|
+
}
|
|
551
|
+
const option = question.prompt.options.find((candidate) => candidate.value === options.answer);
|
|
552
|
+
const explicitOther = options.answer === "other" && question.prompt.allowOther;
|
|
553
|
+
const answer = {
|
|
554
|
+
promptId: question.prompt.id,
|
|
555
|
+
selectedValues: [option?.value ?? "other"],
|
|
556
|
+
selectedLabels: [option?.label ?? (explicitOther ? question.prompt.otherLabel ?? "Other" : options.answer)],
|
|
557
|
+
...(option || explicitOther ? {} : { otherText: options.answer }),
|
|
558
|
+
...(options.rationale ? { rationale: options.rationale } : {}),
|
|
559
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
560
|
+
surface: options.provider === "claude" ? "native_structured" : "numbered"
|
|
561
|
+
};
|
|
562
|
+
const timestamp = nowIso();
|
|
563
|
+
const decision = {
|
|
564
|
+
id: createId("decision"),
|
|
565
|
+
timestamp,
|
|
566
|
+
checkpointKey: question.prompt.checkpointKey ?? "manual",
|
|
567
|
+
level: question.prompt.required ? "adaptive_required" : "recommended",
|
|
568
|
+
mode: "commit",
|
|
569
|
+
summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
|
|
570
|
+
selectedOption: answer.selectedValues[0],
|
|
571
|
+
...(options.rationale ? { rationale: options.rationale } : {})
|
|
572
|
+
};
|
|
573
|
+
const answeredQuestion = {
|
|
574
|
+
...question,
|
|
575
|
+
updatedAt: timestamp,
|
|
576
|
+
status: "answered",
|
|
577
|
+
answer,
|
|
578
|
+
decisionRecordId: decision.id
|
|
579
|
+
};
|
|
580
|
+
const withQuestion = {
|
|
581
|
+
...state,
|
|
582
|
+
questionLog: (state.questionLog ?? []).map((record) => record.id === question.id ? answeredQuestion : record),
|
|
583
|
+
invocationLog: (state.invocationLog ?? []).map((record) => updateInvocationWithDecision(record, question.id, decision.id))
|
|
584
|
+
};
|
|
585
|
+
const updated = appendDecisionToResearchState(withQuestion, decision);
|
|
586
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
587
|
+
await syncCurrentWorkspaceView(options.context);
|
|
588
|
+
return {
|
|
589
|
+
question: answeredQuestion,
|
|
590
|
+
decision,
|
|
591
|
+
state: updated
|
|
592
|
+
};
|
|
593
|
+
}
|
|
216
594
|
export async function createOrUpdateProjectWorkspace(options) {
|
|
217
595
|
const projectPath = resolve(options.projectPath);
|
|
218
596
|
const metaDir = resolveMetaDir(projectPath);
|
|
219
597
|
const sessionsDir = join(metaDir, "sessions");
|
|
220
598
|
const projectFilePath = join(metaDir, "project.json");
|
|
221
599
|
const sessionFilePath = join(metaDir, "current-session.json");
|
|
600
|
+
const stateFilePath = resolveStateFilePath(projectPath);
|
|
601
|
+
const currentFilePath = resolveCurrentFilePath(projectPath);
|
|
222
602
|
const sessionId = slugify(`${options.projectName}-${Date.now()}`);
|
|
603
|
+
const locale = resolveUserLocale();
|
|
604
|
+
const timestamp = nowIso();
|
|
223
605
|
await mkdir(projectPath, { recursive: true });
|
|
224
606
|
await mkdir(metaDir, { recursive: true });
|
|
225
607
|
await mkdir(sessionsDir, { recursive: true });
|
|
226
608
|
const project = existsSync(projectFilePath)
|
|
227
|
-
?
|
|
609
|
+
? {
|
|
610
|
+
...JSON.parse(await readFile(projectFilePath, "utf8")),
|
|
611
|
+
contractVersion: "workspace-v2",
|
|
612
|
+
locale
|
|
613
|
+
}
|
|
228
614
|
: {
|
|
229
615
|
schemaVersion: 1,
|
|
230
|
-
product: "
|
|
616
|
+
product: "LongTable",
|
|
231
617
|
projectName: options.projectName,
|
|
232
618
|
projectPath,
|
|
233
|
-
createdAt:
|
|
619
|
+
createdAt: timestamp,
|
|
620
|
+
contractVersion: "workspace-v2",
|
|
621
|
+
locale,
|
|
234
622
|
globalSetupSummary: {
|
|
235
623
|
field: options.setup.profileSeed.field,
|
|
236
624
|
careerStage: options.setup.profileSeed.careerStage,
|
|
@@ -250,30 +638,67 @@ export async function createOrUpdateProjectWorkspace(options) {
|
|
|
250
638
|
const session = {
|
|
251
639
|
schemaVersion: 1,
|
|
252
640
|
id: sessionId,
|
|
253
|
-
createdAt:
|
|
641
|
+
createdAt: timestamp,
|
|
642
|
+
lastUpdatedAt: timestamp,
|
|
254
643
|
projectName: project.projectName,
|
|
255
644
|
projectPath,
|
|
256
645
|
currentGoal: options.currentGoal,
|
|
257
646
|
...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
|
|
647
|
+
nextAction: buildNextAction({
|
|
648
|
+
schemaVersion: 1,
|
|
649
|
+
id: sessionId,
|
|
650
|
+
createdAt: timestamp,
|
|
651
|
+
projectName: project.projectName,
|
|
652
|
+
projectPath,
|
|
653
|
+
currentGoal: options.currentGoal,
|
|
654
|
+
...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
|
|
655
|
+
requestedPerspectives: options.requestedPerspectives,
|
|
656
|
+
disagreementPreference: options.disagreementPreference
|
|
657
|
+
}),
|
|
658
|
+
openQuestions: buildOpenQuestions({
|
|
659
|
+
schemaVersion: 1,
|
|
660
|
+
id: sessionId,
|
|
661
|
+
createdAt: timestamp,
|
|
662
|
+
projectName: project.projectName,
|
|
663
|
+
projectPath,
|
|
664
|
+
currentGoal: options.currentGoal,
|
|
665
|
+
...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
|
|
666
|
+
requestedPerspectives: options.requestedPerspectives,
|
|
667
|
+
disagreementPreference: options.disagreementPreference
|
|
668
|
+
}),
|
|
258
669
|
requestedPerspectives: options.requestedPerspectives,
|
|
259
|
-
disagreementPreference: options.disagreementPreference
|
|
670
|
+
disagreementPreference: options.disagreementPreference,
|
|
671
|
+
activeModes: ["explore"],
|
|
672
|
+
resumeHint: buildResumeHint({
|
|
673
|
+
schemaVersion: 1,
|
|
674
|
+
id: sessionId,
|
|
675
|
+
createdAt: timestamp,
|
|
676
|
+
projectName: project.projectName,
|
|
677
|
+
projectPath,
|
|
678
|
+
currentGoal: options.currentGoal,
|
|
679
|
+
...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
|
|
680
|
+
requestedPerspectives: options.requestedPerspectives,
|
|
681
|
+
disagreementPreference: options.disagreementPreference
|
|
682
|
+
}),
|
|
683
|
+
locale
|
|
260
684
|
};
|
|
261
|
-
|
|
262
|
-
await writeFile(sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
263
|
-
await writeFile(join(sessionsDir, `${sessionId}.json`), JSON.stringify(session, null, 2), "utf8");
|
|
264
|
-
await writeFile(join(metaDir, "state.json"), buildStateSeed(project, session, options.setup), "utf8");
|
|
265
|
-
await writeFile(join(projectPath, "LONGTABLE.md"), buildWorkspaceGuide(project, session), "utf8");
|
|
266
|
-
await writeFile(join(projectPath, "START-HERE.md"), buildStartHereGuide(project, session), "utf8");
|
|
267
|
-
await writeFile(join(projectPath, "NEXT-STEPS.md"), buildNextStepsGuide(project, session), "utf8");
|
|
268
|
-
await writeFile(join(projectPath, "SESSION-SNAPSHOT.md"), buildSessionSnapshot(project, session), "utf8");
|
|
269
|
-
await writeFile(join(projectPath, "AGENTS.md"), buildProjectAgentsMd(project, session), "utf8");
|
|
270
|
-
return {
|
|
685
|
+
const context = {
|
|
271
686
|
project,
|
|
272
687
|
session,
|
|
273
688
|
projectFilePath,
|
|
274
689
|
sessionFilePath,
|
|
690
|
+
stateFilePath,
|
|
691
|
+
currentFilePath,
|
|
275
692
|
metaDir
|
|
276
693
|
};
|
|
694
|
+
await writeFile(projectFilePath, JSON.stringify(project, null, 2), "utf8");
|
|
695
|
+
await writeFile(sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
696
|
+
await writeFile(join(sessionsDir, `${sessionId}.json`), JSON.stringify(session, null, 2), "utf8");
|
|
697
|
+
await writeFile(stateFilePath, buildStateSeed(project, session, options.setup), "utf8");
|
|
698
|
+
await writeFile(join(projectPath, "AGENTS.md"), buildProjectAgentsMd(project, session), "utf8");
|
|
699
|
+
await syncCurrentWorkspaceView(context);
|
|
700
|
+
await removeLegacyRootFiles(projectPath);
|
|
701
|
+
return context;
|
|
277
702
|
}
|
|
278
703
|
export async function loadProjectContextFromDirectory(startPath) {
|
|
279
704
|
let current = resolve(startPath);
|
|
@@ -282,11 +707,22 @@ export async function loadProjectContextFromDirectory(startPath) {
|
|
|
282
707
|
const projectFilePath = join(metaDir, "project.json");
|
|
283
708
|
const sessionFilePath = join(metaDir, "current-session.json");
|
|
284
709
|
if (existsSync(projectFilePath) && existsSync(sessionFilePath)) {
|
|
710
|
+
const project = JSON.parse(await readFile(projectFilePath, "utf8"));
|
|
711
|
+
const session = JSON.parse(await readFile(sessionFilePath, "utf8"));
|
|
285
712
|
return {
|
|
286
|
-
project
|
|
287
|
-
session:
|
|
713
|
+
project,
|
|
714
|
+
session: {
|
|
715
|
+
...session,
|
|
716
|
+
locale: session.locale ?? project.locale ?? resolveUserLocale(),
|
|
717
|
+
openQuestions: session.openQuestions ?? buildOpenQuestions(session),
|
|
718
|
+
nextAction: session.nextAction ?? buildNextAction(session),
|
|
719
|
+
resumeHint: session.resumeHint ?? buildResumeHint(session),
|
|
720
|
+
activeModes: session.activeModes ?? ["explore"]
|
|
721
|
+
},
|
|
288
722
|
projectFilePath,
|
|
289
723
|
sessionFilePath,
|
|
724
|
+
stateFilePath: resolveStateFilePath(project.projectPath),
|
|
725
|
+
currentFilePath: resolveCurrentFilePath(project.projectPath),
|
|
290
726
|
metaDir
|
|
291
727
|
};
|
|
292
728
|
}
|
|
@@ -297,10 +733,18 @@ export async function loadProjectContextFromDirectory(startPath) {
|
|
|
297
733
|
current = parent;
|
|
298
734
|
}
|
|
299
735
|
}
|
|
736
|
+
export async function inspectProjectWorkspace(startPath) {
|
|
737
|
+
const context = await loadProjectContextFromDirectory(startPath);
|
|
738
|
+
if (!context) {
|
|
739
|
+
return { found: false };
|
|
740
|
+
}
|
|
741
|
+
const state = await loadResearchState(context.stateFilePath);
|
|
742
|
+
return summarizeWorkspaceInspection(context, state);
|
|
743
|
+
}
|
|
300
744
|
export function renderProjectWorkspaceSummary(context) {
|
|
301
745
|
return [
|
|
302
746
|
"┌──────────────────────────────────────────────┐",
|
|
303
|
-
"│
|
|
747
|
+
"│ LongTable Project Workspace │",
|
|
304
748
|
"└──────────────────────────────────────────────┘",
|
|
305
749
|
`Project: ${context.project.projectName}`,
|
|
306
750
|
`Path: ${context.project.projectPath}`,
|
|
@@ -312,9 +756,8 @@ export function renderProjectWorkspaceSummary(context) {
|
|
|
312
756
|
"Created files:",
|
|
313
757
|
`- ${context.projectFilePath}`,
|
|
314
758
|
`- ${context.sessionFilePath}`,
|
|
315
|
-
`- ${
|
|
316
|
-
`- ${
|
|
317
|
-
`- ${join(context.project.projectPath, "SESSION-SNAPSHOT.md")}`,
|
|
759
|
+
`- ${context.stateFilePath}`,
|
|
760
|
+
`- ${context.currentFilePath}`,
|
|
318
761
|
`- ${join(context.project.projectPath, "AGENTS.md")}`
|
|
319
762
|
].join("\n");
|
|
320
763
|
}
|