chapterhouse 0.7.0 → 0.8.1

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.
Files changed (81) hide show
  1. package/agents/korg.agent.md +65 -0
  2. package/dist/api/korg.js +34 -0
  3. package/dist/api/korg.test.js +42 -0
  4. package/dist/api/server.js +238 -2
  5. package/dist/api/server.test.js +199 -0
  6. package/dist/config.js +28 -0
  7. package/dist/config.test.js +20 -0
  8. package/dist/copilot/agents.js +3 -4
  9. package/dist/copilot/agents.test.js +12 -1
  10. package/dist/copilot/orchestrator.js +12 -1
  11. package/dist/copilot/orchestrator.test.js +3 -7
  12. package/dist/copilot/system-message.js +12 -10
  13. package/dist/copilot/system-message.test.js +6 -1
  14. package/dist/copilot/tools.js +193 -375
  15. package/dist/copilot/tools.memory.test.js +32 -0
  16. package/dist/copilot/tools.wiki.test.js +80 -59
  17. package/dist/copilot/turn-event-log-env.test.js +11 -15
  18. package/dist/daemon.js +19 -0
  19. package/dist/memory/decisions.js +6 -5
  20. package/dist/memory/entities.js +20 -9
  21. package/dist/memory/eot.js +30 -8
  22. package/dist/memory/eot.test.js +220 -6
  23. package/dist/memory/hooks.js +151 -0
  24. package/dist/memory/hooks.test.js +325 -0
  25. package/dist/memory/hot-tier.js +37 -0
  26. package/dist/memory/hot-tier.test.js +30 -0
  27. package/dist/memory/housekeeping-scheduler.js +35 -0
  28. package/dist/memory/housekeeping-scheduler.test.js +50 -0
  29. package/dist/memory/inbox.js +10 -0
  30. package/dist/memory/index.js +3 -1
  31. package/dist/memory/migration.js +244 -0
  32. package/dist/memory/migration.test.js +108 -0
  33. package/dist/memory/reflect.js +273 -0
  34. package/dist/memory/reflect.test.js +254 -0
  35. package/dist/paths.js +31 -11
  36. package/dist/store/db.js +187 -4
  37. package/dist/store/db.test.js +66 -2
  38. package/dist/test/helpers/reset-singletons.js +8 -0
  39. package/dist/test/helpers/reset-singletons.test.js +37 -0
  40. package/dist/test/setup-env.js +9 -1
  41. package/dist/wiki/consolidation.js +641 -0
  42. package/dist/wiki/consolidation.test.js +143 -0
  43. package/dist/wiki/frontmatter.js +48 -0
  44. package/dist/wiki/frontmatter.test.js +42 -0
  45. package/dist/wiki/fs.js +22 -13
  46. package/dist/wiki/index-manager.js +305 -330
  47. package/dist/wiki/index-manager.test.js +265 -144
  48. package/dist/wiki/ingest.js +347 -0
  49. package/dist/wiki/ingest.test.js +111 -0
  50. package/dist/wiki/links.js +151 -0
  51. package/dist/wiki/links.test.js +176 -0
  52. package/dist/wiki/log-manager.js +8 -5
  53. package/dist/wiki/log-manager.test.js +4 -0
  54. package/dist/wiki/migrate-topics.test.js +16 -6
  55. package/dist/wiki/scheduler.js +118 -0
  56. package/dist/wiki/scheduler.test.js +64 -0
  57. package/dist/wiki/timeline.js +51 -0
  58. package/dist/wiki/timeline.test.js +65 -0
  59. package/dist/wiki/topic-structure.js +1 -1
  60. package/package.json +1 -1
  61. package/skills/pkb-ideas/SKILL.md +78 -0
  62. package/skills/pkb-ideas/_meta.json +4 -0
  63. package/skills/pkb-org/SKILL.md +82 -0
  64. package/skills/pkb-org/_meta.json +4 -0
  65. package/skills/pkb-people/SKILL.md +74 -0
  66. package/skills/pkb-people/_meta.json +4 -0
  67. package/skills/pkb-research/SKILL.md +83 -0
  68. package/skills/pkb-research/_meta.json +4 -0
  69. package/skills/pkb-source/SKILL.md +38 -0
  70. package/skills/pkb-source/_meta.json +4 -0
  71. package/skills/wiki-conventions/SKILL.md +5 -5
  72. package/web/dist/assets/{index-DuKYxMIR.css → index-5kz9aRU9.css} +1 -1
  73. package/web/dist/assets/{index-DytB69KC.js → index-BbX9RKf3.js} +91 -89
  74. package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
  75. package/web/dist/index.html +2 -2
  76. package/dist/wiki/context.js +0 -138
  77. package/dist/wiki/fix.js +0 -335
  78. package/dist/wiki/fix.test.js +0 -350
  79. package/dist/wiki/lint.js +0 -451
  80. package/dist/wiki/lint.test.js +0 -329
  81. package/web/dist/assets/index-DytB69KC.js.map +0 -1
@@ -0,0 +1,143 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { resetSingletons } from "../test/helpers/reset-singletons.js";
6
+ const repoRoot = process.cwd();
7
+ const sandboxRoot = join(repoRoot, ".test-work", `wiki-consolidation-${process.pid}`);
8
+ const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
9
+ function resetSandbox() {
10
+ mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
11
+ rmSync(sandboxRoot, { recursive: true, force: true });
12
+ mkdirSync(chapterhouseHome, { recursive: true });
13
+ }
14
+ async function loadModules() {
15
+ const nonce = `${Date.now()}-${Math.random()}`;
16
+ const dbModule = await import(new URL(`../store/db.js?c=${nonce}`, import.meta.url).href);
17
+ const wikiFs = await import(new URL(`./fs.js?c=${nonce}`, import.meta.url).href);
18
+ const indexManager = await import(new URL(`./index-manager.js?c=${nonce}`, import.meta.url).href);
19
+ const consolidation = await import(new URL(`./consolidation.js?c=${nonce}`, import.meta.url).href);
20
+ const memory = await import(new URL(`../memory/index.js?c=${nonce}`, import.meta.url).href);
21
+ return { dbModule, wikiFs, indexManager, consolidation, memory };
22
+ }
23
+ function makePage(title, summary, updated, summaryBody, timelineBlocks, pinned = false) {
24
+ return `---\ntitle: ${title}\nsummary: ${summary}\nupdated: ${updated}\ntags: []\npinned: ${pinned ? "true" : "false"}\n---\n\n# ${title}\n\n## Summary\n\n${summaryBody}\n\n## Timeline\n\n${timelineBlocks.trim()}\n`;
25
+ }
26
+ test.beforeEach(async () => {
27
+ process.env.CHAPTERHOUSE_HOME = sandboxRoot;
28
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
29
+ dbModule.closeDb();
30
+ resetSandbox();
31
+ resetSingletons();
32
+ });
33
+ test.after(async () => {
34
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
35
+ dbModule.closeDb();
36
+ resetSingletons();
37
+ rmSync(sandboxRoot, { recursive: true, force: true });
38
+ });
39
+ test("runConsolidation rewrites stale compiled truth and skips pinned pages", async () => {
40
+ const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
41
+ const db = dbModule.getDb();
42
+ wikiFs.ensureWikiStructure();
43
+ wikiFs.writePage("pages/topics/rust/index.md", makePage("Rust", "Systems language", "2026-05-01", "Old summary.", `### 2026-05-02T10:00:00.000Z\n\nStabilized async patterns.\n\n### 2026-05-04T10:00:00.000Z\n\nAdopted by backend team.`));
44
+ wikiFs.writePage("pages/topics/pinned/index.md", makePage("Pinned", "Pinned page", "2026-05-01", "Keep this summary.", `### 2026-05-10T10:00:00.000Z\n\nNew fact that should not trigger rewrite.`, true));
45
+ indexManager.rebuildWikiIndex();
46
+ const result = await consolidation.runConsolidationWithDeps(db, {
47
+ now: () => new Date("2026-05-14T22:30:03.086Z"),
48
+ synthesizeTruth: async ({ pagePath, pendingEntries }) => `Synthesized ${pagePath} from ${pendingEntries.length} entries.`,
49
+ commitWikiChanges: async () => false,
50
+ });
51
+ const rewritten = wikiFs.readPage("pages/topics/rust/index.md") ?? "";
52
+ const pinned = wikiFs.readPage("pages/topics/pinned/index.md") ?? "";
53
+ const row = db.prepare(`SELECT compiled_truth_hash FROM wiki_pages WHERE path = ?`).get("pages/topics/rust/index.md");
54
+ assert.match(rewritten, /Synthesized pages\/topics\/rust\/index.md from 2 entries\./);
55
+ assert.match(rewritten, /last_updated:/);
56
+ assert.ok(row.compiled_truth_hash, "compiled truth hash should be stored");
57
+ assert.equal(result.truthRewrites, 1);
58
+ assert.equal(result.llmCallsUsed, 1);
59
+ assert.match(pinned, /Keep this summary\./);
60
+ assert.doesNotMatch(pinned, /Synthesized/);
61
+ });
62
+ test("runConsolidation merges fragment pages into a canonical page and repoints links", async () => {
63
+ const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
64
+ const db = dbModule.getDb();
65
+ wikiFs.ensureWikiStructure();
66
+ wikiFs.writePage("pages/topics/rust/index.md", makePage("Rust", "Main Rust page", "2026-05-10", "Canonical summary.", `### 2026-05-10T10:00:00.000Z\n\nCanonical fact.`));
67
+ wikiFs.writePage("pages/topics/ruts/index.md", makePage("Ruts", "Fragment Rust page", "2026-05-11", "Fragment summary.", `### 2026-05-11T10:00:00.000Z\n\nFragment fact.`));
68
+ wikiFs.writePage("pages/topics/async/index.md", makePage("Async", "Async topic", "2026-05-10", "Async summary with [[Ruts]].", `### 2026-05-10T08:00:00.000Z\n\nReferences [[Ruts]].`));
69
+ indexManager.rebuildWikiIndex();
70
+ const result = await consolidation.runConsolidationWithDeps(db, {
71
+ now: () => new Date("2026-05-14T22:30:03.086Z"),
72
+ synthesizeTruth: async () => "unused",
73
+ commitWikiChanges: async () => false,
74
+ });
75
+ const canonical = wikiFs.readPage("pages/topics/rust/index.md") ?? "";
76
+ const fragmentExists = wikiFs.pageExists("pages/topics/ruts/index.md");
77
+ const repointed = db.prepare(`SELECT COUNT(*) as c FROM wiki_links WHERE to_page = ?`).get("pages/topics/rust/index.md");
78
+ const fragmentRow = db.prepare(`SELECT path FROM wiki_pages WHERE path = ?`).get("pages/topics/ruts/index.md");
79
+ assert.equal(result.fragmentsMerged, 1);
80
+ assert.match(canonical, /Canonical fact\./);
81
+ assert.match(canonical, /Fragment fact\./);
82
+ assert.equal(fragmentExists, false);
83
+ assert.equal(fragmentRow, undefined);
84
+ assert.ok(repointed.c >= 1, "links should be repointed to the canonical page");
85
+ });
86
+ test("runConsolidation removes orphaned wiki_links rows", async () => {
87
+ const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
88
+ const db = dbModule.getDb();
89
+ wikiFs.ensureWikiStructure();
90
+ wikiFs.writePage("pages/topics/alpha/index.md", makePage("Alpha", "Alpha page", "2026-05-10", "Alpha summary.", `### 2026-05-10T10:00:00.000Z\n\nAlpha fact.`));
91
+ indexManager.rebuildWikiIndex();
92
+ db.prepare(`INSERT INTO wiki_links (from_page, to_page, link_type, extracted_at) VALUES (?, ?, ?, ?)`)
93
+ .run("pages/topics/alpha/index.md", "pages/topics/missing/index.md", "references", "2026-05-12T00:00:00.000Z");
94
+ const result = await consolidation.runConsolidationWithDeps(db, {
95
+ now: () => new Date("2026-05-14T22:30:03.086Z"),
96
+ synthesizeTruth: async () => "unused",
97
+ commitWikiChanges: async () => false,
98
+ });
99
+ const orphanCount = db.prepare(`SELECT COUNT(*) as c FROM wiki_links WHERE to_page = ?`).get("pages/topics/missing/index.md");
100
+ assert.equal(result.linksRepaired, 1);
101
+ assert.equal(orphanCount.c, 0);
102
+ });
103
+ test("runConsolidation creates an action item for research sessions inactive for 7+ days", async () => {
104
+ const { dbModule, wikiFs, consolidation, memory } = await loadModules();
105
+ const db = dbModule.getDb();
106
+ wikiFs.ensureWikiStructure();
107
+ const globalScope = memory.getScope("global");
108
+ assert.ok(globalScope);
109
+ db.prepare(`
110
+ INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
111
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
112
+ `).run("source-1", "url", "https://example.test/1", "Research note", "2026-05-01T10:00:00.000Z", "sources/source-1.md", "content", "[]", "active", "session-123", "Compiler research");
113
+ const result = await consolidation.runConsolidationWithDeps(db, {
114
+ now: () => new Date("2026-05-14T22:30:03.086Z"),
115
+ synthesizeTruth: async () => "unused",
116
+ commitWikiChanges: async () => false,
117
+ });
118
+ const actionItems = memory.listActionItems({ scope_id: globalScope.id, includeArchived: true });
119
+ assert.equal(result.staleSessionsNotified, 1);
120
+ assert.equal(actionItems.some((item) => item.title.includes("Research session 'Compiler research' has been inactive for 7+ days")), true);
121
+ });
122
+ test("runConsolidation caps truth rewrites before exceeding the LLM budget", async () => {
123
+ const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
124
+ const db = dbModule.getDb();
125
+ wikiFs.ensureWikiStructure();
126
+ for (let i = 0; i < 25; i++) {
127
+ wikiFs.writePage(`pages/topics/topic-${i}/index.md`, makePage(`Topic ${i}`, `Topic ${i}`, "2026-05-01", `Old topic ${i} summary.`, `### 2026-05-13T10:00:00.000Z\n\nFresh fact ${i}.`));
128
+ }
129
+ indexManager.rebuildWikiIndex();
130
+ let llmCalls = 0;
131
+ const result = await consolidation.runConsolidationWithDeps(db, {
132
+ now: () => new Date("2026-05-14T22:30:03.086Z"),
133
+ synthesizeTruth: async () => {
134
+ llmCalls += 1;
135
+ return `Synthesized call ${llmCalls}`;
136
+ },
137
+ commitWikiChanges: async () => false,
138
+ });
139
+ assert.equal(result.truthRewrites, 18);
140
+ assert.equal(result.llmCallsUsed, 18);
141
+ assert.equal(llmCalls, 18);
142
+ });
143
+ //# sourceMappingURL=consolidation.test.js.map
@@ -1,3 +1,4 @@
1
+ import { normalizeWikiPath } from "./path-utils.js";
1
2
  const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
2
3
  const SUMMARY_MARKDOWN_RE = /(\*\*|__|[_*`~]|^\s*#+\s|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|^\s*>)/m;
3
4
  const FRONTMATTER_TEMPLATE = `---\ntitle: <title>\nsummary: <plain-text one-line summary, max 200 chars>\nupdated: YYYY-MM-DD\ntags: []\nrelated: []\n---`;
@@ -208,6 +209,53 @@ export function validateProjectRulesFrontmatter(content, options = {}) {
208
209
  warnings,
209
210
  };
210
211
  }
212
+ /**
213
+ * Validate frontmatter and backfill required fields that are missing.
214
+ * Required fields: title, entity_type (null default), last_updated, version.
215
+ * Returns updated content and whether any changes were made.
216
+ */
217
+ export function validateAndBackfillFrontmatter(path, content) {
218
+ const { parsed: fm } = parseWikiFrontmatter(content);
219
+ // Also handle missing-frontmatter case: inject a minimal block
220
+ if (!hasWikiFrontmatter(content)) {
221
+ const title = deriveTitleFromPath(path);
222
+ const now = new Date().toISOString();
223
+ const injected = `---\ntitle: ${title}\nlast_updated: ${now}\nversion: 1\n---\n\n${content}`;
224
+ return { content: injected, changed: true };
225
+ }
226
+ let changed = false;
227
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
228
+ if (!fmMatch)
229
+ return { content, changed: false };
230
+ const body = content.slice(fmMatch[0].length);
231
+ const fmLines = fmMatch[1].split("\n");
232
+ const fmKeys = new Set(fmLines.map((l) => l.split(":")[0].trim()).filter(Boolean));
233
+ if (!fmKeys.has("title") || !fm.title?.trim()) {
234
+ fmLines.push(`title: ${deriveTitleFromPath(path)}`);
235
+ changed = true;
236
+ }
237
+ if (!fmKeys.has("last_updated")) {
238
+ fmLines.push(`last_updated: ${new Date().toISOString()}`);
239
+ changed = true;
240
+ }
241
+ if (!fmKeys.has("version")) {
242
+ fmLines.push("version: 1");
243
+ changed = true;
244
+ }
245
+ // entity_type: only add if not already present (null default is fine — don't inject "null" string)
246
+ if (!changed)
247
+ return { content, changed: false };
248
+ const newContent = `---\n${fmLines.join("\n")}\n---\n${body}`;
249
+ return { content: newContent, changed: true };
250
+ }
251
+ function deriveTitleFromPath(path) {
252
+ const normalizedPath = normalizeWikiPath(path);
253
+ const segs = normalizedPath.split("/").filter(Boolean);
254
+ const file = segs[segs.length - 1] || normalizedPath;
255
+ const base = file.replace(/\.md$/, "");
256
+ const titleBase = base === "index" && segs.length >= 2 ? segs[segs.length - 2] : base;
257
+ return titleBase.split(/[-_]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
258
+ }
211
259
  function formatFrontmatterMessage(reason) {
212
260
  return `Wiki page frontmatter violates the required shape: ${reason}. Use:\n${FRONTMATTER_TEMPLATE}`;
213
261
  }
@@ -226,4 +226,46 @@ custom_rule: preserve-me
226
226
  },
227
227
  ]);
228
228
  });
229
+ test("validateAndBackfillFrontmatter backfills missing required fields", async () => {
230
+ const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
231
+ const result = validateAndBackfillFrontmatter("pages/topics/rust/index.md", "---\ntitle: Rust\n---\n\n# Rust\n\nContent.\n");
232
+ assert.equal(result.changed, true);
233
+ assert.match(result.content, /last_updated:/);
234
+ assert.match(result.content, /version:/);
235
+ });
236
+ test("validateAndBackfillFrontmatter passes valid frontmatter unchanged", async () => {
237
+ const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
238
+ const content = "---\ntitle: Rust\nlast_updated: 2026-05-14T00:00:00.000Z\nversion: 1\n---\n\n# Rust\n";
239
+ const result = validateAndBackfillFrontmatter("pages/topics/rust/index.md", content);
240
+ assert.equal(result.changed, false);
241
+ assert.equal(result.content, content);
242
+ });
243
+ test("validateAndBackfillFrontmatter backfills title from filename when missing", async () => {
244
+ const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
245
+ const result = validateAndBackfillFrontmatter("pages/topics/rust-async/index.md", "---\nlast_updated: 2026-05-14T00:00:00.000Z\nversion: 1\n---\n\n# Content\n");
246
+ assert.equal(result.changed, true);
247
+ assert.match(result.content, /title: Rust Async/);
248
+ });
249
+ test("validateAndBackfillFrontmatter injects frontmatter block when completely missing", async () => {
250
+ const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
251
+ const result = validateAndBackfillFrontmatter("pages/topics/new-page/index.md", "# New Page\n\nContent here.\n");
252
+ assert.equal(result.changed, true);
253
+ assert.match(result.content, /^---\n/);
254
+ assert.match(result.content, /title: /);
255
+ assert.match(result.content, /last_updated: /);
256
+ assert.match(result.content, /version: 1/);
257
+ // ISO timestamp
258
+ const match = result.content.match(/last_updated: (.+)/);
259
+ assert.ok(match, "Should have last_updated");
260
+ assert.doesNotThrow(() => new Date(match[1].trim()).toISOString(), "last_updated should be valid ISO timestamp");
261
+ });
262
+ test("validateAndBackfillFrontmatter backfilled last_updated is valid ISO timestamp", async () => {
263
+ const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
264
+ const result = validateAndBackfillFrontmatter("pages/people/ada/index.md", "---\ntitle: Ada\n---\n\n# Ada\n");
265
+ assert.equal(result.changed, true);
266
+ const match = result.content.match(/last_updated: (.+)/);
267
+ assert.ok(match, "Should have last_updated after backfill");
268
+ const ts = match[1].trim();
269
+ assert.doesNotThrow(() => new Date(ts).toISOString(), `${ts} should be valid ISO timestamp`);
270
+ });
229
271
  //# sourceMappingURL=frontmatter.test.js.map
package/dist/wiki/fs.js CHANGED
@@ -6,9 +6,15 @@ import { join, dirname, relative, resolve, sep } from "path";
6
6
  import { WIKI_DIR, WIKI_PAGES_DIR, WIKI_SOURCES_DIR } from "../paths.js";
7
7
  import { normalizeWikiPath } from "./path-utils.js";
8
8
  import { topicPathError } from "./topic-structure.js";
9
- const INDEX_PATH = join(WIKI_DIR, "index.md");
10
- const PAGES_INDEX_PATH = join(WIKI_PAGES_DIR, "index.md");
11
- const LOG_PATH = join(WIKI_PAGES_DIR, "_meta", "log.md");
9
+ function getIndexPath() {
10
+ return join(WIKI_DIR, "index.md");
11
+ }
12
+ function getPagesIndexPath() {
13
+ return join(WIKI_PAGES_DIR, "index.md");
14
+ }
15
+ function getLogPath() {
16
+ return join(WIKI_PAGES_DIR, "_meta", "log.md");
17
+ }
12
18
  /**
13
19
  * Write a file atomically: write to a temp file in the same directory, fsync,
14
20
  * then rename over the destination. Prevents partial writes on crash and
@@ -89,14 +95,17 @@ export function ensureWikiStructure() {
89
95
  const isNew = !existsSync(WIKI_DIR);
90
96
  mkdirSync(WIKI_PAGES_DIR, { recursive: true });
91
97
  mkdirSync(WIKI_SOURCES_DIR, { recursive: true });
92
- if (!existsSync(INDEX_PATH)) {
93
- writeFileAtomic(INDEX_PATH, getInitialIndex());
98
+ const indexPath = getIndexPath();
99
+ const pagesIndexPath = getPagesIndexPath();
100
+ const logPath = getLogPath();
101
+ if (!existsSync(indexPath)) {
102
+ writeFileAtomic(indexPath, getInitialIndex());
94
103
  }
95
- if (!existsSync(PAGES_INDEX_PATH)) {
96
- writeFileAtomic(PAGES_INDEX_PATH, getInitialPagesIndex());
104
+ if (!existsSync(pagesIndexPath)) {
105
+ writeFileAtomic(pagesIndexPath, getInitialPagesIndex());
97
106
  }
98
- if (!existsSync(LOG_PATH)) {
99
- writeFileAtomic(LOG_PATH, INITIAL_LOG);
107
+ if (!existsSync(logPath)) {
108
+ writeFileAtomic(logPath, INITIAL_LOG);
100
109
  }
101
110
  return isNew;
102
111
  }
@@ -161,20 +170,20 @@ export function listSources() {
161
170
  /** Read index.md raw content. */
162
171
  export function readIndexFile() {
163
172
  ensureWikiStructure();
164
- return readFileSync(INDEX_PATH, "utf-8");
173
+ return readFileSync(getIndexPath(), "utf-8");
165
174
  }
166
175
  /** Write index.md content atomically. */
167
176
  export function writeIndexFile(content) {
168
- writeFileAtomic(INDEX_PATH, content);
177
+ writeFileAtomic(getIndexPath(), content);
169
178
  }
170
179
  /** Read log.md raw content. */
171
180
  export function readLogFile() {
172
181
  ensureWikiStructure();
173
- return readFileSync(LOG_PATH, "utf-8");
182
+ return readFileSync(getLogPath(), "utf-8");
174
183
  }
175
184
  /** Write log.md content atomically. */
176
185
  export function writeLogFile(content) {
177
- writeFileAtomic(LOG_PATH, content);
186
+ writeFileAtomic(getLogPath(), content);
178
187
  }
179
188
  /** Get the full wiki directory path (for external tools that need it). */
180
189
  export function getWikiDir() {