openclew 0.2.0 → 0.3.0

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/lib/init.js CHANGED
@@ -15,7 +15,7 @@ const readline = require("readline");
15
15
  const { detectInstructionFiles, findAgentsMdCaseInsensitive } = require("./detect");
16
16
  const { inject, isAlreadyInjected } = require("./inject");
17
17
  const { writeConfig } = require("./config");
18
- const { guideContent, exampleLivingDocContent, exampleLogContent, today } = require("./templates");
18
+ const { guideContent, frameworkIntegrationContent, exampleRefdocContent, exampleLogContent, today } = require("./templates");
19
19
 
20
20
  const PROJECT_ROOT = process.cwd();
21
21
  const DOC_DIR = path.join(PROJECT_ROOT, "doc");
@@ -126,8 +126,8 @@ function installPreCommitHook() {
126
126
  }
127
127
 
128
128
  const preCommitPath = path.join(hooksDir, "pre-commit");
129
- const indexScript = `if [ -f doc/generate-index.py ]; then
130
- python3 doc/generate-index.py doc 2>/dev/null || echo "openclew: index generation failed"
129
+ const indexScript = `if command -v npx >/dev/null 2>&1; then
130
+ npx --yes openclew index 2>/dev/null || echo "openclew: index generation failed"
131
131
  git add doc/_INDEX.md 2>/dev/null
132
132
  fi`;
133
133
 
@@ -150,26 +150,37 @@ fi`;
150
150
  return true;
151
151
  }
152
152
 
153
- function copyGenerateIndex() {
154
- const src = path.join(__dirname, "..", "hooks", "generate-index.py");
155
- const dst = path.join(DOC_DIR, "generate-index.py");
153
+ function updateGitignore() {
154
+ const gitignorePath = path.join(PROJECT_ROOT, ".gitignore");
155
+ const entry = "doc/log/";
156
156
 
157
- if (fs.existsSync(dst)) {
158
- console.log(" doc/generate-index.py already exists");
159
- return false;
157
+ if (fs.existsSync(gitignorePath)) {
158
+ const content = fs.readFileSync(gitignorePath, "utf-8");
159
+ if (content.includes(entry)) {
160
+ console.log(" .gitignore already ignores doc/log/");
161
+ return;
162
+ }
163
+ fs.appendFileSync(gitignorePath, `\n${entry}\n`, "utf-8");
164
+ console.log(" Added doc/log/ to .gitignore");
165
+ } else {
166
+ fs.writeFileSync(gitignorePath, `${entry}\n`, "utf-8");
167
+ console.log(" Created .gitignore with doc/log/");
160
168
  }
169
+ }
161
170
 
162
- if (fs.existsSync(src)) {
163
- fs.copyFileSync(src, dst);
164
- console.log(" Copied generate-index.py to doc/");
171
+ function cleanupLegacyPython() {
172
+ // Remove legacy generate-index.py if present (replaced by JS-native index-gen)
173
+ const legacyScript = path.join(DOC_DIR, "generate-index.py");
174
+ if (fs.existsSync(legacyScript)) {
175
+ fs.unlinkSync(legacyScript);
176
+ console.log(" Removed legacy doc/generate-index.py (now JS-native)");
165
177
  return true;
166
178
  }
167
-
168
- console.log(" generate-index.py not found in package — skipping");
179
+ console.log(" No legacy Python script to clean up");
169
180
  return false;
170
181
  }
171
182
 
172
- function createDocs() {
183
+ function createDocs(entryPointPath) {
173
184
  // Guide — always created
174
185
  const guidePath = path.join(DOC_DIR, "_USING_OPENCLEW.md");
175
186
  if (!fs.existsSync(guidePath)) {
@@ -179,11 +190,30 @@ function createDocs() {
179
190
  console.log(" doc/_USING_OPENCLEW.md already exists");
180
191
  }
181
192
 
182
- // Example living doc
193
+ // Framework integration guide
194
+ const frameworkPath = path.join(DOC_DIR, "_OPENCLEW_FRAMEWORK_INTEGRATION.md");
195
+ if (!fs.existsSync(frameworkPath)) {
196
+ fs.writeFileSync(frameworkPath, frameworkIntegrationContent(), "utf-8");
197
+ console.log(" Created doc/_OPENCLEW_FRAMEWORK_INTEGRATION.md (framework integration guide)");
198
+ } else {
199
+ console.log(" doc/_OPENCLEW_FRAMEWORK_INTEGRATION.md already exists");
200
+ }
201
+
202
+ // Architecture refdoc — seeded from existing instruction file if available
183
203
  const examplePath = path.join(DOC_DIR, "_ARCHITECTURE.md");
184
204
  if (!fs.existsSync(examplePath)) {
185
- fs.writeFileSync(examplePath, exampleLivingDocContent(), "utf-8");
186
- console.log(" Created doc/_ARCHITECTURE.md (example living doc)");
205
+ let existingInstructions = null;
206
+ if (entryPointPath && fs.existsSync(entryPointPath)) {
207
+ try {
208
+ existingInstructions = fs.readFileSync(entryPointPath, "utf-8");
209
+ } catch {}
210
+ }
211
+ fs.writeFileSync(examplePath, exampleRefdocContent(existingInstructions), "utf-8");
212
+ if (existingInstructions) {
213
+ console.log(" Created doc/_ARCHITECTURE.md (seeded from instruction file)");
214
+ } else {
215
+ console.log(" Created doc/_ARCHITECTURE.md (template)");
216
+ }
187
217
  } else {
188
218
  console.log(" doc/_ARCHITECTURE.md already exists");
189
219
  }
@@ -199,15 +229,14 @@ function createDocs() {
199
229
  }
200
230
 
201
231
  function runIndexGenerator() {
202
- const indexScript = path.join(DOC_DIR, "generate-index.py");
203
- if (!fs.existsSync(indexScript)) return;
232
+ if (!fs.existsSync(DOC_DIR)) return;
204
233
 
205
234
  try {
206
- const { execSync } = require("child_process");
207
- execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
208
- console.log(" Generated doc/_INDEX.md");
209
- } catch {
210
- console.log(" Could not generate index (python3 not available)");
235
+ const { writeIndex } = require("./index-gen");
236
+ const { refdocs, logs } = writeIndex(DOC_DIR);
237
+ console.log(` Generated doc/_INDEX.md (${refdocs} refdocs, ${logs} logs)`);
238
+ } catch (err) {
239
+ console.log(` Could not generate index: ${err.message}`);
211
240
  }
212
241
  }
213
242
 
@@ -218,12 +247,16 @@ async function main() {
218
247
  console.log("1. Project structure");
219
248
  createDirs();
220
249
 
221
- // Step 2: Copy index generator
222
- console.log("\n2. Index generator");
223
- copyGenerateIndex();
250
+ // Step 2: Gitignore
251
+ console.log("\n2. Gitignore");
252
+ updateGitignore();
253
+
254
+ // Step 3: Cleanup legacy Python (if upgrading from older version)
255
+ console.log("\n3. Index generator");
256
+ cleanupLegacyPython();
224
257
 
225
- // Step 3: Entry point
226
- console.log("\n3. Entry point");
258
+ // Step 4: Entry point
259
+ console.log("\n4. Entry point");
227
260
  const entryPoint = await resolveEntryPoint();
228
261
 
229
262
  if (entryPoint) {
@@ -241,20 +274,20 @@ async function main() {
241
274
  writeConfig({ entryPoint: null }, PROJECT_ROOT);
242
275
  }
243
276
 
244
- // Step 4: Pre-commit hook
245
- console.log("\n4. Pre-commit hook");
277
+ // Step 5: Pre-commit hook
278
+ console.log("\n5. Pre-commit hook");
246
279
  if (noHook) {
247
280
  console.log(" Skipping (--no-hook)");
248
281
  } else {
249
282
  installPreCommitHook();
250
283
  }
251
284
 
252
- // Step 5: Docs
253
- console.log("\n5. Docs");
254
- createDocs();
285
+ // Step 6: Docs
286
+ console.log("\n6. Docs");
287
+ createDocs(entryPoint ? entryPoint.fullPath : null);
255
288
 
256
- // Step 6: Generate index
257
- console.log("\n6. Index");
289
+ // Step 7: Generate index
290
+ console.log("\n7. Index");
258
291
  runIndexGenerator();
259
292
 
260
293
  // Done
package/lib/inject.js CHANGED
@@ -7,17 +7,26 @@ const fs = require("fs");
7
7
  const OPENCLEW_BLOCK = `
8
8
  ## Project knowledge (openclew)
9
9
 
10
- This file is the **entry point** for project documentation.
10
+ This project uses \`doc/\` as its knowledge base. Before starting any task:
11
11
 
12
- **Doc-first rule:** before any task, read \`doc/_INDEX.md\` to find docs related to the task. Read them before exploring code.
12
+ 1. **Read \`doc/_INDEX.md\`** it lists all available docs with a one-line summary each
13
+ 2. **Pick your reference doc(s)** — choose one or more docs relevant to what you're about to do
14
+ 3. **Read them** (L1 for relevance, L2 for context) — then start working
15
+ 4. **No matching doc?** — propose creating a refdoc with \`npx openclew new "Title"\` before starting
13
16
 
14
- Two types of docs in \`doc/\`:
15
- - **Living docs** (\`doc/_*.md\`) — evolve with the project (architecture, conventions, decisions)
16
- - **Logs** (\`doc/log/YYYY-MM-DD_*.md\`) — frozen facts from a session, never modified after
17
+ If a doc contains placeholder comments (\`<!-- ... -->\`), fill them in based on what you observe in the code. This is expected — the docs are meant to be written by you.
17
18
 
18
- Each doc has 3 levels: **L1** (metadata — read first to decide relevance) → **L2** (summary) → **L3** (full details, only when needed).
19
+ Two types of docs:
20
+ - **Refdocs** (\`doc/_*.md\`) — architecture, conventions, decisions (evolve over time)
21
+ - **Logs** (\`doc/log/YYYY-MM-DD_*.md\`) — frozen facts from past sessions
19
22
 
20
- **Creating docs:** when a decision, convention, or significant event needs to be captured, create the file directly following the format in \`doc/_USING_OPENCLEW.md\`.
23
+ Each doc has 3 levels: **L1** (subject + brief 1 line) **L2** (summary) **L3** (full details, only when needed).
24
+
25
+ **Session commands** (user asks in chat, you run):
26
+ - "checkout" → \`npx openclew checkout\` (end-of-session summary + log)
27
+ - "new doc about X" → \`npx openclew new "X"\` (create refdoc)
28
+ - "search X" → \`npx openclew search "X"\` (search docs)
29
+ - "doc status" → \`npx openclew status\` (health dashboard)
21
30
  `.trim();
22
31
 
23
32
  const MARKER_START = "<!-- openclew_START -->";
@@ -0,0 +1,313 @@
1
+ /**
2
+ * openclew MCP server — Model Context Protocol over stdio.
3
+ *
4
+ * Exposes openclew docs as MCP tools so AI agents (Claude Code, Cursor, etc.)
5
+ * can search and read project documentation natively.
6
+ *
7
+ * Tools:
8
+ * - search_docs(query) Search docs by keyword (L1/metadata)
9
+ * - read_doc(path, level?) Read a doc at specified level (L1/L2/L3/full)
10
+ * - list_docs(kind?) List all docs with L1 metadata
11
+ *
12
+ * Protocol: MCP 2024-11-05 over stdio (JSON-RPC line-delimited)
13
+ * Zero dependencies — Node 16+ standard library only.
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const readline = require("readline");
19
+ const { searchDocs, collectDocs, parseFile } = require("./search");
20
+
21
+ const PROJECT_ROOT = process.cwd();
22
+ const DOC_DIR = path.join(PROJECT_ROOT, "doc");
23
+
24
+ // ── Helpers ─────────────────────────────────────────────────────────
25
+
26
+ function ocVersion() {
27
+ try {
28
+ return require(path.join(__dirname, "..", "package.json")).version;
29
+ } catch {
30
+ return "0.0.0";
31
+ }
32
+ }
33
+
34
+ function extractLevel(content, level) {
35
+ if (level === "full") return content;
36
+
37
+ const markers = {
38
+ L1: [/<!--\s*L1_START\s*-->/, /<!--\s*L1_END\s*-->/],
39
+ L2: [/<!--\s*L2_START\s*-->/, /<!--\s*L2_END\s*-->/],
40
+ L3: [/<!--\s*L3_START\s*-->/, /<!--\s*L3_END\s*-->/],
41
+ };
42
+
43
+ const key = level.toUpperCase();
44
+ if (!markers[key]) return content;
45
+
46
+ const [startRe, endRe] = markers[key];
47
+ const startMatch = content.match(startRe);
48
+ const endMatch = content.match(endRe);
49
+ if (!startMatch || !endMatch) return `No ${key} block found in this document.`;
50
+
51
+ const startIdx = startMatch.index + startMatch[0].length;
52
+ const endIdx = endMatch.index;
53
+ return content.slice(startIdx, endIdx).trim();
54
+ }
55
+
56
+ // ── MCP Tool implementations ────────────────────────────────────────
57
+
58
+ function toolSearchDocs(params) {
59
+ const query = params.query;
60
+ if (!query) return { error: "Missing required parameter: query" };
61
+ if (!fs.existsSync(DOC_DIR)) return { error: "No doc/ directory found." };
62
+
63
+ const results = searchDocs(DOC_DIR, query);
64
+ return results.map((r) => ({
65
+ path: path.relative(PROJECT_ROOT, r.filepath),
66
+ kind: r.kind,
67
+ subject: r.meta.subject || r.filename,
68
+ doc_brief: r.meta.doc_brief || "",
69
+ status: r.meta.status || "",
70
+ category: r.meta.category || "",
71
+ score: r.score,
72
+ }));
73
+ }
74
+
75
+ function toolReadDoc(params) {
76
+ const docPath = params.path;
77
+ if (!docPath) return { error: "Missing required parameter: path" };
78
+
79
+ const absPath = path.resolve(PROJECT_ROOT, docPath);
80
+ // Security: ensure path is within project
81
+ if (!absPath.startsWith(PROJECT_ROOT)) return { error: "Path outside project." };
82
+ if (!fs.existsSync(absPath)) return { error: `File not found: ${docPath}` };
83
+
84
+ const content = fs.readFileSync(absPath, "utf-8");
85
+ const level = params.level || "L2";
86
+
87
+ return {
88
+ path: docPath,
89
+ level: level,
90
+ content: extractLevel(content, level),
91
+ };
92
+ }
93
+
94
+ function toolListDocs(params) {
95
+ if (!fs.existsSync(DOC_DIR)) return { error: "No doc/ directory found." };
96
+
97
+ const docs = collectDocs(DOC_DIR);
98
+ const kind = params.kind; // "refdoc", "log", or undefined (all)
99
+
100
+ return docs
101
+ .filter((d) => !kind || d.kind === kind)
102
+ .map((d) => ({
103
+ path: path.relative(PROJECT_ROOT, d.filepath),
104
+ kind: d.kind,
105
+ subject: d.meta.subject || d.filename,
106
+ doc_brief: d.meta.doc_brief || "",
107
+ status: d.meta.status || "",
108
+ category: d.meta.category || "",
109
+ }));
110
+ }
111
+
112
+ // ── MCP Protocol ────────────────────────────────────────────────────
113
+
114
+ const TOOLS = [
115
+ {
116
+ name: "search_docs",
117
+ description:
118
+ "Search project documentation by keyword. Searches subject, doc_brief, category, keywords, type, and status fields. Returns results sorted by relevance.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ query: {
123
+ type: "string",
124
+ description: "Search query (space-separated terms)",
125
+ },
126
+ },
127
+ required: ["query"],
128
+ },
129
+ },
130
+ {
131
+ name: "read_doc",
132
+ description:
133
+ "Read a project document at a specified level. L1 = subject + brief (~40 tokens). L2 = summary + key points. L3 = full technical details. full = entire file.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ path: {
138
+ type: "string",
139
+ description: "Relative path to the document (e.g. doc/_ARCHITECTURE.md)",
140
+ },
141
+ level: {
142
+ type: "string",
143
+ enum: ["L1", "L2", "L3", "full"],
144
+ description: "Level of detail to return (default: L2)",
145
+ },
146
+ },
147
+ required: ["path"],
148
+ },
149
+ },
150
+ {
151
+ name: "list_docs",
152
+ description:
153
+ "List all project documents with their L1 metadata (subject, brief, status, category).",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ kind: {
158
+ type: "string",
159
+ enum: ["refdoc", "log"],
160
+ description: "Filter by document type. Omit to list all.",
161
+ },
162
+ },
163
+ },
164
+ },
165
+ ];
166
+
167
+ const TOOL_HANDLERS = {
168
+ search_docs: toolSearchDocs,
169
+ read_doc: toolReadDoc,
170
+ list_docs: toolListDocs,
171
+ };
172
+
173
+ function handleMessage(msg) {
174
+ const { method, id, params } = msg;
175
+
176
+ switch (method) {
177
+ case "initialize":
178
+ return {
179
+ jsonrpc: "2.0",
180
+ id,
181
+ result: {
182
+ protocolVersion: "2024-11-05",
183
+ capabilities: { tools: {} },
184
+ serverInfo: {
185
+ name: "openclew",
186
+ version: ocVersion(),
187
+ },
188
+ },
189
+ };
190
+
191
+ case "notifications/initialized":
192
+ return null; // No response for notifications
193
+
194
+ case "tools/list":
195
+ return {
196
+ jsonrpc: "2.0",
197
+ id,
198
+ result: { tools: TOOLS },
199
+ };
200
+
201
+ case "tools/call": {
202
+ const toolName = params && params.name;
203
+ const handler = TOOL_HANDLERS[toolName];
204
+ if (!handler) {
205
+ return {
206
+ jsonrpc: "2.0",
207
+ id,
208
+ error: { code: -32601, message: `Unknown tool: ${toolName}` },
209
+ };
210
+ }
211
+
212
+ const toolArgs = params.arguments || {};
213
+ const result = handler(toolArgs);
214
+
215
+ // If result has an error field, return as tool error content
216
+ if (result && result.error) {
217
+ return {
218
+ jsonrpc: "2.0",
219
+ id,
220
+ result: {
221
+ content: [{ type: "text", text: result.error }],
222
+ isError: true,
223
+ },
224
+ };
225
+ }
226
+
227
+ return {
228
+ jsonrpc: "2.0",
229
+ id,
230
+ result: {
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
235
+ },
236
+ ],
237
+ },
238
+ };
239
+ }
240
+
241
+ default:
242
+ if (id !== undefined) {
243
+ return {
244
+ jsonrpc: "2.0",
245
+ id,
246
+ error: { code: -32601, message: `Method not found: ${method}` },
247
+ };
248
+ }
249
+ return null; // Ignore unknown notifications
250
+ }
251
+ }
252
+
253
+ // ── stdio transport ─────────────────────────────────────────────────
254
+
255
+ function run() {
256
+ // Check if running as CLI help
257
+ const args = process.argv.slice(2);
258
+ const cmdIndex = args.indexOf("mcp");
259
+ const extraArgs = cmdIndex >= 0 ? args.slice(cmdIndex + 1) : args.slice(1);
260
+
261
+ if (extraArgs.includes("--help") || extraArgs.includes("-h")) {
262
+ console.log("openclew MCP server — Model Context Protocol over stdio");
263
+ console.log("");
264
+ console.log("Usage: openclew mcp");
265
+ console.log("");
266
+ console.log("Starts an MCP server on stdin/stdout for AI agent integration.");
267
+ console.log("Configure in your AI tool's MCP settings:");
268
+ console.log("");
269
+ console.log(' { "command": "npx", "args": ["openclew", "mcp"] }');
270
+ console.log("");
271
+ console.log("Tools exposed:");
272
+ console.log(" search_docs(query) Search docs by keyword");
273
+ console.log(" read_doc(path, level?) Read doc at L1/L2/L3/full");
274
+ console.log(" list_docs(kind?) List all docs with L1 metadata");
275
+ process.exit(0);
276
+ }
277
+
278
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
279
+
280
+ rl.on("line", (line) => {
281
+ const trimmed = line.trim();
282
+ if (!trimmed) return;
283
+
284
+ let msg;
285
+ try {
286
+ msg = JSON.parse(trimmed);
287
+ } catch {
288
+ const err = {
289
+ jsonrpc: "2.0",
290
+ id: null,
291
+ error: { code: -32700, message: "Parse error" },
292
+ };
293
+ process.stdout.write(JSON.stringify(err) + "\n");
294
+ return;
295
+ }
296
+
297
+ const response = handleMessage(msg);
298
+ if (response) {
299
+ process.stdout.write(JSON.stringify(response) + "\n");
300
+ }
301
+ });
302
+
303
+ rl.on("close", () => process.exit(0));
304
+ }
305
+
306
+ // Export for tests
307
+ module.exports = { handleMessage, extractLevel, TOOLS };
308
+
309
+ // Run as CLI (invoked via dispatcher or directly)
310
+ const calledAsMcp = process.argv.includes("mcp");
311
+ if (require.main === module || calledAsMcp) {
312
+ run();
313
+ }
package/lib/new-doc.js CHANGED
@@ -1,20 +1,28 @@
1
1
  /**
2
- * openclew new <title> — create a new living doc.
2
+ * openclew new <title> — create a new refdoc.
3
3
  */
4
4
 
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
- const { livingContent, slugify } = require("./templates");
7
+ const { refdocContent, slugify } = require("./templates");
8
8
  const { readConfig } = require("./config");
9
9
 
10
10
  const args = process.argv.slice(2);
11
- // Remove "new" command from args
12
- const cmdIndex = args.indexOf("new");
13
- const titleArgs = cmdIndex >= 0 ? args.slice(cmdIndex + 1) : args.slice(1);
11
+ // Support both "add ref <title>" and legacy "new <title>"
12
+ const refIndex = args.indexOf("ref");
13
+ const newIndex = args.indexOf("new");
14
+ let titleArgs;
15
+ if (refIndex >= 0) {
16
+ titleArgs = args.slice(refIndex + 1);
17
+ } else if (newIndex >= 0) {
18
+ titleArgs = args.slice(newIndex + 1);
19
+ } else {
20
+ titleArgs = args.slice(1);
21
+ }
14
22
  const title = titleArgs.join(" ");
15
23
 
16
24
  if (!title) {
17
- console.error('Usage: openclew new "Title of the document"');
25
+ console.error('Usage: openclew add ref "Title of the document"');
18
26
  process.exit(1);
19
27
  }
20
28
 
@@ -37,7 +45,7 @@ if (fs.existsSync(filepath)) {
37
45
  process.exit(1);
38
46
  }
39
47
 
40
- fs.writeFileSync(filepath, livingContent(title), "utf-8");
48
+ fs.writeFileSync(filepath, refdocContent(title), "utf-8");
41
49
  console.log(`Created doc/${filename}`);
42
50
  console.log("");
43
51
  console.log("Next: open the file and fill in:");
package/lib/new-log.js CHANGED
@@ -8,13 +8,13 @@ const { logContent, slugifyLog, today } = require("./templates");
8
8
  const { readConfig } = require("./config");
9
9
 
10
10
  const args = process.argv.slice(2);
11
- // Remove "log" command from args
12
- const cmdIndex = args.indexOf("log");
13
- const titleArgs = cmdIndex >= 0 ? args.slice(cmdIndex + 1) : args.slice(1);
11
+ // Support both "add log <title>" and legacy "log <title>"
12
+ const logIndex = args.lastIndexOf("log");
13
+ const titleArgs = logIndex >= 0 ? args.slice(logIndex + 1) : args.slice(1);
14
14
  const title = titleArgs.join(" ");
15
15
 
16
16
  if (!title) {
17
- console.error('Usage: openclew log "Title of the log"');
17
+ console.error('Usage: openclew add log "Title of the log"');
18
18
  process.exit(1);
19
19
  }
20
20
 
@@ -42,7 +42,7 @@ fs.writeFileSync(filepath, logContent(title), "utf-8");
42
42
  console.log(`Created doc/log/${filename}`);
43
43
  console.log("");
44
44
  console.log("Next: open the file and fill in:");
45
- console.log(" L1 — type, status, short_story (what happened in 1-2 sentences)");
45
+ console.log(" L1 — subject + doc_brief (what happened in 1-2 sentences)");
46
46
  console.log(" L2 — problem + solution (the facts, frozen after this session)");
47
47
  console.log("");
48
48
  console.log("Logs are immutable — once written, never modified.");