@vortex-os/base 0.1.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/dist/index.js CHANGED
@@ -1,20 +1,22 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
1
+ import {
2
+ __export,
3
+ catchUpSessions
4
+ } from "./chunk-6SO4DAWJ.js";
6
5
 
7
6
  // ../core/dist/index.js
8
7
  var dist_exports = {};
9
8
  __export(dist_exports, {
10
9
  Privacy: () => Privacy,
11
10
  isVisibleAt: () => isVisibleAt,
11
+ loadVortexConfig: () => loadVortexConfig,
12
12
  makeContext: () => makeContext,
13
13
  maxPrivacy: () => maxPrivacy,
14
14
  moduleDir: () => moduleDir,
15
15
  normalizePrivacy: () => normalizePrivacy,
16
16
  parseFrontmatter: () => parseFrontmatter,
17
- serializeFrontmatter: () => serializeFrontmatter
17
+ resolveEnvironment: () => resolveEnvironment,
18
+ serializeFrontmatter: () => serializeFrontmatter,
19
+ vortexConfigPath: () => vortexConfigPath
18
20
  });
19
21
 
20
22
  // ../core/dist/types.js
@@ -93,6 +95,54 @@ function moduleDir(ctx, moduleName) {
93
95
  return join(ctx.modulesDir, moduleName);
94
96
  }
95
97
 
98
+ // ../core/dist/config.js
99
+ import { existsSync, readFileSync } from "fs";
100
+ import { join as join2 } from "path";
101
+ var DEFAULT_CONFIG = {
102
+ autoRecord: { sessionStart: true, worklog: true, decision: true, ambientRecall: true, archive: true },
103
+ environments: []
104
+ };
105
+ function vortexConfigPath(ctx) {
106
+ return join2(ctx.agentDir, "vortex.json");
107
+ }
108
+ function loadVortexConfig(ctx) {
109
+ const path = vortexConfigPath(ctx);
110
+ if (!existsSync(path))
111
+ return DEFAULT_CONFIG;
112
+ try {
113
+ const raw = JSON.parse(readFileSync(path, "utf8"));
114
+ return {
115
+ autoRecord: { ...DEFAULT_CONFIG.autoRecord, ...raw.autoRecord ?? {} },
116
+ environments: Array.isArray(raw.environments) ? raw.environments : []
117
+ };
118
+ } catch {
119
+ return DEFAULT_CONFIG;
120
+ }
121
+ }
122
+ function resolveEnvironment(config, signals) {
123
+ const host = signals.hostname?.toLowerCase();
124
+ const env = signals.env ?? {};
125
+ const pathExists = signals.pathExists ?? (() => false);
126
+ for (const rule of config.environments) {
127
+ if (rule.pathExists && pathExists(rule.pathExists))
128
+ return rule.label;
129
+ if (rule.hostname && host && rule.hostname.toLowerCase() === host)
130
+ return rule.label;
131
+ if (rule.envVar) {
132
+ if (typeof rule.envVar === "string") {
133
+ if (env[rule.envVar] !== void 0)
134
+ return rule.label;
135
+ } else {
136
+ const v2 = env[rule.envVar.name];
137
+ if (v2 !== void 0 && (rule.envVar.equals === void 0 || v2 === rule.envVar.equals)) {
138
+ return rule.label;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
96
146
  // ../modules/slash-commands/dist/index.js
97
147
  var dist_exports2 = {};
98
148
  __export(dist_exports2, {
@@ -188,7 +238,7 @@ var MemoryType = {
188
238
 
189
239
  // ../modules/memory-system/dist/store.js
190
240
  import { readdir, readFile, writeFile, mkdir, unlink, stat } from "fs/promises";
191
- import { join as join2, basename, extname } from "path";
241
+ import { join as join3, basename, extname } from "path";
192
242
  var MemoryStore = class {
193
243
  dir;
194
244
  constructor(dir) {
@@ -246,13 +296,13 @@ var MemoryStore = class {
246
296
  }
247
297
  /** Absolute path of a memory file (file may not exist). */
248
298
  pathFor(id) {
249
- return join2(this.dir, `${id}.md`);
299
+ return join3(this.dir, `${id}.md`);
250
300
  }
251
301
  };
252
302
 
253
303
  // ../modules/memory-system/dist/memory-index.js
254
304
  import { writeFile as writeFile2 } from "fs/promises";
255
- import { join as join3 } from "path";
305
+ import { join as join4 } from "path";
256
306
  async function writeMemoryIndex(store, options = {}) {
257
307
  const ids = await store.list();
258
308
  const lines = [];
@@ -264,7 +314,7 @@ async function writeMemoryIndex(store, options = {}) {
264
314
  const memory = await store.read(id);
265
315
  lines.push(`- [${memory.frontmatter.name}](${id}.md) \u2014 ${memory.frontmatter.description}`);
266
316
  }
267
- await writeFile2(join3(store.dir, "MEMORY.md"), `${lines.join("\n")}
317
+ await writeFile2(join4(store.dir, "MEMORY.md"), `${lines.join("\n")}
268
318
  `, "utf8");
269
319
  }
270
320
 
@@ -301,7 +351,7 @@ __export(dist_exports4, {
301
351
 
302
352
  // ../modules/data-lint/dist/runner.js
303
353
  import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
304
- import { join as join4 } from "path";
354
+ import { join as join5 } from "path";
305
355
  async function lintDirectory(options) {
306
356
  const start = Date.now();
307
357
  const extensions = options.extensions ?? [".md"];
@@ -338,7 +388,7 @@ async function collectFiles(dir, extensions) {
338
388
  throw e;
339
389
  }
340
390
  for (const entry of entries) {
341
- const full = join4(current, entry.name);
391
+ const full = join5(current, entry.name);
342
392
  if (entry.isDirectory()) {
343
393
  stack.push(full);
344
394
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -399,7 +449,7 @@ function privacyValid() {
399
449
 
400
450
  // ../modules/data-lint/dist/rules/wiki-link-resolves.js
401
451
  import { readdir as readdir3 } from "fs/promises";
402
- import { basename as basename2, extname as extname2, join as join5 } from "path";
452
+ import { basename as basename2, extname as extname2, join as join6 } from "path";
403
453
  var WIKI_LINK = /\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g;
404
454
  function wikiLinkResolves(options) {
405
455
  let cache;
@@ -420,7 +470,7 @@ function wikiLinkResolves(options) {
420
470
  continue;
421
471
  }
422
472
  for (const entry of entries) {
423
- const full = join5(current, entry.name);
473
+ const full = join6(current, entry.name);
424
474
  if (entry.isDirectory()) {
425
475
  stack.push(full);
426
476
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -465,7 +515,7 @@ __export(dist_exports5, {
465
515
 
466
516
  // ../modules/ai-coding-pitfalls/dist/catalog.js
467
517
  import { readdir as readdir4, readFile as readFile4 } from "fs/promises";
468
- import { basename as basename3, extname as extname3, join as join6 } from "path";
518
+ import { basename as basename3, extname as extname3, join as join7 } from "path";
469
519
  var PitfallCatalog = class {
470
520
  dir;
471
521
  constructor(dir) {
@@ -493,7 +543,7 @@ var PitfallCatalog = class {
493
543
  async entries() {
494
544
  try {
495
545
  const names = await readdir4(this.dir);
496
- return names.filter((n) => n.endsWith(".md")).map((n) => join6(this.dir, n));
546
+ return names.filter((n) => n.endsWith(".md")).map((n) => join7(this.dir, n));
497
547
  } catch (e) {
498
548
  if (e.code === "ENOENT")
499
549
  return [];
@@ -525,7 +575,7 @@ __export(dist_exports6, {
525
575
 
526
576
  // ../modules/tool-rules/dist/catalog.js
527
577
  import { readdir as readdir5, readFile as readFile5 } from "fs/promises";
528
- import { basename as basename4, extname as extname4, join as join7 } from "path";
578
+ import { basename as basename4, extname as extname4, join as join8 } from "path";
529
579
  var ToolRuleCatalog = class {
530
580
  dir;
531
581
  constructor(dir) {
@@ -558,7 +608,7 @@ var ToolRuleCatalog = class {
558
608
  async entries() {
559
609
  try {
560
610
  const names = await readdir5(this.dir);
561
- return names.filter((n) => n.endsWith(".md")).map((n) => join7(this.dir, n));
611
+ return names.filter((n) => n.endsWith(".md")).map((n) => join8(this.dir, n));
562
612
  } catch (e) {
563
613
  if (e.code === "ENOENT")
564
614
  return [];
@@ -1813,20 +1863,20 @@ async function renderHtml(source, options) {
1813
1863
  };
1814
1864
  }
1815
1865
 
1816
- // ../modules/til/dist/index.js
1866
+ // ../modules/worklog/dist/index.js
1817
1867
  var dist_exports8 = {};
1818
1868
  __export(dist_exports8, {
1819
- TilStore: () => TilStore,
1869
+ WorklogStore: () => WorklogStore,
1820
1870
  appendSection: () => appendSection
1821
1871
  });
1822
1872
 
1823
- // ../modules/til/dist/store.js
1873
+ // ../modules/worklog/dist/store.js
1824
1874
  import { readdir as readdir6, readFile as readFile6, stat as stat2 } from "fs/promises";
1825
- import { join as join8 } from "path";
1875
+ import { join as join9, resolve as resolve2, sep } from "path";
1826
1876
  var FILENAME_PATTERN = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
1827
1877
  var MONTH_PATTERN = /^\d{2}$/;
1828
1878
  var YEAR_PATTERN = /^\d{4}$/;
1829
- var TilStore = class {
1879
+ var WorklogStore = class {
1830
1880
  rootDir;
1831
1881
  constructor(rootDir) {
1832
1882
  this.rootDir = rootDir;
@@ -1836,17 +1886,17 @@ var TilStore = class {
1836
1886
  const years = await this.listSubdirs(this.rootDir, YEAR_PATTERN);
1837
1887
  const entries = [];
1838
1888
  for (const year of years) {
1839
- const yearDir = join8(this.rootDir, year);
1889
+ const yearDir = join9(this.rootDir, year);
1840
1890
  const months = await this.listSubdirs(yearDir, MONTH_PATTERN);
1841
1891
  for (const month of months) {
1842
- entries.push(...await this.entriesIn(join8(yearDir, month)));
1892
+ entries.push(...await this.entriesIn(join9(yearDir, month)));
1843
1893
  }
1844
1894
  }
1845
1895
  return entries.sort((a, b2) => a.date === b2.date ? a.keyword.localeCompare(b2.keyword) : a.date.localeCompare(b2.date));
1846
1896
  }
1847
1897
  /** Entries within one calendar month. */
1848
1898
  async listByMonth(year, month) {
1849
- const monthDir = join8(this.rootDir, String(year).padStart(4, "0"), String(month).padStart(2, "0"));
1899
+ const monthDir = join9(this.rootDir, String(year).padStart(4, "0"), String(month).padStart(2, "0"));
1850
1900
  const entries = await this.entriesIn(monthDir);
1851
1901
  return entries.sort((a, b2) => a.date === b2.date ? a.keyword.localeCompare(b2.keyword) : a.date.localeCompare(b2.date));
1852
1902
  }
@@ -1859,7 +1909,7 @@ var TilStore = class {
1859
1909
  const [year, month] = date.split("-");
1860
1910
  if (!year || !month)
1861
1911
  return void 0;
1862
- const monthDir = join8(this.rootDir, year, month);
1912
+ const monthDir = join9(this.rootDir, year, month);
1863
1913
  const entries = (await this.entriesIn(monthDir)).filter((e) => e.date === date).sort((a, b2) => a.keyword.localeCompare(b2.keyword));
1864
1914
  return entries[0];
1865
1915
  }
@@ -1872,11 +1922,14 @@ var TilStore = class {
1872
1922
  }
1873
1923
  /** Resolve the file path for a given (date, keyword), without creating it. */
1874
1924
  pathFor(date, keyword) {
1875
- const [year, month] = date.split("-");
1876
- if (!year || !month) {
1925
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
1877
1926
  throw new Error(`Invalid date: ${date} (expected YYYY-MM-DD)`);
1878
1927
  }
1879
- return join8(this.rootDir, year, month, `${date}-${keyword}.md`);
1928
+ const [year, month] = date.split("-");
1929
+ validateSegment("keyword", keyword);
1930
+ const abs = join9(this.rootDir, year, month, `${date}-${keyword}.md`);
1931
+ assertContained(abs, this.rootDir);
1932
+ return abs;
1880
1933
  }
1881
1934
  async listSubdirs(dir, pattern) {
1882
1935
  try {
@@ -1902,7 +1955,7 @@ var TilStore = class {
1902
1955
  const match = name.match(FILENAME_PATTERN);
1903
1956
  if (!match)
1904
1957
  continue;
1905
- const path = join8(monthDir, name);
1958
+ const path = join9(monthDir, name);
1906
1959
  try {
1907
1960
  const info = await stat2(path);
1908
1961
  if (!info.isFile())
@@ -1919,8 +1972,28 @@ var TilStore = class {
1919
1972
  return out;
1920
1973
  }
1921
1974
  };
1975
+ function validateSegment(label, value) {
1976
+ const v2 = (value ?? "").trim();
1977
+ if (v2.length === 0)
1978
+ throw new Error(`${label} is required`);
1979
+ if (v2.includes("/") || v2.includes("\\"))
1980
+ throw new Error(`${label} must not contain path separators: ${value}`);
1981
+ if (/(^|[\\/])\.\.([\\/]|$)/.test(v2) || v2 === "..")
1982
+ throw new Error(`${label} must not contain '..': ${value}`);
1983
+ if (v2.startsWith("/") || v2.startsWith("\\") || /^[a-zA-Z]:/.test(v2))
1984
+ throw new Error(`${label} must be a relative name, not an absolute path: ${value}`);
1985
+ if (/[\u0000-\u001f\u007f]/.test(v2))
1986
+ throw new Error(`${label} must not contain NUL or control characters: ${value}`);
1987
+ }
1988
+ function assertContained(abs, rootDir) {
1989
+ const root = resolve2(rootDir);
1990
+ const target = resolve2(abs);
1991
+ if (target !== root && !(target + sep).startsWith(root + sep)) {
1992
+ throw new Error(`Refusing to write outside the store directory: ${abs}`);
1993
+ }
1994
+ }
1922
1995
 
1923
- // ../modules/til/dist/append.js
1996
+ // ../modules/worklog/dist/append.js
1924
1997
  import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
1925
1998
  async function appendSection(entry, title, body) {
1926
1999
  const original = await readFile7(entry.path, "utf8");
@@ -1945,7 +2018,7 @@ __export(dist_exports9, {
1945
2018
 
1946
2019
  // ../modules/decision-log/dist/store.js
1947
2020
  import { readdir as readdir7, readFile as readFile8, stat as stat3 } from "fs/promises";
1948
- import { join as join9 } from "path";
2021
+ import { join as join10, resolve as resolve3, sep as sep2 } from "path";
1949
2022
  var FILENAME_PATTERN2 = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
1950
2023
  var DecisionStore = class {
1951
2024
  rootDir;
@@ -1967,7 +2040,7 @@ var DecisionStore = class {
1967
2040
  const match = name.match(FILENAME_PATTERN2);
1968
2041
  if (!match)
1969
2042
  continue;
1970
- const path = join9(this.rootDir, name);
2043
+ const path = join10(this.rootDir, name);
1971
2044
  try {
1972
2045
  const info = await stat3(path);
1973
2046
  if (!info.isFile())
@@ -2038,9 +2111,32 @@ var DecisionStore = class {
2038
2111
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
2039
2112
  throw new Error(`Invalid date: ${date} (expected YYYY-MM-DD)`);
2040
2113
  }
2041
- return join9(this.rootDir, `${date}-${slug}.md`);
2114
+ validateSegment2("slug", slug);
2115
+ const abs = join10(this.rootDir, `${date}-${slug}.md`);
2116
+ assertContained2(abs, this.rootDir);
2117
+ return abs;
2042
2118
  }
2043
2119
  };
2120
+ function validateSegment2(label, value) {
2121
+ const v2 = (value ?? "").trim();
2122
+ if (v2.length === 0)
2123
+ throw new Error(`${label} is required`);
2124
+ if (v2.includes("/") || v2.includes("\\"))
2125
+ throw new Error(`${label} must not contain path separators: ${value}`);
2126
+ if (/(^|[\\/])\.\.([\\/]|$)/.test(v2) || v2 === "..")
2127
+ throw new Error(`${label} must not contain '..': ${value}`);
2128
+ if (v2.startsWith("/") || v2.startsWith("\\") || /^[a-zA-Z]:/.test(v2))
2129
+ throw new Error(`${label} must be a relative name, not an absolute path: ${value}`);
2130
+ if (/[\u0000-\u001f\u007f]/.test(v2))
2131
+ throw new Error(`${label} must not contain NUL or control characters: ${value}`);
2132
+ }
2133
+ function assertContained2(abs, rootDir) {
2134
+ const root = resolve3(rootDir);
2135
+ const target = resolve3(abs);
2136
+ if (target !== root && !(target + sep2).startsWith(root + sep2)) {
2137
+ throw new Error(`Refusing to write outside the store directory: ${abs}`);
2138
+ }
2139
+ }
2044
2140
 
2045
2141
  // ../modules/decision-log/dist/template.js
2046
2142
  function renderTemplate(input) {
@@ -2102,7 +2198,7 @@ __export(dist_exports10, {
2102
2198
 
2103
2199
  // ../modules/index-generator/dist/scan.js
2104
2200
  import { readdir as readdir8, readFile as readFile9, stat as stat4 } from "fs/promises";
2105
- import { basename as basename5, extname as extname5, join as join10, relative } from "path";
2201
+ import { basename as basename5, extname as extname5, join as join11, relative } from "path";
2106
2202
  var RESERVED_FILES = /* @__PURE__ */ new Set(["README.md", "_INDEX.md"]);
2107
2203
  var H1_PATTERN = /^#\s+(.+?)\s*$/m;
2108
2204
  async function scanDirectory(rootDir, opts = {}) {
@@ -2132,7 +2228,7 @@ async function walk(rootDir, currentDir, recursive, skipFilenames, skipPrefixes,
2132
2228
  continue;
2133
2229
  if (name.startsWith(".") || name.startsWith("_"))
2134
2230
  continue;
2135
- await walk(rootDir, join10(currentDir, name), recursive, skipFilenames, skipPrefixes, out);
2231
+ await walk(rootDir, join11(currentDir, name), recursive, skipFilenames, skipPrefixes, out);
2136
2232
  continue;
2137
2233
  }
2138
2234
  if (!dirent.isFile())
@@ -2144,7 +2240,7 @@ async function walk(rootDir, currentDir, recursive, skipFilenames, skipPrefixes,
2144
2240
  const nameNoExt = basename5(name, ".md");
2145
2241
  if (skipPrefixes.some((p) => nameNoExt.startsWith(p)))
2146
2242
  continue;
2147
- const fullPath = join10(currentDir, name);
2243
+ const fullPath = join11(currentDir, name);
2148
2244
  let info;
2149
2245
  try {
2150
2246
  info = await stat4(fullPath);
@@ -2254,7 +2350,7 @@ function today() {
2254
2350
 
2255
2351
  // ../modules/index-generator/dist/nested.js
2256
2352
  import { readdir as readdir9, stat as stat5 } from "fs/promises";
2257
- import { extname as extname6, join as join11 } from "path";
2353
+ import { extname as extname6, join as join12 } from "path";
2258
2354
  async function findIndexableDirs(rootDir, options = {}) {
2259
2355
  const minEntries = options.minEntries ?? 1;
2260
2356
  const skipPrefixes = options.skipPrefixes ?? [];
@@ -2290,7 +2386,7 @@ async function walk2(dir, minEntries, skipPrefixes, out) {
2290
2386
  if (skipPrefixes.some((p) => nameNoExt.startsWith(p)))
2291
2387
  continue;
2292
2388
  try {
2293
- const info = await stat5(join11(dir, dirent.name));
2389
+ const info = await stat5(join12(dir, dirent.name));
2294
2390
  if (!info.isFile())
2295
2391
  continue;
2296
2392
  } catch {
@@ -2302,7 +2398,7 @@ async function walk2(dir, minEntries, skipPrefixes, out) {
2302
2398
  out.push(dir);
2303
2399
  }
2304
2400
  for (const name of subdirs) {
2305
- await walk2(join11(dir, name), minEntries, skipPrefixes, out);
2401
+ await walk2(join12(dir, name), minEntries, skipPrefixes, out);
2306
2402
  }
2307
2403
  }
2308
2404
 
@@ -2314,7 +2410,7 @@ __export(dist_exports11, {
2314
2410
 
2315
2411
  // ../modules/runbooks/dist/store.js
2316
2412
  import { readdir as readdir10, readFile as readFile10, stat as stat6 } from "fs/promises";
2317
- import { extname as extname7, join as join12 } from "path";
2413
+ import { extname as extname7, join as join13 } from "path";
2318
2414
  var RESERVED_FILES2 = /* @__PURE__ */ new Set(["README.md", "_INDEX.md"]);
2319
2415
  var RunbookStore = class {
2320
2416
  rootDir;
@@ -2337,7 +2433,7 @@ var RunbookStore = class {
2337
2433
  continue;
2338
2434
  if (RESERVED_FILES2.has(name))
2339
2435
  continue;
2340
- const path = join12(this.rootDir, name);
2436
+ const path = join13(this.rootDir, name);
2341
2437
  try {
2342
2438
  const info = await stat6(path);
2343
2439
  if (!info.isFile())
@@ -2446,7 +2542,7 @@ function extractWikiLinks(body) {
2446
2542
 
2447
2543
  // ../modules/link-rewriter/dist/resolve.js
2448
2544
  import { readdir as readdir11, stat as stat7 } from "fs/promises";
2449
- import { basename as basename6, dirname, extname as extname8, isAbsolute, join as join13, relative as relative2, resolve as pathResolve } from "path";
2545
+ import { basename as basename6, dirname, extname as extname8, isAbsolute, join as join14, relative as relative2, resolve as pathResolve } from "path";
2450
2546
  async function buildFileIndex(rootDir, options = {}) {
2451
2547
  const byBasename = /* @__PURE__ */ new Map();
2452
2548
  const byRelPath = /* @__PURE__ */ new Map();
@@ -2477,7 +2573,7 @@ async function walk3(rootDir, dir, byBasename, byRelPath, additionalExts) {
2477
2573
  if (dirent.isDirectory()) {
2478
2574
  if (name.startsWith("."))
2479
2575
  continue;
2480
- await walk3(rootDir, join13(dir, name), byBasename, byRelPath, additionalExts);
2576
+ await walk3(rootDir, join14(dir, name), byBasename, byRelPath, additionalExts);
2481
2577
  continue;
2482
2578
  }
2483
2579
  if (!dirent.isFile())
@@ -2486,7 +2582,7 @@ async function walk3(rootDir, dir, byBasename, byRelPath, additionalExts) {
2486
2582
  const isMd = ext === ".md";
2487
2583
  if (!isMd && !additionalExts.has(ext))
2488
2584
  continue;
2489
- const path = join13(dir, name);
2585
+ const path = join14(dir, name);
2490
2586
  try {
2491
2587
  const info = await stat7(path);
2492
2588
  if (!info.isFile())
@@ -2657,611 +2753,3655 @@ function rewriteBody(body, redirections) {
2657
2753
  return { newBody: out, fileRewrites };
2658
2754
  }
2659
2755
 
2660
- // ../plugins/session-rituals/dist/index.js
2756
+ // ../modules/proactive-curator/dist/index.js
2661
2757
  var dist_exports13 = {};
2662
2758
  __export(dist_exports13, {
2663
- createRitualRegistry: () => createRitualRegistry,
2664
- decisionCommand: () => decisionCommand,
2665
- reindexCommand: () => reindexCommand,
2666
- sessionStartCommand: () => sessionStartCommand,
2667
- tilCommand: () => tilCommand,
2668
- vortexCommand: () => vortexCommand
2759
+ AmbientBackpressure: () => AmbientBackpressure,
2760
+ AmbientRecaller: () => AmbientRecaller,
2761
+ ClaudeCodeLLMJudge: () => ClaudeCodeLLMJudge,
2762
+ ClaudeCodeLLMJudgeError: () => ClaudeCodeLLMJudgeError,
2763
+ ClaudeDesktopLLMJudge: () => ClaudeDesktopLLMJudge,
2764
+ ClaudeDesktopLLMJudgeError: () => ClaudeDesktopLLMJudgeError,
2765
+ CodexLLMJudge: () => CodexLLMJudge,
2766
+ CodexLLMJudgeError: () => CodexLLMJudgeError,
2767
+ GeminiLLMJudge: () => GeminiLLMJudge,
2768
+ GeminiLLMJudgeError: () => GeminiLLMJudgeError,
2769
+ HubProposer: () => HubProposer,
2770
+ InjectedLLMJudge: () => InjectedLLMJudge,
2771
+ InsightProposer: () => InsightProposer,
2772
+ LLMJudgeError: () => LLMJudgeError,
2773
+ TurnBuffer: () => TurnBuffer,
2774
+ computeFingerprint: () => computeFingerprint,
2775
+ deriveQueryFromTurns: () => deriveQueryFromTurns,
2776
+ frameForJudge: () => frameForJudge,
2777
+ loadDeclinedFingerprints: () => loadDeclinedFingerprints,
2778
+ parseJudgeResponse: () => parseJudgeResponse,
2779
+ recordAcceptance: () => recordAcceptance,
2780
+ recordDecline: () => recordDecline,
2781
+ resetDeclined: () => resetDeclined
2669
2782
  });
2670
2783
 
2671
- // ../plugins/session-rituals/dist/commands/decision.js
2672
- import { writeFile as writeFile5 } from "fs/promises";
2673
- import { join as join14 } from "path";
2674
- import { existsSync } from "fs";
2675
- var decisionCommand = {
2676
- name: "decision",
2677
- description: "Create a new Decision Log entry from the template.",
2678
- args: [
2679
- { name: "slug", description: "Kebab-style identifier used in the filename.", required: true },
2680
- { name: "title", description: "One-line decision title (rest of the input).", required: true }
2681
- ],
2682
- handler: async (input) => {
2683
- const slug = input.args.slug;
2684
- if (!slug) {
2685
- throw new Error("`/decision` requires a slug argument.");
2784
+ // ../modules/proactive-curator/dist/fingerprint.js
2785
+ import { createHash } from "crypto";
2786
+ function computeFingerprint(input) {
2787
+ const normalized = normalizeForHash(input);
2788
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
2789
+ }
2790
+ function normalizeForHash(input) {
2791
+ if (input.kind === "capture-insight") {
2792
+ return JSON.stringify({
2793
+ kind: "capture-insight",
2794
+ turnIds: [...input.turnIds].sort(),
2795
+ topic: normalizeTopic(input.topic),
2796
+ actionKind: input.actionKind,
2797
+ targetPath: normalizePath(input.targetPath)
2798
+ });
2799
+ }
2800
+ return JSON.stringify({
2801
+ kind: "create-hub",
2802
+ topic: normalizeTopic(input.topic),
2803
+ sourceDocs: [...input.sourceDocs].map(normalizePath).sort(),
2804
+ actionKind: input.actionKind,
2805
+ targetPath: normalizePath(input.targetPath)
2806
+ });
2807
+ }
2808
+ function normalizeTopic(t) {
2809
+ return t.trim().toLowerCase();
2810
+ }
2811
+ function normalizePath(p) {
2812
+ return p.replace(/\\/g, "/");
2813
+ }
2814
+
2815
+ // ../modules/proactive-curator/dist/decline-store.js
2816
+ import { existsSync as existsSync2 } from "fs";
2817
+ import { appendFile, mkdir as mkdir2, readFile as readFile13, writeFile as writeFile5 } from "fs/promises";
2818
+ import { join as join15 } from "path";
2819
+ var STORE_DIR = "data/_proactive-curator";
2820
+ var DECLINED_FILE = "declined.json";
2821
+ var ACCEPTED_LOG = "accepted.log";
2822
+ var DEFAULT_EXPIRY_DAYS = 30;
2823
+ async function loadDeclinedFingerprints(cwd, now) {
2824
+ const parsed = await readDeclinedFile(cwd);
2825
+ if (!parsed)
2826
+ return /* @__PURE__ */ new Set();
2827
+ const active = /* @__PURE__ */ new Set();
2828
+ const nowMs = now.getTime();
2829
+ for (const kind of ["capture-insight", "create-hub"]) {
2830
+ const entries = parsed[kind] ?? {};
2831
+ for (const [fp, entry] of Object.entries(entries)) {
2832
+ if (isActive(entry, nowMs))
2833
+ active.add(fp);
2686
2834
  }
2687
- const title = extractTitle2(input.rest, slug);
2688
- if (!title) {
2689
- throw new Error("`/decision` requires a title after the slug.");
2835
+ }
2836
+ return active;
2837
+ }
2838
+ async function recordDecline(cwd, args) {
2839
+ await ensureStoreDir(cwd);
2840
+ const existing = await readDeclinedFile(cwd) ?? emptyDeclinedFile();
2841
+ const expiryDays = args.expiryDays ?? DEFAULT_EXPIRY_DAYS;
2842
+ const expiresAt = new Date(args.now.getTime() + expiryDays * 24 * 60 * 60 * 1e3).toISOString();
2843
+ const updated = {
2844
+ "capture-insight": purgeExpired(existing["capture-insight"], args.now),
2845
+ "create-hub": purgeExpired(existing["create-hub"], args.now)
2846
+ };
2847
+ const entry = {
2848
+ declinedAt: args.now.toISOString(),
2849
+ topic: args.topic,
2850
+ expiresAt,
2851
+ actionKind: args.actionKind,
2852
+ targetPath: args.targetPath,
2853
+ ...args.sourceDocs ? { sourceDocs: args.sourceDocs } : {}
2854
+ };
2855
+ updated[args.kind] = { ...updated[args.kind], [args.fingerprint]: entry };
2856
+ await writeFile5(join15(cwd, STORE_DIR, DECLINED_FILE), JSON.stringify(updated, null, 2) + "\n", "utf8");
2857
+ }
2858
+ async function recordAcceptance(cwd, args) {
2859
+ await ensureStoreDir(cwd);
2860
+ const line = JSON.stringify({
2861
+ acceptedAt: args.now.toISOString(),
2862
+ kind: args.kind,
2863
+ fingerprint: args.fingerprint,
2864
+ topic: args.topic,
2865
+ actionKind: args.actionKind,
2866
+ writtenPath: args.writtenPath
2867
+ });
2868
+ await appendFile(join15(cwd, STORE_DIR, ACCEPTED_LOG), line + "\n", "utf8");
2869
+ }
2870
+ async function resetDeclined(cwd, kind) {
2871
+ const file = join15(cwd, STORE_DIR, DECLINED_FILE);
2872
+ if (!existsSync2(file))
2873
+ return;
2874
+ if (kind === void 0) {
2875
+ await writeFile5(file, JSON.stringify(emptyDeclinedFile(), null, 2) + "\n", "utf8");
2876
+ return;
2877
+ }
2878
+ const parsed = await readDeclinedFile(cwd) ?? emptyDeclinedFile();
2879
+ parsed[kind] = {};
2880
+ await writeFile5(file, JSON.stringify(parsed, null, 2) + "\n", "utf8");
2881
+ }
2882
+ function isActive(entry, nowMs) {
2883
+ const expiresMs = new Date(entry.expiresAt).getTime();
2884
+ return Number.isFinite(expiresMs) && expiresMs > nowMs;
2885
+ }
2886
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2887
+ function purgeExpired(entries, now) {
2888
+ if (!entries)
2889
+ return {};
2890
+ const out = /* @__PURE__ */ Object.create(null);
2891
+ const nowMs = now.getTime();
2892
+ for (const [fp, entry] of Object.entries(entries)) {
2893
+ if (DANGEROUS_KEYS.has(fp))
2894
+ continue;
2895
+ if (isActive(entry, nowMs))
2896
+ out[fp] = entry;
2897
+ }
2898
+ return out;
2899
+ }
2900
+ async function readDeclinedFile(cwd) {
2901
+ const file = join15(cwd, STORE_DIR, DECLINED_FILE);
2902
+ if (!existsSync2(file))
2903
+ return null;
2904
+ try {
2905
+ const raw = await readFile13(file, "utf8");
2906
+ const parsed = JSON.parse(raw);
2907
+ return {
2908
+ "capture-insight": parsed["capture-insight"] ?? {},
2909
+ "create-hub": parsed["create-hub"] ?? {}
2910
+ };
2911
+ } catch {
2912
+ return null;
2913
+ }
2914
+ }
2915
+ function emptyDeclinedFile() {
2916
+ return { "capture-insight": {}, "create-hub": {} };
2917
+ }
2918
+ async function ensureStoreDir(cwd) {
2919
+ await mkdir2(join15(cwd, STORE_DIR), { recursive: true });
2920
+ }
2921
+
2922
+ // ../modules/proactive-curator/dist/insight-proposer.js
2923
+ import { existsSync as existsSync3 } from "fs";
2924
+ import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile14, readdir as readdir12, writeFile as writeFile6 } from "fs/promises";
2925
+ import { dirname as dirname2, join as join16 } from "path";
2926
+ var SYSTEM_META_DIRS = /* @__PURE__ */ new Set([
2927
+ "worklog",
2928
+ "decision-log",
2929
+ "runbooks",
2930
+ "hubs",
2931
+ "_memory",
2932
+ "_templates",
2933
+ "_proactive-curator"
2934
+ ]);
2935
+ var InsightProposer = class {
2936
+ options;
2937
+ kind = "capture-insight";
2938
+ constructor(options) {
2939
+ this.options = options;
2940
+ }
2941
+ async evaluate(input, ctx) {
2942
+ if (input.accumulatedTokens < this.options.minTokensAccumulated)
2943
+ return null;
2944
+ if (input.recentTurns.length < this.options.minTurnsBeforeFirstCheck)
2945
+ return null;
2946
+ const capture = await askCaptureDecision(ctx.llm, input.recentTurns);
2947
+ if (capture.verdict === "no" || !capture.summary || !capture.topic)
2948
+ return null;
2949
+ const tree = await scanTopicTree(ctx.cwd, this.options.maxTreeEntries ?? 100);
2950
+ const placement = await askPlacementDecision(ctx.llm, capture.summary, capture.topic, tree);
2951
+ if (!placement)
2952
+ return null;
2953
+ const action = buildAction(placement, capture.summary, capture.topic, ctx.now);
2954
+ if (!action)
2955
+ return null;
2956
+ const targetPath = targetPathFromAction(action);
2957
+ const fingerprint = computeFingerprint({
2958
+ kind: "capture-insight",
2959
+ turnIds: input.recentTurns.map((t) => t.turnId),
2960
+ topic: capture.topic,
2961
+ actionKind: action.kind,
2962
+ targetPath
2963
+ });
2964
+ if (ctx.declinedFingerprints.has(fingerprint))
2965
+ return null;
2966
+ const sourceRefs = input.recentTurns.map((t) => t.turnId);
2967
+ const declineExpiryDays = this.options.declineExpiryDays;
2968
+ return {
2969
+ kind: "capture-insight",
2970
+ fingerprint,
2971
+ action,
2972
+ preview: capture.summary,
2973
+ rationale: placement.rationale,
2974
+ sourceRefs,
2975
+ onAccept: async () => {
2976
+ const writtenPath = await applyAction(ctx.cwd, action);
2977
+ await recordAcceptance(ctx.cwd, {
2978
+ kind: "capture-insight",
2979
+ fingerprint,
2980
+ topic: capture.topic,
2981
+ actionKind: action.kind,
2982
+ writtenPath,
2983
+ now: ctx.now
2984
+ });
2985
+ return {
2986
+ writtenPath,
2987
+ actionApplied: action.kind,
2988
+ nextActionHint: nextActionHintFor(action)
2989
+ };
2990
+ },
2991
+ onDecline: async () => {
2992
+ await recordDecline(ctx.cwd, {
2993
+ kind: "capture-insight",
2994
+ fingerprint,
2995
+ topic: capture.topic,
2996
+ actionKind: action.kind,
2997
+ targetPath,
2998
+ now: ctx.now,
2999
+ ...declineExpiryDays !== void 0 ? { expiryDays: declineExpiryDays } : {}
3000
+ });
3001
+ }
3002
+ };
3003
+ }
3004
+ };
3005
+ async function askCaptureDecision(llm, turns) {
3006
+ const prompt = renderCapturePrompt(turns);
3007
+ const expected = {
3008
+ shape: "json",
3009
+ schema: {
3010
+ verdict: "yes|no",
3011
+ summary: "string (required when verdict=yes)",
3012
+ topic: "string (required when verdict=yes, 1-3 words)"
2690
3013
  }
2691
- const date = todayIso();
2692
- const dir = join14(input.context.dataDir, "decision-log");
2693
- const store = new DecisionStore(dir);
2694
- const path = store.pathFor(date, slug);
2695
- if (existsSync(path)) {
2696
- throw new Error(`Refusing to overwrite existing entry: ${path}`);
3014
+ };
3015
+ const raw = await llm.ask(prompt, expected);
3016
+ return normalizeCaptureDecision(raw);
3017
+ }
3018
+ function renderCapturePrompt(turns) {
3019
+ const transcript = turns.map((t) => `[${t.role}] ${t.content}`).join("\n\n");
3020
+ return [
3021
+ "You are reviewing a conversation snippet to decide whether anything in it deserves to be captured as a standalone note.",
3022
+ "",
3023
+ "Capture only if the recent turns produced an insight, decision, or piece of structured thought that would be lost if the session ended right now. Routine status updates, command output, or trivial back-and-forth do not qualify.",
3024
+ "",
3025
+ "Return JSON:",
3026
+ ` { "verdict": "yes" | "no", "summary": "3-5 sentences if verdict=yes", "topic": "1-3 words if verdict=yes" }`,
3027
+ "",
3028
+ "Recent turns:",
3029
+ transcript
3030
+ ].join("\n");
3031
+ }
3032
+ function normalizeCaptureDecision(raw) {
3033
+ if (typeof raw !== "object" || raw === null)
3034
+ return { verdict: "no" };
3035
+ const obj = raw;
3036
+ if (obj.verdict !== "yes")
3037
+ return { verdict: "no" };
3038
+ const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
3039
+ const topic = typeof obj.topic === "string" ? obj.topic.trim() : "";
3040
+ if (!summary || !topic)
3041
+ return { verdict: "no" };
3042
+ return { verdict: "yes", summary, topic };
3043
+ }
3044
+ async function askPlacementDecision(llm, summary, topic, tree) {
3045
+ const prompt = renderPlacementPrompt(summary, topic, tree);
3046
+ const expected = {
3047
+ shape: "json",
3048
+ schema: {
3049
+ actionKind: "create-folder|create-file|append-section|update-file",
3050
+ rationale: "string (one line)",
3051
+ folderPath: "string (create-folder, create-file)",
3052
+ filename: "string (create-folder, create-file)",
3053
+ filePath: "string (append-section, update-file)",
3054
+ sectionHeader: "string (append-section)",
3055
+ reason: "string (update-file)"
2697
3056
  }
2698
- const body = renderTemplate({ date, slug, title });
2699
- await writeFile5(path, body, "utf8");
2700
- return { path, date, slug };
3057
+ };
3058
+ const raw = await llm.ask(prompt, expected);
3059
+ return normalizePlacementDecision(raw);
3060
+ }
3061
+ function renderPlacementPrompt(summary, topic, tree) {
3062
+ const treeText = tree.folders.length === 0 ? "(no existing topic folders)" : tree.folders.map((f) => {
3063
+ const fileLines = f.files.map((file) => ` - ${file.filename}${file.frontmatterTopic ? ` [topic: ${file.frontmatterTopic}]` : ""}`).join("\n");
3064
+ return ` ${f.path}/
3065
+ ${fileLines}`;
3066
+ }).join("\n");
3067
+ const truncationNote = tree.truncated ? "\n\n(Tree snapshot truncated \u2014 additional folders exist but are omitted for brevity.)" : "";
3068
+ return [
3069
+ "You are deciding where to place a captured insight in the user's existing topic tree.",
3070
+ "",
3071
+ `Insight topic: ${topic}`,
3072
+ `Insight summary: ${summary}`,
3073
+ "",
3074
+ "Existing topic tree (data-relative paths, system-meta dirs excluded):",
3075
+ treeText + truncationNote,
3076
+ "",
3077
+ "Pick one placement action. Preference order \u2014 extend existing structure before adding new:",
3078
+ " 1. append-section \u2014 an existing file is the right home; add a `## <sectionHeader>` section to it.",
3079
+ " 2. create-file \u2014 a topic folder exists; add a new sibling file alongside its current contents.",
3080
+ " 3. create-folder \u2014 the topic is genuinely new; create a folder and seed it with the first file.",
3081
+ " 4. update-file \u2014 an existing file needs in-place revision (rare; prefer append).",
3082
+ "",
3083
+ "Return JSON:",
3084
+ ` { "actionKind": "append-section", "rationale": "one line why", "filePath": "<data-relative path>", "sectionHeader": "<section title without leading ##>" }`,
3085
+ ` { "actionKind": "create-file", "rationale": "one line why", "folderPath": "<data-relative folder>", "filename": "<slug>.md" }`,
3086
+ ` { "actionKind": "create-folder", "rationale": "one line why", "folderPath": "<data-relative folder>", "filename": "<slug>.md" }`,
3087
+ ` { "actionKind": "update-file", "rationale": "one line why", "filePath": "<data-relative path>", "reason": "what needs revising" }`,
3088
+ "",
3089
+ "Paths must be relative to data/. Do not propose paths inside system-meta directories (worklog/, decision-log/, runbooks/, hubs/, _memory/, _templates/, _proactive-curator/, or any other _* directory)."
3090
+ ].join("\n");
3091
+ }
3092
+ function normalizePlacementDecision(raw) {
3093
+ if (typeof raw !== "object" || raw === null)
3094
+ return null;
3095
+ const obj = raw;
3096
+ const actionKind = String(obj.actionKind ?? "");
3097
+ const rationale = typeof obj.rationale === "string" ? obj.rationale.trim() : "";
3098
+ if (!rationale)
3099
+ return null;
3100
+ switch (actionKind) {
3101
+ case "create-folder":
3102
+ case "create-file": {
3103
+ const folderPath = typeof obj.folderPath === "string" ? obj.folderPath.trim() : "";
3104
+ const filename = typeof obj.filename === "string" ? obj.filename.trim() : "";
3105
+ if (!folderPath || !filename)
3106
+ return null;
3107
+ if (isSystemMetaPath(folderPath))
3108
+ return null;
3109
+ return { actionKind, rationale, folderPath, filename };
3110
+ }
3111
+ case "append-section": {
3112
+ const filePath = typeof obj.filePath === "string" ? obj.filePath.trim() : "";
3113
+ const sectionHeader = typeof obj.sectionHeader === "string" ? obj.sectionHeader.trim() : "";
3114
+ if (!filePath || !sectionHeader)
3115
+ return null;
3116
+ if (isSystemMetaPath(filePath))
3117
+ return null;
3118
+ return { actionKind: "append-section", rationale, filePath, sectionHeader };
3119
+ }
3120
+ case "update-file": {
3121
+ const filePath = typeof obj.filePath === "string" ? obj.filePath.trim() : "";
3122
+ const reason = typeof obj.reason === "string" ? obj.reason.trim() : "";
3123
+ if (!filePath || !reason)
3124
+ return null;
3125
+ if (isSystemMetaPath(filePath))
3126
+ return null;
3127
+ return { actionKind: "update-file", rationale, filePath, reason };
3128
+ }
3129
+ default:
3130
+ return null;
2701
3131
  }
2702
- };
2703
- function extractTitle2(rest, slug) {
2704
- const trimmed = rest.trim();
2705
- if (trimmed.startsWith(slug)) {
2706
- return trimmed.slice(slug.length).trim();
3132
+ }
3133
+ function isSystemMetaPath(p) {
3134
+ const normalized = p.replace(/\\/g, "/").replace(/^\/+/, "");
3135
+ const first = normalized.split("/")[0] ?? "";
3136
+ if (SYSTEM_META_DIRS.has(first))
3137
+ return true;
3138
+ if (first.startsWith("_"))
3139
+ return true;
3140
+ return false;
3141
+ }
3142
+ function buildAction(placement, summary, topic, now) {
3143
+ const body = renderCaptureBody(summary, topic, now);
3144
+ switch (placement.actionKind) {
3145
+ case "create-folder":
3146
+ case "create-file":
3147
+ if (!placement.folderPath || !placement.filename)
3148
+ return null;
3149
+ return {
3150
+ kind: placement.actionKind,
3151
+ folderPath: placement.folderPath,
3152
+ filename: placement.filename,
3153
+ body
3154
+ };
3155
+ case "append-section":
3156
+ if (!placement.filePath || !placement.sectionHeader)
3157
+ return null;
3158
+ return {
3159
+ kind: "append-section",
3160
+ filePath: placement.filePath,
3161
+ sectionHeader: placement.sectionHeader,
3162
+ body: renderSectionBody(summary, now)
3163
+ };
3164
+ case "update-file":
3165
+ if (!placement.filePath || !placement.reason)
3166
+ return null;
3167
+ return {
3168
+ kind: "update-file",
3169
+ filePath: placement.filePath,
3170
+ body: renderUpdateBody(summary, topic, placement.reason, now),
3171
+ reason: placement.reason
3172
+ };
2707
3173
  }
2708
- return trimmed;
2709
3174
  }
2710
- function todayIso() {
2711
- const d2 = /* @__PURE__ */ new Date();
3175
+ function renderCaptureBody(summary, topic, now) {
3176
+ const today2 = formatYmd(now);
3177
+ return `---
3178
+ type: note
3179
+ topic: ${topic}
3180
+ created: ${today2}
3181
+ updated: ${today2}
3182
+ tags: [note, proactive-curator]
3183
+ ---
3184
+
3185
+ # ${topic}
3186
+
3187
+ > Captured by proactive-curator on ${today2}. The user accepted this proposal; edit freely.
3188
+
3189
+ ${summary}
3190
+ `;
3191
+ }
3192
+ function renderSectionBody(summary, now) {
3193
+ const ymd = formatYmd(now);
3194
+ return `
3195
+ _Appended ${ymd} by proactive-curator._
3196
+
3197
+ ${summary}
3198
+ `;
3199
+ }
3200
+ function renderUpdateBody(summary, topic, reason, now) {
3201
+ const ymd = formatYmd(now);
3202
+ return `---
3203
+ type: note
3204
+ topic: ${topic}
3205
+ updated: ${ymd}
3206
+ ---
3207
+
3208
+ # ${topic}
3209
+
3210
+ > Updated ${ymd} by proactive-curator. Reason: ${reason}
3211
+
3212
+ ${summary}
3213
+ `;
3214
+ }
3215
+ function formatYmd(d2) {
2712
3216
  const y2 = d2.getFullYear();
2713
3217
  const m2 = String(d2.getMonth() + 1).padStart(2, "0");
2714
3218
  const day = String(d2.getDate()).padStart(2, "0");
2715
3219
  return `${y2}-${m2}-${day}`;
2716
3220
  }
2717
-
2718
- // ../plugins/session-rituals/dist/commands/reindex.js
2719
- import { existsSync as existsSync2 } from "fs";
2720
- import { readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
2721
- import { join as join15 } from "path";
2722
- var TARGETS = [
2723
- {
2724
- dir: "_memory",
2725
- title: "Memory",
2726
- description: "\uC138\uC158 \uAC04 \uACF5\uC720\uB418\uB294 \uC601\uC18D \uBA54\uBAA8\uB9AC \uD56D\uBAA9\uB4E4. MEMORY.md\uB294 \uBCF8 \uB514\uB809\uD1A0\uB9AC\uC758 \uC6D0\uBCF8 \uC778\uB371\uC2A4\uC774\uBA70, \uBCF8 _INDEX.md\uB294 \uC0AC\uB78C\xB7Obsidian \uCE5C\uD654 \uC591\uC2DD\uC758 \uD30C\uC0DD\uBB3C\uC785\uB2C8\uB2E4.",
2727
- privacy: "internal",
2728
- recursive: false,
2729
- skipPrefixes: [],
2730
- skipFilenames: ["MEMORY.md"]
2731
- },
2732
- {
2733
- dir: "til",
2734
- title: "TIL",
2735
- description: "\uB0A0\uC9DC\uBCC4 \uC791\uC5C5 \uAE30\uB85D. `YYYY/MM/YYYY-MM-DD-keyword.md` \uAD6C\uC870.",
2736
- privacy: "internal",
2737
- recursive: true,
2738
- skipPrefixes: [],
2739
- skipFilenames: []
2740
- },
2741
- {
2742
- dir: "decision-log",
2743
- title: "Decision Log",
2744
- description: "\uAC1C\uC778 \uC758\uC0AC\uACB0\uC815 \uAE30\uB85D \u2014 \uC65C \uADF8\uAC78 \uACE8\uB790\uB294\uC9C0\uB97C \uB0A8\uAE41\uB2C8\uB2E4.",
2745
- privacy: "personal",
2746
- recursive: false,
2747
- skipPrefixes: ["_TEMPLATE"],
2748
- skipFilenames: []
2749
- },
2750
- {
2751
- dir: "runbooks",
2752
- title: "Runbooks",
2753
- description: "\uC7A5\uC560 \uB300\uC751\xB7\uC815\uAE30 \uC815\uBE44 \uC808\uCC28. `last_tested`\uB85C \uAC31\uC2E0 \uAE30\uD55C \uCD94\uC801.",
2754
- privacy: "internal",
2755
- recursive: false,
2756
- skipPrefixes: [],
2757
- skipFilenames: []
2758
- },
2759
- {
2760
- dir: "hubs",
2761
- title: "Hubs",
2762
- description: "\uC8FC\uC81C\uBCC4 \uC9C4\uC785\uC810 \u2014 \uAD00\uB828 \uC790\uB8CC\uB97C \uD55C \uACF3\uC5D0\uC11C \uBB36\uB294 \uC778\uB371\uC2A4 \uD398\uC774\uC9C0 \uBAA8\uC74C.",
2763
- privacy: "internal",
2764
- recursive: false,
2765
- skipPrefixes: [],
2766
- skipFilenames: []
2767
- },
2768
- {
2769
- dir: "projects",
2770
- title: "Projects",
2771
- description: "\uC9C4\uD589 \uC911\xB7\uC644\uB8CC \uD504\uB85C\uC81D\uD2B8 \uBAA8\uC74C (Work\xB7Home). \uBCF8 _INDEX\uB294 \uD3C9\uD0C4 \uBAA9\uB85D\uC774\uBA70, \uD558\uC704 \uD504\uB85C\uC81D\uD2B8 \uD3F4\uB354\uBCC4 _INDEX\uAC00 \uB354 \uC720\uC6A9\uD560 \uC218 \uC788\uC74C (\uB2E4\uC74C \uC0AC\uC774\uD074 \uAC80\uD1A0).",
2772
- privacy: "internal",
2773
- recursive: true,
2774
- skipPrefixes: [],
2775
- skipFilenames: []
2776
- },
2777
- {
2778
- dir: "reference",
2779
- title: "Reference",
2780
- description: "AI \uADDC\uCE59\xB7\uAE30\uC220 \uCE58\uD2B8\uC2DC\uD2B8\xB7\uC7A5\uBE44\xB7\uC2A4\uD0AC \uB4F1 \uB808\uD37C\uB7F0\uC2A4 \uC790\uB8CC (AI-Rules\xB7Games\xB7Gear\xB7Infra\xB7Org\xB7Skills\xB7Tech).",
2781
- privacy: "internal",
2782
- recursive: true,
2783
- skipPrefixes: [],
2784
- skipFilenames: []
2785
- },
2786
- {
2787
- dir: "reports",
2788
- title: "Reports",
2789
- description: "\uC815\uAE30 \uAC74\uAC15\uAC80\uC9C4 \uB9AC\uD3EC\uD2B8 (Service-Health\xB7Homelab-Health). \uC2DC\uC810 \uC2A4\uB0C5\uC0F7 \uB204\uC801.",
2790
- privacy: "internal",
2791
- recursive: true,
2792
- skipPrefixes: [],
2793
- skipFilenames: []
2794
- },
2795
- {
2796
- dir: "inbox",
2797
- title: "Inbox",
2798
- description: "\uBBF8\uBD84\uB958 \uC784\uC2DC \uBA54\uBAA8\xB7\uC544\uC774\uB514\uC5B4\xB7\uBC31\uB85C\uADF8. \uC815\uC2DD \uBD84\uB958 \uC2DC \uD574\uB2F9 \uCE74\uD14C\uACE0\uB9AC\uB85C \uC774\uB3D9.",
2799
- privacy: "internal",
2800
- recursive: true,
2801
- skipPrefixes: [],
2802
- skipFilenames: []
2803
- },
2804
- {
2805
- dir: "_templates",
2806
- title: "Templates",
2807
- description: "\uBCF4\uACE0\uC6A9\xB7\uACF5\uC720\uC6A9 \uC790\uB8CC \uD15C\uD50C\uB9BF \uBAA8\uC74C (Sharing \uB4F1).",
2808
- privacy: "internal",
2809
- recursive: true,
2810
- skipPrefixes: [],
2811
- skipFilenames: []
3221
+ function targetPathFromAction(action) {
3222
+ if (action.kind === "create-folder" || action.kind === "create-file") {
3223
+ return joinDataPath(action.folderPath, action.filename);
2812
3224
  }
2813
- ];
2814
- var reindexCommand = {
2815
- name: "reindex",
2816
- description: "Regenerate _INDEX.md for one or all data directories (memory, til, decision-log).",
2817
- args: [
2818
- {
2819
- name: "dir",
2820
- description: "Optional directory name. Omit to reindex all targets."
3225
+ return joinDataPath(action.filePath);
3226
+ }
3227
+ function joinDataPath(...parts) {
3228
+ return parts.filter((p) => p.length > 0).map((p) => p.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")).join("/");
3229
+ }
3230
+ async function applyAction(cwd, action) {
3231
+ const dataDir = join16(cwd, "data");
3232
+ switch (action.kind) {
3233
+ case "create-folder":
3234
+ case "create-file": {
3235
+ const folder = join16(dataDir, action.folderPath);
3236
+ await mkdir3(folder, { recursive: true });
3237
+ const file = join16(folder, action.filename);
3238
+ await writeFile6(file, action.body, "utf8");
3239
+ return file;
2821
3240
  }
2822
- ],
2823
- handler: async (input) => {
2824
- const requested = input.args.dir;
2825
- const targets = requested ? TARGETS.filter((t) => t.dir === requested) : TARGETS;
2826
- if (requested && targets.length === 0) {
2827
- throw new Error(`Unknown reindex target: "${requested}". Known: ${TARGETS.map((t) => t.dir).join(", ")}`);
3241
+ case "append-section": {
3242
+ const file = join16(dataDir, action.filePath);
3243
+ await mkdir3(dirname2(file), { recursive: true });
3244
+ const existing = existsSync3(file) ? await readFile14(file, "utf8") : "";
3245
+ const newline = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3246
+ const section = `${newline}
3247
+ ## ${action.sectionHeader}
3248
+ ${action.body}`;
3249
+ await appendFile2(file, section, "utf8");
3250
+ return file;
2828
3251
  }
2829
- const results = [];
2830
- for (const t of targets) {
2831
- const dir = join15(input.context.dataDir, t.dir);
2832
- if (!existsSync2(dir)) {
2833
- results.push({ dir: t.dir, status: "missing", entries: 0, bytes: 0 });
2834
- continue;
3252
+ case "update-file": {
3253
+ const file = join16(dataDir, action.filePath);
3254
+ await mkdir3(dirname2(file), { recursive: true });
3255
+ await writeFile6(file, action.body, "utf8");
3256
+ return file;
3257
+ }
3258
+ }
3259
+ }
3260
+ function nextActionHintFor(action) {
3261
+ if (action.kind === "append-section") {
3262
+ return `Section appended to ${action.filePath}. Open it to see the full thread of this topic.`;
3263
+ }
3264
+ if (action.kind === "update-file") {
3265
+ return `File updated at ${action.filePath}. The original content was replaced; check git history if you need to recover it.`;
3266
+ }
3267
+ return `New file at ${action.folderPath}/${action.filename}. Add cross-links from related docs as the topic grows.`;
3268
+ }
3269
+ async function scanTopicTree(cwd, maxEntries) {
3270
+ const dataDir = join16(cwd, "data");
3271
+ if (!existsSync3(dataDir)) {
3272
+ return { folders: [], truncated: false };
3273
+ }
3274
+ const folders = [];
3275
+ let counted = 0;
3276
+ let truncated = false;
3277
+ async function visit(absDir, relDir) {
3278
+ if (counted >= maxEntries) {
3279
+ truncated = true;
3280
+ return;
3281
+ }
3282
+ let entries;
3283
+ try {
3284
+ entries = await readdir12(absDir, { withFileTypes: true });
3285
+ } catch {
3286
+ return;
3287
+ }
3288
+ const files = [];
3289
+ const subdirs = [];
3290
+ for (const e of entries) {
3291
+ if (e.isDirectory()) {
3292
+ if (isReservedDir(e.name, relDir === ""))
3293
+ continue;
3294
+ subdirs.push(e.name);
3295
+ } else if (e.isFile() && e.name.endsWith(".md")) {
3296
+ if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md")
3297
+ continue;
3298
+ const filePath = join16(absDir, e.name);
3299
+ let frontmatterTopic;
3300
+ let tags;
3301
+ try {
3302
+ const raw = await readFile14(filePath, "utf8");
3303
+ const parsed = parseFrontmatter(raw);
3304
+ if (typeof parsed.frontmatter.topic === "string") {
3305
+ frontmatterTopic = parsed.frontmatter.topic;
3306
+ }
3307
+ if (Array.isArray(parsed.frontmatter.tags)) {
3308
+ tags = parsed.frontmatter.tags.filter((t) => typeof t === "string");
3309
+ }
3310
+ } catch {
3311
+ }
3312
+ files.push({
3313
+ path: joinDataPath(relDir, e.name),
3314
+ filename: e.name,
3315
+ ...frontmatterTopic ? { frontmatterTopic } : {},
3316
+ ...tags ? { tags } : {}
3317
+ });
2835
3318
  }
2836
- const entries = await scanDirectory(dir, {
2837
- recursive: t.recursive,
2838
- skipPrefixes: t.skipPrefixes,
2839
- skipFilenames: t.skipFilenames
2840
- });
2841
- const body = renderIndex({
2842
- title: t.title,
2843
- description: t.description,
2844
- entries,
2845
- privacy: t.privacy
2846
- });
2847
- const target = join15(dir, "_INDEX.md");
2848
- let existing;
2849
- try {
2850
- existing = await readFile13(target, "utf8");
2851
- } catch {
2852
- existing = void 0;
3319
+ }
3320
+ if (files.length > 0 || subdirs.length === 0) {
3321
+ if (relDir !== "") {
3322
+ folders.push({ path: relDir, files });
3323
+ counted += 1 + files.length;
3324
+ } else if (files.length > 0) {
3325
+ folders.push({ path: ".", files });
3326
+ counted += 1 + files.length;
2853
3327
  }
2854
- if (existing === body) {
2855
- results.push({
2856
- dir: t.dir,
2857
- status: "unchanged",
2858
- entries: entries.length,
2859
- bytes: body.length
3328
+ }
3329
+ for (const d2 of subdirs) {
3330
+ if (counted >= maxEntries) {
3331
+ truncated = true;
3332
+ return;
3333
+ }
3334
+ const childRel = joinDataPath(relDir, d2);
3335
+ await visit(join16(absDir, d2), childRel);
3336
+ }
3337
+ }
3338
+ await visit(dataDir, "");
3339
+ return { folders, truncated };
3340
+ }
3341
+ function isReservedDir(name, atRoot) {
3342
+ if (atRoot && SYSTEM_META_DIRS.has(name))
3343
+ return true;
3344
+ if (name.startsWith("."))
3345
+ return true;
3346
+ if (atRoot && name.startsWith("_"))
3347
+ return true;
3348
+ return false;
3349
+ }
3350
+
3351
+ // ../modules/proactive-curator/dist/hub-proposer.js
3352
+ import { existsSync as existsSync4 } from "fs";
3353
+ import { mkdir as mkdir4, readFile as readFile15, readdir as readdir13, writeFile as writeFile7 } from "fs/promises";
3354
+ import { dirname as dirname3, join as join17 } from "path";
3355
+ var DEFAULT_CATEGORIES = [
3356
+ "worklog",
3357
+ "decision-log",
3358
+ "runbooks"
3359
+ ];
3360
+ var HUB_DIR = "hubs";
3361
+ var MIN_KEYWORD_LENGTH = 3;
3362
+ var MAX_LLM_DOC_LIST = 30;
3363
+ var HubProposer = class {
3364
+ options;
3365
+ kind = "create-hub";
3366
+ constructor(options) {
3367
+ this.options = options;
3368
+ }
3369
+ async evaluate(input, ctx) {
3370
+ const categories = input.hint?.onlyCategories ?? this.options.scanCategories ?? DEFAULT_CATEGORIES;
3371
+ const docs = await scanDocs(ctx.cwd, categories);
3372
+ if (docs.length === 0)
3373
+ return null;
3374
+ const clusters = groupByTopic(docs);
3375
+ const candidate = pickCluster(clusters, this.options.weakSignalAt, ctx.cwd);
3376
+ if (!candidate)
3377
+ return null;
3378
+ const verdict = await askCoherence(ctx.llm, candidate, candidate.docs.length >= this.options.strongSignalAt);
3379
+ if (!verdict)
3380
+ return null;
3381
+ const slug = candidate.topic;
3382
+ const filename = `_HUB-${slug}.md`;
3383
+ const action = {
3384
+ kind: "create-file",
3385
+ folderPath: HUB_DIR,
3386
+ filename,
3387
+ body: renderHubBody(candidate, verdict.contextParagraph, ctx.now)
3388
+ };
3389
+ const targetPath = `${HUB_DIR}/${filename}`;
3390
+ const sourceDocs = candidate.docs.map((d2) => d2.path);
3391
+ const fingerprint = computeFingerprint({
3392
+ kind: "create-hub",
3393
+ topic: candidate.topic,
3394
+ sourceDocs,
3395
+ actionKind: action.kind,
3396
+ targetPath
3397
+ });
3398
+ if (ctx.declinedFingerprints.has(fingerprint))
3399
+ return null;
3400
+ const rationale = candidate.docs.length >= this.options.strongSignalAt ? `${candidate.docs.length} docs about "${candidate.topic}" \u2014 strong-signal cluster (above ${this.options.strongSignalAt}); LLM concurred.` : `${candidate.docs.length} docs about "${candidate.topic}" \u2014 weak-signal cluster (\u2265 ${this.options.weakSignalAt}); LLM affirmed cohesion.`;
3401
+ const declineExpiryDays = this.options.declineExpiryDays;
3402
+ return {
3403
+ kind: "create-hub",
3404
+ fingerprint,
3405
+ action,
3406
+ preview: renderPreview(candidate, verdict.contextParagraph),
3407
+ rationale,
3408
+ sourceRefs: sourceDocs,
3409
+ onAccept: async () => {
3410
+ const writtenPath = await applyHubCreate(ctx.cwd, action);
3411
+ await recordAcceptance(ctx.cwd, {
3412
+ kind: "create-hub",
3413
+ fingerprint,
3414
+ topic: candidate.topic,
3415
+ actionKind: action.kind,
3416
+ writtenPath,
3417
+ now: ctx.now
3418
+ });
3419
+ return {
3420
+ writtenPath,
3421
+ actionApplied: action.kind,
3422
+ nextActionHint: `Hub created at hubs/${filename}. Open it to review the cross-links; the source docs were not modified.`
3423
+ };
3424
+ },
3425
+ onDecline: async () => {
3426
+ await recordDecline(ctx.cwd, {
3427
+ kind: "create-hub",
3428
+ fingerprint,
3429
+ topic: candidate.topic,
3430
+ actionKind: action.kind,
3431
+ targetPath,
3432
+ sourceDocs,
3433
+ now: ctx.now,
3434
+ ...declineExpiryDays !== void 0 ? { expiryDays: declineExpiryDays } : {}
2860
3435
  });
3436
+ }
3437
+ };
3438
+ }
3439
+ };
3440
+ async function scanDocs(cwd, categories) {
3441
+ const dataDir = join17(cwd, "data");
3442
+ if (!existsSync4(dataDir))
3443
+ return [];
3444
+ const out = [];
3445
+ for (const category of categories) {
3446
+ const abs = join17(dataDir, category);
3447
+ if (!existsSync4(abs))
3448
+ continue;
3449
+ await walk4(abs, category, out);
3450
+ }
3451
+ return out;
3452
+ }
3453
+ async function walk4(absDir, relPath, acc) {
3454
+ let entries;
3455
+ try {
3456
+ entries = await readdir13(absDir, { withFileTypes: true });
3457
+ } catch {
3458
+ return;
3459
+ }
3460
+ for (const e of entries) {
3461
+ if (e.isDirectory()) {
3462
+ if (e.name.startsWith(".") || e.name.startsWith("_"))
2861
3463
  continue;
3464
+ await walk4(join17(absDir, e.name), `${relPath}/${e.name}`, acc);
3465
+ } else if (e.isFile() && e.name.endsWith(".md")) {
3466
+ if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md")
3467
+ continue;
3468
+ if (e.name.startsWith("_TEMPLATE"))
3469
+ continue;
3470
+ const filePath = join17(absDir, e.name);
3471
+ let frontmatterTopic;
3472
+ let tags = [];
3473
+ try {
3474
+ const raw = await readFile15(filePath, "utf8");
3475
+ const parsed = parseFrontmatter(raw);
3476
+ if (typeof parsed.frontmatter.topic === "string") {
3477
+ frontmatterTopic = parsed.frontmatter.topic.trim();
3478
+ }
3479
+ if (Array.isArray(parsed.frontmatter.tags)) {
3480
+ tags = parsed.frontmatter.tags.filter((t) => typeof t === "string");
3481
+ }
3482
+ } catch {
2862
3483
  }
2863
- await writeFile6(target, body, "utf8");
2864
- results.push({
2865
- dir: t.dir,
2866
- status: "written",
2867
- entries: entries.length,
2868
- bytes: body.length
3484
+ acc.push({
3485
+ path: `${relPath}/${e.name}`,
3486
+ filename: e.name,
3487
+ ...frontmatterTopic ? { frontmatterTopic } : {},
3488
+ tags,
3489
+ filenameKeywords: extractFilenameKeywords(e.name)
2869
3490
  });
2870
3491
  }
2871
- return results;
3492
+ }
3493
+ }
3494
+ function extractFilenameKeywords(filename) {
3495
+ const stem = filename.replace(/\.md$/i, "");
3496
+ const withoutDate = stem.replace(/^\d{4}-\d{2}-\d{2}-/, "");
3497
+ return withoutDate.split(/[-_\s]+/).map((s) => s.toLowerCase()).filter((s) => s.length >= MIN_KEYWORD_LENGTH);
3498
+ }
3499
+ function groupByTopic(docs) {
3500
+ const buckets = /* @__PURE__ */ new Map();
3501
+ for (const doc of docs) {
3502
+ const topics = candidateTopicsFor(doc);
3503
+ for (const topic of topics) {
3504
+ const list = buckets.get(topic);
3505
+ if (list) {
3506
+ if (!list.some((d2) => d2.path === doc.path))
3507
+ list.push(doc);
3508
+ } else {
3509
+ buckets.set(topic, [doc]);
3510
+ }
3511
+ }
3512
+ }
3513
+ const clusters = [];
3514
+ for (const [topic, list] of buckets) {
3515
+ clusters.push({ topic, docs: list });
3516
+ }
3517
+ clusters.sort((a, b2) => b2.docs.length - a.docs.length);
3518
+ return clusters;
3519
+ }
3520
+ function candidateTopicsFor(doc) {
3521
+ const topics = /* @__PURE__ */ new Set();
3522
+ if (doc.frontmatterTopic)
3523
+ topics.add(slugify(doc.frontmatterTopic));
3524
+ for (const tag of doc.tags) {
3525
+ const slug = slugify(tag);
3526
+ if (slug.length >= MIN_KEYWORD_LENGTH)
3527
+ topics.add(slug);
3528
+ }
3529
+ for (const kw of doc.filenameKeywords) {
3530
+ topics.add(kw);
3531
+ }
3532
+ return [...topics];
3533
+ }
3534
+ function slugify(s) {
3535
+ return s.trim().toLowerCase().replace(/\s+/g, "-");
3536
+ }
3537
+ function pickCluster(clusters, weakThreshold, cwd) {
3538
+ for (const c of clusters) {
3539
+ if (c.docs.length < weakThreshold)
3540
+ return null;
3541
+ const hubPath = join17(cwd, "data", HUB_DIR, `_HUB-${c.topic}.md`);
3542
+ if (existsSync4(hubPath))
3543
+ continue;
3544
+ return c;
3545
+ }
3546
+ return null;
3547
+ }
3548
+ async function askCoherence(llm, cluster, isStrongSignal) {
3549
+ const prompt = renderCoherencePrompt(cluster, isStrongSignal);
3550
+ const expected = {
3551
+ shape: "json",
3552
+ schema: {
3553
+ verdict: "yes|no",
3554
+ contextParagraph: "string (1-2 sentences, used in the hub body when verdict=yes)"
3555
+ }
3556
+ };
3557
+ const raw = await llm.ask(prompt, expected);
3558
+ return interpretCoherence(raw, isStrongSignal);
3559
+ }
3560
+ function renderCoherencePrompt(cluster, isStrongSignal) {
3561
+ const docList = cluster.docs.slice(0, MAX_LLM_DOC_LIST).map((d2) => ` - ${d2.path}`).join("\n");
3562
+ const truncationNote = cluster.docs.length > MAX_LLM_DOC_LIST ? `
3563
+ (... ${cluster.docs.length - MAX_LLM_DOC_LIST} more docs omitted for brevity)` : "";
3564
+ if (isStrongSignal) {
3565
+ return [
3566
+ `${cluster.docs.length} documents share the candidate topic "${cluster.topic}" \u2014 a strong-signal cluster.`,
3567
+ "",
3568
+ "Documents:",
3569
+ docList + truncationNote,
3570
+ "",
3571
+ "Default action is to propose creating a `_HUB-` page that cross-links them. Reject *only* if the docs are clearly incoherent \u2014 sharing a keyword but not a real subject. If the cluster has any reasonable cohesion, return verdict=yes.",
3572
+ "",
3573
+ `Return JSON: { "verdict": "yes" | "no", "contextParagraph": "1-2 sentences describing the topic for the hub body (required if verdict=yes)" }`
3574
+ ].join("\n");
3575
+ }
3576
+ return [
3577
+ `${cluster.docs.length} documents share the candidate topic "${cluster.topic}" \u2014 a weak-signal cluster.`,
3578
+ "",
3579
+ "Documents:",
3580
+ docList + truncationNote,
3581
+ "",
3582
+ "Affirm verdict=yes only if the topic is clearly cross-cutting and the cluster has real cohesion (not a coincidence of keywords or tags). When uncertain, return verdict=no.",
3583
+ "",
3584
+ `Return JSON: { "verdict": "yes" | "no", "contextParagraph": "1-2 sentences describing the topic for the hub body (required if verdict=yes)" }`
3585
+ ].join("\n");
3586
+ }
3587
+ function interpretCoherence(raw, isStrongSignal) {
3588
+ if (typeof raw !== "object" || raw === null)
3589
+ return null;
3590
+ const obj = raw;
3591
+ const verdict = obj.verdict === "yes" ? "yes" : obj.verdict === "no" ? "no" : null;
3592
+ if (verdict === null)
3593
+ return null;
3594
+ if (isStrongSignal) {
3595
+ if (verdict === "no")
3596
+ return null;
3597
+ const contextParagraph2 = typeof obj.contextParagraph === "string" ? obj.contextParagraph.trim() || void 0 : void 0;
3598
+ return contextParagraph2 ? { verdict: "yes", contextParagraph: contextParagraph2 } : { verdict: "yes" };
3599
+ }
3600
+ if (verdict !== "yes")
3601
+ return null;
3602
+ const contextParagraph = typeof obj.contextParagraph === "string" ? obj.contextParagraph.trim() : "";
3603
+ if (!contextParagraph)
3604
+ return null;
3605
+ return { verdict: "yes", contextParagraph };
3606
+ }
3607
+ function renderPreview(cluster, contextParagraph) {
3608
+ const lines = [
3609
+ `Proposed hub: hubs/_HUB-${cluster.topic}.md`,
3610
+ `Cluster size: ${cluster.docs.length} documents`
3611
+ ];
3612
+ if (contextParagraph) {
3613
+ lines.push("", contextParagraph);
3614
+ }
3615
+ lines.push("", "Cross-linked docs:");
3616
+ for (const d2 of cluster.docs.slice(0, MAX_LLM_DOC_LIST)) {
3617
+ lines.push(` - ${d2.path}`);
3618
+ }
3619
+ if (cluster.docs.length > MAX_LLM_DOC_LIST) {
3620
+ lines.push(` ... ${cluster.docs.length - MAX_LLM_DOC_LIST} more`);
3621
+ }
3622
+ return lines.join("\n");
3623
+ }
3624
+ function renderHubBody(cluster, contextParagraph, now) {
3625
+ const ymd = formatYmd2(now);
3626
+ const links = cluster.docs.map((d2) => `- [[${stripMdExtension(d2.path)}]]`).join("\n");
3627
+ const context = contextParagraph ? `${contextParagraph}
3628
+
3629
+ ` : "";
3630
+ return `---
3631
+ type: hub
3632
+ topic: ${cluster.topic}
3633
+ created: ${ymd}
3634
+ updated: ${ymd}
3635
+ tags: [hub, proactive-curator]
3636
+ ---
3637
+
3638
+ # ${cluster.topic}
3639
+
3640
+ > Hub proposed by proactive-curator on ${ymd}. The user accepted this proposal; edit freely. The cross-linked source docs were not modified.
3641
+
3642
+ ${context}## Cross-linked docs
3643
+
3644
+ ${links}
3645
+ `;
3646
+ }
3647
+ function stripMdExtension(p) {
3648
+ return p.replace(/\.md$/i, "");
3649
+ }
3650
+ function formatYmd2(d2) {
3651
+ const y2 = d2.getFullYear();
3652
+ const m2 = String(d2.getMonth() + 1).padStart(2, "0");
3653
+ const day = String(d2.getDate()).padStart(2, "0");
3654
+ return `${y2}-${m2}-${day}`;
3655
+ }
3656
+ async function applyHubCreate(cwd, action) {
3657
+ const folder = join17(cwd, "data", action.folderPath);
3658
+ await mkdir4(folder, { recursive: true });
3659
+ const file = join17(folder, action.filename);
3660
+ await mkdir4(dirname3(file), { recursive: true });
3661
+ await writeFile7(file, action.body, "utf8");
3662
+ return file;
3663
+ }
3664
+
3665
+ // ../modules/proactive-curator/dist/ambient-tracker.js
3666
+ var TurnBuffer = class {
3667
+ turns = [];
3668
+ maxTurns;
3669
+ estimator;
3670
+ constructor(options) {
3671
+ this.maxTurns = options?.maxTurns ?? 50;
3672
+ this.estimator = options?.estimator ?? defaultTokenEstimator;
3673
+ }
3674
+ /** Append a turn, evicting the oldest entry if the buffer is full. */
3675
+ add(turn) {
3676
+ this.turns.push(turn);
3677
+ while (this.turns.length > this.maxTurns) {
3678
+ this.turns.shift();
3679
+ }
3680
+ }
3681
+ /** Return the most recent `n` turns (or fewer if the buffer has fewer). */
3682
+ recent(n) {
3683
+ if (n >= this.turns.length)
3684
+ return [...this.turns];
3685
+ return this.turns.slice(-n);
3686
+ }
3687
+ /** Number of turns currently buffered. */
3688
+ get length() {
3689
+ return this.turns.length;
3690
+ }
3691
+ /** Estimated total token count over the entire buffer. */
3692
+ tokenCount() {
3693
+ let total = 0;
3694
+ for (const t of this.turns)
3695
+ total += this.estimator(t);
3696
+ return total;
3697
+ }
3698
+ /** Reset the buffer — typically called at session-end. */
3699
+ clear() {
3700
+ this.turns.length = 0;
3701
+ }
3702
+ };
3703
+ var AmbientBackpressure = class {
3704
+ declineStreak = 0;
3705
+ maxStreak;
3706
+ constructor(options) {
3707
+ this.maxStreak = options?.maxStreak ?? 3;
3708
+ }
3709
+ /** Record a user decline. Counts toward backpressure. */
3710
+ recordDecline() {
3711
+ this.declineStreak++;
3712
+ }
3713
+ /** Record a user acceptance. Clears the streak. */
3714
+ recordAccept() {
3715
+ this.declineStreak = 0;
3716
+ }
3717
+ /** Explicit reset (e.g. user runs `/curate` after backpressure activated). */
3718
+ reset() {
3719
+ this.declineStreak = 0;
3720
+ }
3721
+ /** True when the streak has reached the maximum and ambient should pause. */
3722
+ isActive() {
3723
+ return this.declineStreak >= this.maxStreak;
3724
+ }
3725
+ /** Current decline streak — exposed for diagnostics. */
3726
+ get streak() {
3727
+ return this.declineStreak;
2872
3728
  }
2873
3729
  };
3730
+ function defaultTokenEstimator(turn) {
3731
+ return Math.ceil(turn.content.length / 4);
3732
+ }
2874
3733
 
2875
- // ../plugins/session-rituals/dist/commands/session-start.js
2876
- import { existsSync as existsSync3 } from "fs";
2877
- import { readdir as readdir12 } from "fs/promises";
2878
- import { join as join16 } from "path";
2879
- var COUNTED_DIRS = ["_memory", "til", "decision-log"];
2880
- var sessionStartCommand = {
2881
- name: "session-start",
2882
- description: "Emit a start-of-session report (time + data directory counts).",
2883
- handler: async (input) => {
2884
- const { dataDir, repoRoot } = input.context;
2885
- const counts = {};
2886
- const missing = [];
2887
- for (const name of COUNTED_DIRS) {
2888
- const dir = join16(dataDir, name);
2889
- if (!existsSync3(dir)) {
2890
- missing.push(name);
2891
- counts[name] = 0;
3734
+ // ../modules/proactive-curator/dist/ambient-recaller.js
3735
+ var DEFAULT_MIN_SCORE = 0.5;
3736
+ var DEFAULT_MAX_SUGGESTIONS = 1;
3737
+ var DEFAULT_MIN_QUERY_CHARS = 12;
3738
+ var AmbientRecaller = class {
3739
+ recall;
3740
+ minScore;
3741
+ maxSuggestions;
3742
+ minQueryChars;
3743
+ candidateK;
3744
+ backpressure;
3745
+ surfaced = /* @__PURE__ */ new Set();
3746
+ constructor(options) {
3747
+ this.recall = options.recall;
3748
+ this.minScore = options.minScore ?? DEFAULT_MIN_SCORE;
3749
+ this.maxSuggestions = options.maxSuggestions ?? DEFAULT_MAX_SUGGESTIONS;
3750
+ this.minQueryChars = options.minQueryChars ?? DEFAULT_MIN_QUERY_CHARS;
3751
+ this.candidateK = options.candidateK ?? this.maxSuggestions + 4;
3752
+ this.backpressure = options.backpressure ?? new AmbientBackpressure();
3753
+ }
3754
+ /**
3755
+ * Consider surfacing a memory given the current conversation. Pass an
3756
+ * explicit `query` (the host knows what the user is referencing) or recent
3757
+ * `turns` to derive one from. Returns the gated suggestions plus diagnostics.
3758
+ *
3759
+ * Does not catch errors from the injected engine — a failing embedder
3760
+ * should surface to the host (which decides whether to swallow it for the
3761
+ * turn), not be masked here.
3762
+ */
3763
+ async consider(input) {
3764
+ if (this.backpressure.isActive()) {
3765
+ return emptyResult(true, null);
3766
+ }
3767
+ const query = (input.query ?? deriveQueryFromTurns(input.turns ?? [])).trim();
3768
+ if (query.length < this.minQueryChars) {
3769
+ return emptyResult(false, query.length === 0 ? null : query);
3770
+ }
3771
+ const { hits } = await this.recall(query, { k: this.candidateK });
3772
+ let belowThreshold = 0;
3773
+ let deduped = 0;
3774
+ const suggestions = [];
3775
+ for (const hit of hits) {
3776
+ if (hit.score < this.minScore) {
3777
+ belowThreshold++;
2892
3778
  continue;
2893
3779
  }
2894
- counts[name] = await countMarkdown(dir, name === "til");
3780
+ if (this.surfaced.has(hit.id)) {
3781
+ deduped++;
3782
+ continue;
3783
+ }
3784
+ suggestions.push({ hit, score: hit.score });
3785
+ if (suggestions.length >= this.maxSuggestions)
3786
+ break;
3787
+ }
3788
+ for (const s of suggestions)
3789
+ this.surfaced.add(s.hit.id);
3790
+ return {
3791
+ suggestions,
3792
+ suppressed: false,
3793
+ query,
3794
+ considered: hits.length,
3795
+ belowThreshold,
3796
+ deduped
3797
+ };
3798
+ }
3799
+ /** The user engaged with a surfaced suggestion — clears the decline streak. */
3800
+ recordAccept() {
3801
+ this.backpressure.recordAccept();
3802
+ }
3803
+ /** The user dismissed a surfaced suggestion — counts toward backpressure. */
3804
+ recordDecline() {
3805
+ this.backpressure.recordDecline();
3806
+ }
3807
+ /** True when backpressure has silenced ambient surfacing for the session. */
3808
+ get suppressed() {
3809
+ return this.backpressure.isActive();
3810
+ }
3811
+ /**
3812
+ * Re-engage after backpressure (the user explicitly asks for suggestions
3813
+ * again, e.g. via `/curate`). Clears the decline streak AND the dedup set
3814
+ * so previously surfaced hits can be offered again.
3815
+ */
3816
+ reset() {
3817
+ this.backpressure.reset();
3818
+ this.surfaced.clear();
3819
+ }
3820
+ };
3821
+ function deriveQueryFromTurns(turns) {
3822
+ for (let i = turns.length - 1; i >= 0; i--) {
3823
+ if (turns[i].role === "user")
3824
+ return turns[i].content;
3825
+ }
3826
+ return turns.length > 0 ? turns[turns.length - 1].content : "";
3827
+ }
3828
+ function emptyResult(suppressed, query) {
3829
+ return { suggestions: [], suppressed, query, considered: 0, belowThreshold: 0, deduped: 0 };
3830
+ }
3831
+
3832
+ // ../modules/proactive-curator/dist/adapters/shared.js
3833
+ var LLMJudgeError = class extends Error {
3834
+ raw;
3835
+ constructor(message, raw) {
3836
+ super(message);
3837
+ this.raw = raw;
3838
+ this.name = "LLMJudgeError";
3839
+ }
3840
+ };
3841
+ function frameForJudge(prompt, expected) {
3842
+ const tail = expected.shape === "json" ? '\n\nReturn ONLY the JSON object described above. No prose, no markdown code fences, no explanation around it. If you cannot answer, return the JSON with verdict="no" (for capture decisions) or actionKind="create-file" with a brief rationale (for placement decisions).' : "\n\nReturn ONLY the answer string. No prose preamble, no markdown formatting around it.";
3843
+ return prompt + tail;
3844
+ }
3845
+ function parseJudgeResponse(raw, expected, makeError) {
3846
+ if (typeof raw !== "string") {
3847
+ throw makeError("Host LLM returned a non-string response.");
3848
+ }
3849
+ const stripped = raw.trim();
3850
+ if (expected.shape === "string") {
3851
+ return stripped;
3852
+ }
3853
+ const jsonPayload = extractJsonPayload(stripped);
3854
+ try {
3855
+ return JSON.parse(jsonPayload);
3856
+ } catch (e) {
3857
+ throw makeError(`Host LLM response was not valid JSON: ${e.message}`, raw);
3858
+ }
3859
+ }
3860
+ function extractJsonPayload(s) {
3861
+ const fenced = s.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
3862
+ if (fenced)
3863
+ return fenced[1].trim();
3864
+ const firstBrace = s.indexOf("{");
3865
+ const lastBrace = s.lastIndexOf("}");
3866
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
3867
+ return s.slice(firstBrace, lastBrace + 1);
3868
+ }
3869
+ return s;
3870
+ }
3871
+ var InjectedLLMJudge = class {
3872
+ invoker;
3873
+ constructor(invoker) {
3874
+ this.invoker = invoker;
3875
+ }
3876
+ /** Host adapters override to raise a host-named error subclass. */
3877
+ makeError(message, raw) {
3878
+ return new LLMJudgeError(message, raw);
3879
+ }
3880
+ async ask(prompt, expected) {
3881
+ const framed = frameForJudge(prompt, expected);
3882
+ const raw = await this.invoker({ prompt: framed });
3883
+ return parseJudgeResponse(raw, expected, (m2, r) => this.makeError(m2, r));
3884
+ }
3885
+ };
3886
+
3887
+ // ../modules/proactive-curator/dist/adapters/claude-code.js
3888
+ var ClaudeCodeLLMJudgeError = class extends LLMJudgeError {
3889
+ constructor(message, raw) {
3890
+ super(message, raw);
3891
+ this.name = "ClaudeCodeLLMJudgeError";
3892
+ }
3893
+ };
3894
+ var ClaudeCodeLLMJudge = class extends InjectedLLMJudge {
3895
+ host = "claude-code";
3896
+ makeError(message, raw) {
3897
+ return new ClaudeCodeLLMJudgeError(message, raw);
3898
+ }
3899
+ };
3900
+
3901
+ // ../modules/proactive-curator/dist/adapters/codex.js
3902
+ var CodexLLMJudgeError = class extends LLMJudgeError {
3903
+ constructor(message, raw) {
3904
+ super(message, raw);
3905
+ this.name = "CodexLLMJudgeError";
3906
+ }
3907
+ };
3908
+ var CodexLLMJudge = class extends InjectedLLMJudge {
3909
+ host = "codex";
3910
+ makeError(message, raw) {
3911
+ return new CodexLLMJudgeError(message, raw);
3912
+ }
3913
+ };
3914
+
3915
+ // ../modules/proactive-curator/dist/adapters/gemini.js
3916
+ var GeminiLLMJudgeError = class extends LLMJudgeError {
3917
+ constructor(message, raw) {
3918
+ super(message, raw);
3919
+ this.name = "GeminiLLMJudgeError";
3920
+ }
3921
+ };
3922
+ var GeminiLLMJudge = class extends InjectedLLMJudge {
3923
+ host = "gemini";
3924
+ makeError(message, raw) {
3925
+ return new GeminiLLMJudgeError(message, raw);
3926
+ }
3927
+ };
3928
+
3929
+ // ../modules/proactive-curator/dist/adapters/claude-desktop.js
3930
+ var ClaudeDesktopLLMJudgeError = class extends LLMJudgeError {
3931
+ constructor(message, raw) {
3932
+ super(message, raw);
3933
+ this.name = "ClaudeDesktopLLMJudgeError";
3934
+ }
3935
+ };
3936
+ var ClaudeDesktopLLMJudge = class extends InjectedLLMJudge {
3937
+ host = "claude-desktop";
3938
+ makeError(message, raw) {
3939
+ return new ClaudeDesktopLLMJudgeError(message, raw);
3940
+ }
3941
+ };
3942
+
3943
+ // ../plugins/session-rituals/dist/index.js
3944
+ var dist_exports14 = {};
3945
+ __export(dist_exports14, {
3946
+ SESSION_END_COMMAND: () => SESSION_END_COMMAND,
3947
+ SESSION_START_COMMAND: () => SESSION_START_COMMAND,
3948
+ agendaCommand: () => agendaCommand,
3949
+ buildRegistry: () => buildRegistry,
3950
+ catchUpSessions: () => catchUpSessions,
3951
+ collectAgenda: () => collectAgenda,
3952
+ collectSessionStartReport: () => collectSessionStartReport,
3953
+ createAmbientRecaller: () => createAmbientRecaller,
3954
+ createRitualRegistry: () => createRitualRegistry,
3955
+ curateCommand: () => curateCommand,
3956
+ decisionCommand: () => decisionCommand,
3957
+ detectWorklogGaps: () => detectWorklogGaps,
3958
+ ensureVortexHooks: () => ensureVortexHooks,
3959
+ ensureWorklogEntry: () => ensureWorklogEntry,
3960
+ extractNextUp: () => extractNextUp,
3961
+ extractOpenTasks: () => extractOpenTasks,
3962
+ logCommand: () => logCommand,
3963
+ parseSettings: () => parseSettings,
3964
+ recallCommand: () => recallCommand,
3965
+ reindexCommand: () => reindexCommand,
3966
+ renderAgenda: () => renderAgenda,
3967
+ renderSessionStartReport: () => renderSessionStartReport,
3968
+ resolveRepoRoot: () => resolveRepoRoot,
3969
+ runVortexCli: () => runVortexCli,
3970
+ serializeSettings: () => serializeSettings,
3971
+ sessionStartCommand: () => sessionStartCommand,
3972
+ vortexCommand: () => vortexCommand
3973
+ });
3974
+
3975
+ // ../plugins/session-rituals/dist/commands/curate.js
3976
+ function parseCurateArgs(input) {
3977
+ const args = {};
3978
+ for (const t of input.split(/\s+/).filter(Boolean)) {
3979
+ if (t === "--insight")
3980
+ args.insightOnly = true;
3981
+ else if (t === "--hub")
3982
+ args.hubOnly = true;
3983
+ else if (t === "--reset-declined")
3984
+ args.resetDeclined = true;
3985
+ }
3986
+ return args;
3987
+ }
3988
+ function curateCommand(options) {
3989
+ const insightProposer = options.insightProposer ?? new InsightProposer({
3990
+ minTokensAccumulated: 2e3,
3991
+ minTurnsBeforeFirstCheck: 6
3992
+ });
3993
+ const hubProposer = options.hubProposer ?? new HubProposer({
3994
+ weakSignalAt: 3,
3995
+ strongSignalAt: 5
3996
+ });
3997
+ return {
3998
+ name: "curate",
3999
+ description: "Run proactive-curator proposers and surface any proposals. Flags: --insight | --hub | --reset-declined.",
4000
+ args: [
4001
+ {
4002
+ name: "flag",
4003
+ description: "Optional: --insight, --hub, or --reset-declined.",
4004
+ required: false
4005
+ }
4006
+ ],
4007
+ handler: async (input) => {
4008
+ const args = parseCurateArgs(input.rest);
4009
+ const { repoRoot } = input.context;
4010
+ const now = /* @__PURE__ */ new Date();
4011
+ if (args.resetDeclined) {
4012
+ await resetDeclined(repoRoot);
4013
+ return {
4014
+ subcommand: "curate",
4015
+ status: "reset-declined",
4016
+ proposals: [],
4017
+ skipped: { insight: false, hub: false },
4018
+ nextActions: [
4019
+ "Decline list cleared. Both insight and hub proposers will re-evaluate previously declined topics on the next /curate."
4020
+ ]
4021
+ };
4022
+ }
4023
+ const runInsight = !args.hubOnly;
4024
+ const runHub = !args.insightOnly;
4025
+ const declinedFingerprints = await loadDeclinedFingerprints(repoRoot, now);
4026
+ const proposals = [];
4027
+ let insightSkipped = !runInsight;
4028
+ let hubSkipped = !runHub;
4029
+ if (runInsight) {
4030
+ const insightInput = options.insightInputProvider?.() ?? null;
4031
+ if (!insightInput) {
4032
+ insightSkipped = true;
4033
+ } else {
4034
+ const p = await insightProposer.evaluate(insightInput, {
4035
+ cwd: repoRoot,
4036
+ declinedFingerprints,
4037
+ llm: options.llm,
4038
+ now
4039
+ });
4040
+ if (p)
4041
+ proposals.push(p);
4042
+ }
4043
+ }
4044
+ if (runHub) {
4045
+ const p = await hubProposer.evaluate({}, {
4046
+ cwd: repoRoot,
4047
+ declinedFingerprints,
4048
+ llm: options.llm,
4049
+ now
4050
+ });
4051
+ if (p)
4052
+ proposals.push(p);
4053
+ }
4054
+ const nextActions = [];
4055
+ if (proposals.length === 0) {
4056
+ nextActions.push("No proposals to surface right now.");
4057
+ if (insightSkipped && hubSkipped) {
4058
+ nextActions.push("Both proposers were skipped \u2014 run /curate without flags to enable both.");
4059
+ } else if (insightSkipped && runInsight) {
4060
+ nextActions.push("Insight proposer was skipped because no insightInputProvider was registered with this plugin.");
4061
+ }
4062
+ } else {
4063
+ nextActions.push(`${proposals.length} proposal${proposals.length === 1 ? "" : "s"} ready. Review each preview and call onAccept() or onDecline() to apply or dismiss.`);
4064
+ }
4065
+ return {
4066
+ subcommand: "curate",
4067
+ status: "ok",
4068
+ proposals,
4069
+ skipped: { insight: insightSkipped, hub: hubSkipped },
4070
+ nextActions
4071
+ };
4072
+ }
4073
+ };
4074
+ }
4075
+
4076
+ // ../plugins/session-rituals/dist/commands/recall.js
4077
+ import { join as join18 } from "path";
4078
+ function parseRecallArgs(rest, defaultK) {
4079
+ const tokens = rest.split(/\s+/).filter(Boolean);
4080
+ const out = { k: defaultK, query: "" };
4081
+ const queryParts = [];
4082
+ for (let i = 0; i < tokens.length; i++) {
4083
+ const t = tokens[i];
4084
+ if (t === "--k" && i + 1 < tokens.length) {
4085
+ const n = Number(tokens[++i]);
4086
+ if (Number.isFinite(n) && n > 0)
4087
+ out.k = Math.floor(n);
4088
+ } else if (t.startsWith("--k=")) {
4089
+ const n = Number(t.slice("--k=".length));
4090
+ if (Number.isFinite(n) && n > 0)
4091
+ out.k = Math.floor(n);
4092
+ } else if (t === "--source" && i + 1 < tokens.length) {
4093
+ out.source = tokens[++i];
4094
+ } else if (t.startsWith("--source=")) {
4095
+ out.source = t.slice("--source=".length);
4096
+ } else if (t === "--no-filter") {
4097
+ out.noHardFilter = true;
4098
+ } else {
4099
+ queryParts.push(t);
4100
+ }
4101
+ }
4102
+ out.query = queryParts.join(" ");
4103
+ return out;
4104
+ }
4105
+ function defaultDbPath(ctx) {
4106
+ return join18(ctx.dataDir, "_indexes", "memory.sqlite");
4107
+ }
4108
+ function recallCommand(options) {
4109
+ const defaultK = options.defaultK ?? 5;
4110
+ const resolveDb = options.dbPath ?? defaultDbPath;
4111
+ return {
4112
+ name: "recall",
4113
+ description: "Hybrid semantic search over memories. Usage: /recall <query> [--k N] [--source memory] [--no-filter].",
4114
+ args: [{ name: "query", description: "Natural-language query.", required: true }],
4115
+ handler: async (input) => {
4116
+ const { sqlite, vector, recall: recallEngine, sessionArchive } = await import("@vortex-os/memory-extended");
4117
+ const args = parseRecallArgs(input.rest, defaultK);
4118
+ const dbPath = resolveDb(input.context);
4119
+ if (args.query.trim().length === 0) {
4120
+ return {
4121
+ query: "",
4122
+ intent: { filters: {}, semanticText: "", notes: ["empty query"] },
4123
+ stage: { appliedFilters: {}, hardFilterCandidates: 0, hardFilterDropped: false, corpusSize: 0 },
4124
+ hits: []
4125
+ };
4126
+ }
4127
+ const sqlStore = new sqlite.MemorySqliteStore(dbPath);
4128
+ const vecStore = new vector.MemoryVectorStore({ db: dbPath });
4129
+ try {
4130
+ const archive = new sessionArchive.SessionArchiveStore(input.context.dataDir);
4131
+ try {
4132
+ await vecStore.rebuildSessions(archive, options.embed, { onlyMissing: true });
4133
+ } finally {
4134
+ archive.close();
4135
+ }
4136
+ } catch {
4137
+ }
4138
+ const chunkStore = new vector.SessionChunkStore(dbPath);
4139
+ try {
4140
+ return await recallEngine.recall({ query: args.query, k: args.k, source: args.source, noHardFilter: args.noHardFilter }, { sqlite: sqlStore, vector: vecStore, embed: options.embed, sessionChunks: chunkStore });
4141
+ } finally {
4142
+ chunkStore.close();
4143
+ vecStore.close();
4144
+ sqlStore.close();
4145
+ }
4146
+ }
4147
+ };
4148
+ }
4149
+
4150
+ // ../plugins/session-rituals/dist/commands/decision.js
4151
+ import { writeFile as writeFile8 } from "fs/promises";
4152
+ import { join as join19 } from "path";
4153
+ import { existsSync as existsSync5 } from "fs";
4154
+ var decisionCommand = {
4155
+ name: "decision",
4156
+ description: "Create a new Decision Log entry from the canonical template at `data/decision-log/<today>-<slug>.md`. Refuses to overwrite an existing file.",
4157
+ args: [
4158
+ { name: "slug", description: "Kebab-style identifier used in the filename.", required: true },
4159
+ { name: "title", description: "One-line decision title (rest of the input).", required: true }
4160
+ ],
4161
+ handler: async (input) => {
4162
+ const slug = input.args.slug;
4163
+ if (!slug) {
4164
+ throw new Error("`/decision` requires a slug argument.");
4165
+ }
4166
+ const title = extractTitle2(input.rest, slug);
4167
+ if (!title) {
4168
+ throw new Error("`/decision` requires a title after the slug.");
4169
+ }
4170
+ const date = todayIso();
4171
+ const dir = join19(input.context.dataDir, "decision-log");
4172
+ const store = new DecisionStore(dir);
4173
+ const path = store.pathFor(date, slug);
4174
+ if (existsSync5(path)) {
4175
+ throw new Error(`Refusing to overwrite existing entry: ${path}`);
4176
+ }
4177
+ const body = renderTemplate({ date, slug, title });
4178
+ await writeFile8(path, body, "utf8");
4179
+ return { path, date, slug };
4180
+ }
4181
+ };
4182
+ function extractTitle2(rest, slug) {
4183
+ const trimmed = rest.trim();
4184
+ if (trimmed.startsWith(slug)) {
4185
+ return trimmed.slice(slug.length).trim();
4186
+ }
4187
+ return trimmed;
4188
+ }
4189
+ function todayIso() {
4190
+ const d2 = /* @__PURE__ */ new Date();
4191
+ const y2 = d2.getFullYear();
4192
+ const m2 = String(d2.getMonth() + 1).padStart(2, "0");
4193
+ const day = String(d2.getDate()).padStart(2, "0");
4194
+ return `${y2}-${m2}-${day}`;
4195
+ }
4196
+
4197
+ // ../plugins/session-rituals/dist/commands/reindex.js
4198
+ import { existsSync as existsSync6 } from "fs";
4199
+ import { readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
4200
+ import { join as join20 } from "path";
4201
+ var TARGETS = [
4202
+ {
4203
+ dir: "_memory",
4204
+ title: "Memory",
4205
+ description: "\uC138\uC158 \uAC04 \uACF5\uC720\uB418\uB294 \uC601\uC18D \uBA54\uBAA8\uB9AC \uD56D\uBAA9\uB4E4. MEMORY.md\uB294 \uBCF8 \uB514\uB809\uD1A0\uB9AC\uC758 \uC6D0\uBCF8 \uC778\uB371\uC2A4\uC774\uBA70, \uBCF8 _INDEX.md\uB294 \uC0AC\uB78C\xB7Obsidian \uCE5C\uD654 \uC591\uC2DD\uC758 \uD30C\uC0DD\uBB3C\uC785\uB2C8\uB2E4.",
4206
+ privacy: "internal",
4207
+ recursive: false,
4208
+ skipPrefixes: [],
4209
+ skipFilenames: ["MEMORY.md"]
4210
+ },
4211
+ {
4212
+ dir: "worklog",
4213
+ title: "Worklog",
4214
+ description: "\uB0A0\uC9DC\uBCC4 \uC791\uC5C5 \uAE30\uB85D. `YYYY/MM/YYYY-MM-DD-keyword.md` \uAD6C\uC870.",
4215
+ privacy: "internal",
4216
+ recursive: true,
4217
+ skipPrefixes: [],
4218
+ skipFilenames: []
4219
+ },
4220
+ {
4221
+ dir: "decision-log",
4222
+ title: "Decision Log",
4223
+ description: "\uAC1C\uC778 \uC758\uC0AC\uACB0\uC815 \uAE30\uB85D \u2014 \uC65C \uADF8\uAC78 \uACE8\uB790\uB294\uC9C0\uB97C \uB0A8\uAE41\uB2C8\uB2E4.",
4224
+ privacy: "personal",
4225
+ recursive: false,
4226
+ skipPrefixes: ["_TEMPLATE"],
4227
+ skipFilenames: []
4228
+ },
4229
+ {
4230
+ dir: "runbooks",
4231
+ title: "Runbooks",
4232
+ description: "\uC7A5\uC560 \uB300\uC751\xB7\uC815\uAE30 \uC815\uBE44 \uC808\uCC28. `last_tested`\uB85C \uAC31\uC2E0 \uAE30\uD55C \uCD94\uC801.",
4233
+ privacy: "internal",
4234
+ recursive: false,
4235
+ skipPrefixes: [],
4236
+ skipFilenames: []
4237
+ },
4238
+ {
4239
+ dir: "hubs",
4240
+ title: "Hubs",
4241
+ description: "\uC8FC\uC81C\uBCC4 \uC9C4\uC785\uC810 \u2014 \uAD00\uB828 \uC790\uB8CC\uB97C \uD55C \uACF3\uC5D0\uC11C \uBB36\uB294 \uC778\uB371\uC2A4 \uD398\uC774\uC9C0 \uBAA8\uC74C.",
4242
+ privacy: "internal",
4243
+ recursive: false,
4244
+ skipPrefixes: [],
4245
+ skipFilenames: []
4246
+ },
4247
+ {
4248
+ dir: "projects",
4249
+ title: "Projects",
4250
+ description: "\uC9C4\uD589 \uC911\xB7\uC644\uB8CC \uD504\uB85C\uC81D\uD2B8 \uBAA8\uC74C (Work\xB7Home). \uBCF8 _INDEX\uB294 \uD3C9\uD0C4 \uBAA9\uB85D\uC774\uBA70, \uD558\uC704 \uD504\uB85C\uC81D\uD2B8 \uD3F4\uB354\uBCC4 _INDEX\uAC00 \uB354 \uC720\uC6A9\uD560 \uC218 \uC788\uC74C (\uB2E4\uC74C \uC0AC\uC774\uD074 \uAC80\uD1A0).",
4251
+ privacy: "internal",
4252
+ recursive: true,
4253
+ skipPrefixes: [],
4254
+ skipFilenames: []
4255
+ },
4256
+ {
4257
+ dir: "reference",
4258
+ title: "Reference",
4259
+ description: "AI \uADDC\uCE59\xB7\uAE30\uC220 \uCE58\uD2B8\uC2DC\uD2B8\xB7\uC7A5\uBE44\xB7\uC2A4\uD0AC \uB4F1 \uB808\uD37C\uB7F0\uC2A4 \uC790\uB8CC (AI-Rules\xB7Games\xB7Gear\xB7Infra\xB7Org\xB7Skills\xB7Tech).",
4260
+ privacy: "internal",
4261
+ recursive: true,
4262
+ skipPrefixes: [],
4263
+ skipFilenames: []
4264
+ },
4265
+ {
4266
+ dir: "reports",
4267
+ title: "Reports",
4268
+ description: "\uC815\uAE30 \uAC74\uAC15\uAC80\uC9C4 \uB9AC\uD3EC\uD2B8 (Service-Health\xB7Infra-Health). \uC2DC\uC810 \uC2A4\uB0C5\uC0F7 \uB204\uC801.",
4269
+ privacy: "internal",
4270
+ recursive: true,
4271
+ skipPrefixes: [],
4272
+ skipFilenames: []
4273
+ },
4274
+ {
4275
+ dir: "inbox",
4276
+ title: "Inbox",
4277
+ description: "\uBBF8\uBD84\uB958 \uC784\uC2DC \uBA54\uBAA8\xB7\uC544\uC774\uB514\uC5B4\xB7\uBC31\uB85C\uADF8. \uC815\uC2DD \uBD84\uB958 \uC2DC \uD574\uB2F9 \uCE74\uD14C\uACE0\uB9AC\uB85C \uC774\uB3D9.",
4278
+ privacy: "internal",
4279
+ recursive: true,
4280
+ skipPrefixes: [],
4281
+ skipFilenames: []
4282
+ },
4283
+ {
4284
+ dir: "_templates",
4285
+ title: "Templates",
4286
+ description: "\uBCF4\uACE0\uC6A9\xB7\uACF5\uC720\uC6A9 \uC790\uB8CC \uD15C\uD50C\uB9BF \uBAA8\uC74C (Sharing \uB4F1).",
4287
+ privacy: "internal",
4288
+ recursive: true,
4289
+ skipPrefixes: [],
4290
+ skipFilenames: []
4291
+ }
4292
+ ];
4293
+ var reindexCommand = {
4294
+ name: "reindex",
4295
+ description: "Regenerate _INDEX.md for any configured target directory (or all targets when called with no argument). Idempotent \u2014 unchanged indexes are not rewritten.",
4296
+ args: [
4297
+ {
4298
+ name: "dir",
4299
+ description: "Optional directory name. Omit to reindex all targets."
4300
+ }
4301
+ ],
4302
+ handler: async (input) => {
4303
+ const requested = input.args.dir;
4304
+ const targets = requested ? TARGETS.filter((t) => t.dir === requested) : TARGETS;
4305
+ if (requested && targets.length === 0) {
4306
+ throw new Error(`Unknown reindex target: "${requested}". Known: ${TARGETS.map((t) => t.dir).join(", ")}`);
4307
+ }
4308
+ const results = [];
4309
+ for (const t of targets) {
4310
+ const dir = join20(input.context.dataDir, t.dir);
4311
+ if (!existsSync6(dir)) {
4312
+ results.push({ dir: t.dir, status: "missing", entries: 0, bytes: 0 });
4313
+ continue;
4314
+ }
4315
+ const entries = await scanDirectory(dir, {
4316
+ recursive: t.recursive,
4317
+ skipPrefixes: t.skipPrefixes,
4318
+ skipFilenames: t.skipFilenames
4319
+ });
4320
+ const body = renderIndex({
4321
+ title: t.title,
4322
+ description: t.description,
4323
+ entries,
4324
+ privacy: t.privacy
4325
+ });
4326
+ const target = join20(dir, "_INDEX.md");
4327
+ let existing;
4328
+ try {
4329
+ existing = await readFile16(target, "utf8");
4330
+ } catch {
4331
+ existing = void 0;
4332
+ }
4333
+ if (existing === body) {
4334
+ results.push({
4335
+ dir: t.dir,
4336
+ status: "unchanged",
4337
+ entries: entries.length,
4338
+ bytes: body.length
4339
+ });
4340
+ continue;
4341
+ }
4342
+ await writeFile9(target, body, "utf8");
4343
+ results.push({
4344
+ dir: t.dir,
4345
+ status: "written",
4346
+ entries: entries.length,
4347
+ bytes: body.length
4348
+ });
4349
+ }
4350
+ return results;
4351
+ }
4352
+ };
4353
+
4354
+ // ../plugins/session-rituals/dist/commands/session-start.js
4355
+ import { existsSync as existsSync7 } from "fs";
4356
+ import { readdir as readdir14 } from "fs/promises";
4357
+ import { join as join21 } from "path";
4358
+ var COUNTED_DIRS = ["_memory", "worklog", "decision-log"];
4359
+ var sessionStartCommand = {
4360
+ name: "session-start",
4361
+ description: "Emit a start-of-session report (time + data directory counts).",
4362
+ handler: async (input) => {
4363
+ const { dataDir, repoRoot } = input.context;
4364
+ const counts = {};
4365
+ const missing = [];
4366
+ for (const name of COUNTED_DIRS) {
4367
+ const dir = join21(dataDir, name);
4368
+ if (!existsSync7(dir)) {
4369
+ missing.push(name);
4370
+ counts[name] = 0;
4371
+ continue;
4372
+ }
4373
+ counts[name] = await countMarkdown(dir, name === "worklog");
4374
+ }
4375
+ return {
4376
+ time: (/* @__PURE__ */ new Date()).toISOString(),
4377
+ repoRoot,
4378
+ dataDir,
4379
+ counts,
4380
+ missing
4381
+ };
4382
+ }
4383
+ };
4384
+ async function countMarkdown(dir, recursive) {
4385
+ let total = 0;
4386
+ const entries = await readdir14(dir, { withFileTypes: true });
4387
+ for (const e of entries) {
4388
+ if (e.isFile()) {
4389
+ if (!e.name.endsWith(".md"))
4390
+ continue;
4391
+ if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md") {
4392
+ continue;
4393
+ }
4394
+ if (e.name.startsWith("_TEMPLATE"))
4395
+ continue;
4396
+ total++;
4397
+ } else if (e.isDirectory() && recursive) {
4398
+ if (e.name.startsWith(".") || e.name.startsWith("_"))
4399
+ continue;
4400
+ total += await countMarkdown(join21(dir, e.name), recursive);
4401
+ }
4402
+ }
4403
+ return total;
4404
+ }
4405
+
4406
+ // ../plugins/session-rituals/dist/commands/log.js
4407
+ var logCommand = {
4408
+ name: "log",
4409
+ description: "Append a `## <section-title>` section to today's worklog entry. Throws if today's worklog does not exist (run `/vortex init` once to bootstrap).",
4410
+ args: [
4411
+ {
4412
+ name: "section",
4413
+ description: "Title for the new `## ` section (rest of the input).",
4414
+ required: true
4415
+ }
4416
+ ],
4417
+ handler: async (input) => {
4418
+ const sectionTitle = input.rest.trim();
4419
+ if (!sectionTitle) {
4420
+ throw new Error("`/log` requires a section title.");
4421
+ }
4422
+ const date = todayIso2();
4423
+ const store = new WorklogStore(`${input.context.dataDir}/worklog`);
4424
+ const todayEntry = await store.get(date);
4425
+ if (!todayEntry) {
4426
+ throw new Error(`No worklog entry exists for ${date}. Create one first; this command only appends sections.`);
4427
+ }
4428
+ await appendSection(todayEntry, sectionTitle, "");
4429
+ return {
4430
+ path: todayEntry.path,
4431
+ date: todayEntry.date,
4432
+ keyword: todayEntry.keyword,
4433
+ sectionTitle
4434
+ };
4435
+ }
4436
+ };
4437
+ function todayIso2() {
4438
+ const d2 = /* @__PURE__ */ new Date();
4439
+ const y2 = d2.getFullYear();
4440
+ const m2 = String(d2.getMonth() + 1).padStart(2, "0");
4441
+ const day = String(d2.getDate()).padStart(2, "0");
4442
+ return `${y2}-${m2}-${day}`;
4443
+ }
4444
+
4445
+ // ../plugins/session-rituals/dist/commands/vortex.js
4446
+ import { spawn } from "child_process";
4447
+ import { existsSync as existsSync8 } from "fs";
4448
+ import { copyFile, mkdir as mkdir5, readdir as readdir15, readFile as readFile17, stat as stat8, writeFile as writeFile10 } from "fs/promises";
4449
+ import { basename as basename7, dirname as dirname4, join as join22 } from "path";
4450
+ import { fileURLToPath } from "url";
4451
+
4452
+ // ../plugins/session-rituals/dist/ensure-hooks.js
4453
+ var SESSION_START_COMMAND = "npx --no-install -p @vortex-os/base vortex session-start";
4454
+ var SESSION_END_COMMAND = "npx --no-install -p @vortex-os/base vortex session-end";
4455
+ function parseSettings(text) {
4456
+ const trimmed = (text ?? "").trim();
4457
+ if (trimmed.length === 0)
4458
+ return {};
4459
+ let parsed;
4460
+ try {
4461
+ parsed = JSON.parse(trimmed);
4462
+ } catch (e) {
4463
+ throw new Error(`.claude/settings.json is not valid JSON \u2014 refusing to overwrite. Fix or remove it first. (${e.message})`);
4464
+ }
4465
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4466
+ throw new Error(".claude/settings.json is not a JSON object \u2014 refusing to overwrite.");
4467
+ }
4468
+ return parsed;
4469
+ }
4470
+ function hasCommand(groups, command) {
4471
+ if (!groups)
4472
+ return false;
4473
+ return groups.some((g) => g.hooks?.some((h) => h.command === command));
4474
+ }
4475
+ function ensureVortexHooks(existing) {
4476
+ const base = existing && typeof existing === "object" ? existing : {};
4477
+ const hooks = { ...base.hooks ?? {} };
4478
+ const added = [];
4479
+ const wire = (event, command) => {
4480
+ const groups = hooks[event] ? [...hooks[event]] : [];
4481
+ if (hasCommand(groups, command))
4482
+ return;
4483
+ groups.push({ hooks: [{ type: "command", command }] });
4484
+ hooks[event] = groups;
4485
+ added.push(event);
4486
+ };
4487
+ wire("SessionStart", SESSION_START_COMMAND);
4488
+ wire("SessionEnd", SESSION_END_COMMAND);
4489
+ const settings = { ...base, hooks };
4490
+ return { settings, added, alreadyWired: added.length === 0 };
4491
+ }
4492
+ function serializeSettings(settings) {
4493
+ return JSON.stringify(settings, null, 2) + "\n";
4494
+ }
4495
+
4496
+ // ../plugins/session-rituals/dist/commands/vortex.js
4497
+ var PLANNED_SUBS = [];
4498
+ var vortexCommand = {
4499
+ name: "vortex",
4500
+ description: "VortEX root command. Subcommands: init | status | import | doctor | sync | help.",
4501
+ args: [
4502
+ {
4503
+ name: "sub",
4504
+ description: "Subcommand (init|status|import|doctor|sync|help).",
4505
+ required: false
4506
+ }
4507
+ ],
4508
+ handler: async (input) => {
4509
+ const tokens = tokenize(input.rest);
4510
+ const sub = tokens[0] ?? "help";
4511
+ const restAfterSub = tokens.slice(1);
4512
+ if (sub === "init")
4513
+ return runInit(input, restAfterSub);
4514
+ if (sub === "status")
4515
+ return runStatus(input);
4516
+ if (sub === "import")
4517
+ return runImport(input, restAfterSub);
4518
+ if (sub === "doctor")
4519
+ return runDoctor(input);
4520
+ if (sub === "sync")
4521
+ return runSync(input, restAfterSub);
4522
+ if (sub === "help" || sub === "")
4523
+ return runHelp();
4524
+ if (PLANNED_SUBS.includes(sub)) {
4525
+ return {
4526
+ subcommand: sub,
4527
+ status: "not-implemented",
4528
+ message: `\`/vortex ${sub}\` is reserved but not yet implemented. Planned in a future phase. Run \`/vortex help\` for available subcommands.`
4529
+ };
4530
+ }
4531
+ return {
4532
+ subcommand: "unknown",
4533
+ status: "not-implemented",
4534
+ message: `Unknown subcommand "${sub}". Run \`/vortex help\` for the list.`
4535
+ };
4536
+ }
4537
+ };
4538
+ function runHelp() {
4539
+ return {
4540
+ subcommand: "help",
4541
+ status: "ok",
4542
+ subcommands: [
4543
+ {
4544
+ name: "init",
4545
+ description: "First-time setup wizard. Creates user profile memory and first worklog (hubs grow organically \u2014 not seeded here).",
4546
+ state: "active"
4547
+ },
4548
+ {
4549
+ name: "status",
4550
+ description: "Show instance state (memory count, latest worklog, missing skeletons).",
4551
+ state: "active"
4552
+ },
4553
+ {
4554
+ name: "import",
4555
+ description: "Bring an existing folder into data/ \u2014 preserves your folder structure, auto-classifies worklog/decision-log/runbooks/hubs/_memory files, injects missing frontmatter.",
4556
+ state: "active"
4557
+ },
4558
+ {
4559
+ name: "doctor",
4560
+ description: "Diagnose instance health (system dirs, profile, indexes, wiki links, frontmatter, runbook freshness, node version, git remote).",
4561
+ state: "active"
4562
+ },
4563
+ {
4564
+ name: "sync",
4565
+ description: "Framework-developer workflow: git pull \u2192 npm install \u2192 npm run build \u2192 npm run verify. Stops on first failure. End users on npm registry do not need this.",
4566
+ state: "active"
4567
+ },
4568
+ { name: "help", description: "Show this list.", state: "active" }
4569
+ ],
4570
+ siblingCommands: [
4571
+ {
4572
+ name: "session-start",
4573
+ description: "Emit a start-of-session report (time + data directory counts + missing dirs)."
4574
+ },
4575
+ {
4576
+ name: "log",
4577
+ description: "Append a `## <section-title>` section to today's worklog entry. Throws if today's worklog does not exist (run `/vortex init` once to bootstrap)."
4578
+ },
4579
+ {
4580
+ name: "decision",
4581
+ description: "Create a new Decision Log entry from the canonical template at `data/decision-log/<today>-<slug>.md`. Refuses to overwrite an existing file."
4582
+ },
4583
+ {
4584
+ name: "reindex",
4585
+ description: "Regenerate _INDEX.md for any configured target directory (or all targets when called with no argument). Idempotent \u2014 unchanged indexes are not rewritten."
4586
+ }
4587
+ ]
4588
+ };
4589
+ }
4590
+ function resolveTemplatesDir() {
4591
+ const here = dirname4(fileURLToPath(import.meta.url));
4592
+ const candidates = [
4593
+ join22(here, "..", "..", "templates"),
4594
+ // session-rituals: dist/commands -> templates
4595
+ join22(here, "..", "templates"),
4596
+ // base aggregate: dist -> templates
4597
+ join22(here, "templates")
4598
+ // defensive: alongside the bundle
4599
+ ];
4600
+ for (const c of candidates) {
4601
+ if (existsSync8(join22(c, "commands")) || existsSync8(join22(c, "routers")))
4602
+ return c;
4603
+ }
4604
+ return null;
4605
+ }
4606
+ async function installCommandTemplates(repoRoot, templatesDir) {
4607
+ if (!templatesDir)
4608
+ return [];
4609
+ const commandsDir = join22(templatesDir, "commands");
4610
+ if (!existsSync8(commandsDir))
4611
+ return [];
4612
+ const destDir = join22(repoRoot, ".claude", "commands");
4613
+ await mkdir5(destDir, { recursive: true });
4614
+ const written = [];
4615
+ for (const name of await readdir15(commandsDir)) {
4616
+ if (!name.endsWith(".md"))
4617
+ continue;
4618
+ const dest = join22(destDir, name);
4619
+ if (existsSync8(dest))
4620
+ continue;
4621
+ await copyFile(join22(commandsDir, name), dest);
4622
+ written.push(dest);
4623
+ }
4624
+ return written;
4625
+ }
4626
+ var ROUTER_FILES = [
4627
+ "AGENT.md",
4628
+ "CLAUDE.md",
4629
+ "CODEX.md",
4630
+ "GEMINI.md",
4631
+ ".cursorrules"
4632
+ ];
4633
+ async function installRouterTemplates(repoRoot, templatesDir) {
4634
+ if (!templatesDir)
4635
+ return [];
4636
+ const routersDir = join22(templatesDir, "routers");
4637
+ if (!existsSync8(routersDir))
4638
+ return [];
4639
+ const written = [];
4640
+ for (const name of ROUTER_FILES) {
4641
+ const src = join22(routersDir, name);
4642
+ if (!existsSync8(src))
4643
+ continue;
4644
+ const dest = join22(repoRoot, name);
4645
+ if (existsSync8(dest))
4646
+ continue;
4647
+ await copyFile(src, dest);
4648
+ written.push(dest);
4649
+ }
4650
+ return written;
4651
+ }
4652
+ async function seedInstanceConfig(repoRoot, templatesDir) {
4653
+ const written = [];
4654
+ const agentDir = join22(repoRoot, ".agent");
4655
+ const vortexJson = join22(agentDir, "vortex.json");
4656
+ if (!existsSync8(vortexJson)) {
4657
+ await mkdir5(agentDir, { recursive: true });
4658
+ const tmpl = templatesDir ? join22(templatesDir, "config", "vortex.json") : null;
4659
+ if (tmpl && existsSync8(tmpl)) {
4660
+ await copyFile(tmpl, vortexJson);
4661
+ } else {
4662
+ await writeFile10(vortexJson, JSON.stringify({
4663
+ autoRecord: {
4664
+ sessionStart: true,
4665
+ worklog: true,
4666
+ decision: true,
4667
+ ambientRecall: true,
4668
+ archive: true
4669
+ },
4670
+ environments: []
4671
+ }, null, 2) + "\n", "utf8");
4672
+ }
4673
+ written.push(vortexJson);
4674
+ }
4675
+ const pkgPath = join22(repoRoot, "package.json");
4676
+ if (!existsSync8(pkgPath)) {
4677
+ await writeFile10(pkgPath, JSON.stringify({
4678
+ name: "vortex-instance",
4679
+ version: "0.0.0",
4680
+ private: true,
4681
+ type: "module",
4682
+ description: "A VortEX instance (created by `vortex init`)."
4683
+ }, null, 2) + "\n", "utf8");
4684
+ written.push(pkgPath);
4685
+ }
4686
+ return written;
4687
+ }
4688
+ async function runInit(input, tokens) {
4689
+ const args = parseInitArgs(tokens);
4690
+ const { dataDir, repoRoot } = input.context;
4691
+ const templatesDir = resolveTemplatesDir();
4692
+ const requiredDirs = ["_memory", "worklog", "decision-log", "hubs", "inbox", "runbooks"];
4693
+ for (const d2 of requiredDirs) {
4694
+ const p = join22(dataDir, d2);
4695
+ if (!existsSync8(p))
4696
+ await mkdir5(p, { recursive: true });
4697
+ }
4698
+ const scaffolded = [];
4699
+ try {
4700
+ scaffolded.push(...await installRouterTemplates(repoRoot, templatesDir));
4701
+ } catch {
4702
+ }
4703
+ try {
4704
+ scaffolded.push(...await seedInstanceConfig(repoRoot, templatesDir));
4705
+ } catch {
4706
+ }
4707
+ const profilePath = join22(dataDir, "_memory", "user_profile.md");
4708
+ if (existsSync8(profilePath) && !args.force) {
4709
+ return {
4710
+ subcommand: "init",
4711
+ status: "already-initialized",
4712
+ created: scaffolded,
4713
+ nextActions: [
4714
+ `VortEX instance is already initialized (${profilePath} exists).`,
4715
+ scaffolded.length > 0 ? `Seeded ${scaffolded.length} missing scaffolding file(s): ${scaffolded.map((p) => p.replace(repoRoot, ".")).join(", ")}.` : "All scaffolding (routers, .agent/vortex.json, package.json) already present.",
4716
+ "To re-run, pass `--force` (existing user_profile / first worklog will be overwritten).",
4717
+ "To check current state, try `/session-start`."
4718
+ ]
4719
+ };
4720
+ }
4721
+ const missing = [];
4722
+ if (!args.name) {
4723
+ missing.push({
4724
+ name: "name",
4725
+ prompt: 'What name or handle should VortEX use for you? (e.g. "Alex" or "team-lead")'
4726
+ });
4727
+ }
4728
+ if (!args.role) {
4729
+ missing.push({
4730
+ name: "role",
4731
+ prompt: 'What is your main role in one word? (e.g. "engineer", "researcher", "writer")'
4732
+ });
4733
+ }
4734
+ if (!args.task) {
4735
+ missing.push({
4736
+ name: "task",
4737
+ prompt: "What is one thing you're working on right now? (one sentence \u2014 this becomes your first worklog seed)"
4738
+ });
4739
+ }
4740
+ if (missing.length > 0) {
4741
+ return {
4742
+ subcommand: "init",
4743
+ status: "needs-input",
4744
+ created: [],
4745
+ missingInputs: missing,
4746
+ nextActions: [
4747
+ "Ask the user the prompts in `missingInputs`, then re-run with the answers:",
4748
+ ' /vortex init --name "<name>" --role "<role>" --task "<task>"',
4749
+ "Optional: append `--force` to overwrite an already-initialized instance."
4750
+ ]
4751
+ };
4752
+ }
4753
+ const today2 = todayIso3();
4754
+ const created = [...scaffolded];
4755
+ const scaffoldNotes = [];
4756
+ if (scaffolded.length > 0) {
4757
+ const names = scaffolded.map((p) => p.replace(repoRoot, ".").replace(/\\/g, "/"));
4758
+ scaffoldNotes.push(`Seeded instance scaffolding: ${names.join(", ")}.`);
4759
+ }
4760
+ await writeFile10(profilePath, renderUserProfile(args.name, args.role, args.task, today2), "utf8");
4761
+ created.push(profilePath);
4762
+ const [year, month] = today2.split("-");
4763
+ const worklogDir = join22(dataDir, "worklog", year, month);
4764
+ await mkdir5(worklogDir, { recursive: true });
4765
+ const worklogPath = join22(worklogDir, `${today2}-vortex-init.md`);
4766
+ await writeFile10(worklogPath, renderFirstWorklog(args.name, args.role, args.task, today2), "utf8");
4767
+ created.push(worklogPath);
4768
+ const hookNotes = [];
4769
+ try {
4770
+ const settingsPath = join22(input.context.repoRoot, ".claude", "settings.json");
4771
+ const existingText = existsSync8(settingsPath) ? await readFile17(settingsPath, "utf8") : null;
4772
+ const { settings, added, alreadyWired } = ensureVortexHooks(parseSettings(existingText));
4773
+ if (!alreadyWired) {
4774
+ await mkdir5(join22(input.context.repoRoot, ".claude"), { recursive: true });
4775
+ await writeFile10(settingsPath, serializeSettings(settings), "utf8");
4776
+ created.push(settingsPath);
4777
+ hookNotes.push(`Wired ${added.join(" + ")} hook(s) into .claude/settings.json \u2014 the VortEX boot report runs automatically at session start.`);
4778
+ } else {
4779
+ hookNotes.push("Session hooks already wired in .claude/settings.json.");
4780
+ }
4781
+ } catch (e) {
4782
+ hookNotes.push(`\u26A0\uFE0F Could not wire session hooks automatically: ${e.message} Copy .claude/settings.example.json to .claude/settings.json by hand to enable the boot report.`);
4783
+ }
4784
+ try {
4785
+ const cmds = await installCommandTemplates(input.context.repoRoot, templatesDir);
4786
+ created.push(...cmds);
4787
+ if (cmds.length > 0) {
4788
+ hookNotes.push(`Installed ${cmds.length} slash command(s) into .claude/commands/ (${cmds.map((c) => "/" + basename7(c, ".md")).join(", ")}).`);
4789
+ }
4790
+ } catch (e) {
4791
+ hookNotes.push(`\u26A0\uFE0F Could not install slash commands: ${e.message}`);
4792
+ }
4793
+ const externalFolders = await detectExternalFolders(input.context.repoRoot);
4794
+ const baseNext = [
4795
+ `Done. Created ${created.length} files.`,
4796
+ ...scaffoldNotes,
4797
+ ...hookNotes,
4798
+ "Next 3 things you can try right now:",
4799
+ " /log <one-line update> \u2014 append a section to today's worklog",
4800
+ " /decision <slug> <title> \u2014 record a decision",
4801
+ " /session-start \u2014 daily start-of-session report",
4802
+ `Open ${worklogPath} to see your first worklog \u2014 it already names "${args.task}".`,
4803
+ "Hubs grow organically: once 3+ categories accumulate on the same topic, create `hubs/_HUB-<topic>.md` to cross-link them."
4804
+ ];
4805
+ const importPrompt = [];
4806
+ if (externalFolders && externalFolders.length > 0) {
4807
+ importPrompt.push("");
4808
+ importPrompt.push("\u{1F4C1} Detected existing folders that look like notes/vaults you may want to import:");
4809
+ for (const f of externalFolders) {
4810
+ importPrompt.push(` - ${f.path} (${f.mdCount} .md files)`);
4811
+ }
4812
+ importPrompt.push(" Import any of them now with:");
4813
+ for (const f of externalFolders) {
4814
+ importPrompt.push(` /vortex import --from "${f.path}"`);
4815
+ }
4816
+ importPrompt.push(" (Append --dry-run first to preview without copying. You can also do this later \u2014 these folders are not modified.)");
4817
+ }
4818
+ return {
4819
+ subcommand: "init",
4820
+ status: "completed",
4821
+ created,
4822
+ externalFolders,
4823
+ nextActions: [...baseNext, ...importPrompt]
4824
+ };
4825
+ }
4826
+ function parseInitArgs(tokens) {
4827
+ const args = {};
4828
+ for (let i = 0; i < tokens.length; i++) {
4829
+ const t = tokens[i];
4830
+ if (t === "--force") {
4831
+ args.force = true;
4832
+ continue;
4833
+ }
4834
+ if (t === "--name" && i + 1 < tokens.length) {
4835
+ args.name = tokens[++i];
4836
+ continue;
4837
+ }
4838
+ if (t === "--role" && i + 1 < tokens.length) {
4839
+ args.role = tokens[++i];
4840
+ continue;
4841
+ }
4842
+ if (t === "--task" && i + 1 < tokens.length) {
4843
+ args.task = tokens[++i];
4844
+ continue;
4845
+ }
4846
+ }
4847
+ return args;
4848
+ }
4849
+ function tokenize(s) {
4850
+ const out = [];
4851
+ let i = 0;
4852
+ while (i < s.length) {
4853
+ while (i < s.length && /\s/.test(s[i]))
4854
+ i++;
4855
+ if (i >= s.length)
4856
+ break;
4857
+ const ch = s[i];
4858
+ if (ch === '"' || ch === "'") {
4859
+ const quote = ch;
4860
+ i++;
4861
+ let buf = "";
4862
+ while (i < s.length && s[i] !== quote) {
4863
+ buf += s[i++];
4864
+ }
4865
+ if (i < s.length)
4866
+ i++;
4867
+ out.push(buf);
4868
+ } else {
4869
+ let buf = "";
4870
+ while (i < s.length && !/\s/.test(s[i])) {
4871
+ buf += s[i++];
4872
+ }
4873
+ out.push(buf);
4874
+ }
4875
+ }
4876
+ return out;
4877
+ }
4878
+ function renderUserProfile(name, role, task, date) {
4879
+ return `---
4880
+ name: user-profile
4881
+ description: Operator profile captured by /vortex init.
4882
+ type: user
4883
+ created: ${date}
4884
+ updated: ${date}
4885
+ ---
4886
+
4887
+ # User Profile
4888
+
4889
+ - **Name/handle**: ${name}
4890
+ - **Role**: ${role}
4891
+ - **Initial focus**: ${task}
4892
+
4893
+ This memory was created by \`/vortex init\` on ${date}. Edit freely as your role evolves.
4894
+ `;
4895
+ }
4896
+ function renderFirstWorklog(name, role, task, date) {
4897
+ return `---
4898
+ type: worklog
4899
+ status: active
4900
+ created: ${date}
4901
+ updated: ${date}
4902
+ tags: [worklog, onboarding]
4903
+ ---
4904
+
4905
+ # ${date} \u2014 VortEX \uC2DC\uC791
4906
+
4907
+ > First worklog, created by \`/vortex init\`. ${name} (${role}). Today's focus: ${task}
4908
+
4909
+ ## What I'm working on
4910
+
4911
+ ${task}
4912
+
4913
+ ## Notes
4914
+
4915
+ (append more with \`/log <section-title>\`)
4916
+
4917
+ ## Next
4918
+
4919
+ - [ ] Try \`/decision <slug> <title>\` for your first decision record
4920
+ - [ ] Add a memory by editing \`data/_memory/user_profile.md\` or creating new ones
4921
+ - [ ] Run \`/session-start\` tomorrow to see your accumulated state
4922
+ `;
4923
+ }
4924
+ function todayIso3() {
4925
+ const d2 = /* @__PURE__ */ new Date();
4926
+ const y2 = d2.getFullYear();
4927
+ const m2 = String(d2.getMonth() + 1).padStart(2, "0");
4928
+ const day = String(d2.getDate()).padStart(2, "0");
4929
+ return `${y2}-${m2}-${day}`;
4930
+ }
4931
+ var COUNT_KEY_TO_DIR = {
4932
+ memory: "_memory",
4933
+ worklog: "worklog",
4934
+ decisionLog: "decision-log",
4935
+ runbooks: "runbooks",
4936
+ hubs: "hubs"
4937
+ };
4938
+ async function runStatus(input) {
4939
+ const { dataDir } = input.context;
4940
+ const profilePath = join22(dataDir, "_memory", "user_profile.md");
4941
+ const initialized = existsSync8(profilePath);
4942
+ const counts = {
4943
+ memory: await safeCount(join22(dataDir, "_memory"), false),
4944
+ worklog: await safeCount(join22(dataDir, "worklog"), true),
4945
+ decisionLog: await safeCount(join22(dataDir, "decision-log"), false),
4946
+ runbooks: await safeCount(join22(dataDir, "runbooks"), false),
4947
+ hubs: await safeCount(join22(dataDir, "hubs"), false)
4948
+ };
4949
+ let latestWorklog;
4950
+ try {
4951
+ const store = new WorklogStore(join22(dataDir, "worklog"));
4952
+ const latest = await store.getLatest();
4953
+ if (latest) {
4954
+ latestWorklog = {
4955
+ date: latest.date,
4956
+ keyword: latest.keyword,
4957
+ path: latest.path
4958
+ };
4959
+ }
4960
+ } catch {
4961
+ }
4962
+ let profile;
4963
+ if (initialized) {
4964
+ try {
4965
+ const raw = await readFile17(profilePath, "utf8");
4966
+ const { body } = parseFrontmatter(raw);
4967
+ profile = extractProfile(body);
4968
+ } catch {
4969
+ }
4970
+ }
4971
+ const missing = [];
4972
+ if (!initialized) {
4973
+ missing.push("_memory/user_profile.md \u2014 run `/vortex init`");
4974
+ }
4975
+ for (const [key, count] of Object.entries(counts)) {
4976
+ if (count === 0) {
4977
+ const dirName = COUNT_KEY_TO_DIR[key];
4978
+ const dirPath = join22(dataDir, dirName);
4979
+ missing.push(existsSync8(dirPath) ? `${dirName}/ is empty` : `${dirName}/ does not exist`);
4980
+ }
4981
+ }
4982
+ const nextActions = [];
4983
+ if (!initialized) {
4984
+ nextActions.push("Run `/vortex init --name <name> --role <role> --task <task>` to set up this instance.");
4985
+ } else {
4986
+ nextActions.push("Run `/session-start` for a fuller session-opening report.", "Run `/log <section-title>` to append a section to today's worklog.");
4987
+ if (counts.decisionLog === 0) {
4988
+ nextActions.push("Try `/decision <slug> <title>` to record your first decision.");
4989
+ }
4990
+ }
4991
+ return {
4992
+ subcommand: "status",
4993
+ status: initialized ? "ok" : "uninitialized",
4994
+ instance: { dataDir, initialized, profile },
4995
+ counts,
4996
+ latestWorklog,
4997
+ missing,
4998
+ nextActions
4999
+ };
5000
+ }
5001
+ function extractProfile(body) {
5002
+ const nameMatch = body.match(/^- \*\*Name\/handle\*\*:\s*(.+)$/m);
5003
+ const roleMatch = body.match(/^- \*\*Role\*\*:\s*(.+)$/m);
5004
+ if (!nameMatch && !roleMatch)
5005
+ return void 0;
5006
+ const out = {};
5007
+ if (nameMatch)
5008
+ out.name = nameMatch[1].trim();
5009
+ if (roleMatch)
5010
+ out.role = roleMatch[1].trim();
5011
+ return out;
5012
+ }
5013
+ async function safeCount(dir, recursive) {
5014
+ if (!existsSync8(dir))
5015
+ return 0;
5016
+ try {
5017
+ return await countMarkdown2(dir, recursive);
5018
+ } catch {
5019
+ return 0;
5020
+ }
5021
+ }
5022
+ async function countMarkdown2(dir, recursive) {
5023
+ let total = 0;
5024
+ const entries = await readdir15(dir, { withFileTypes: true });
5025
+ for (const e of entries) {
5026
+ if (e.isFile()) {
5027
+ if (!e.name.endsWith(".md"))
5028
+ continue;
5029
+ if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md") {
5030
+ continue;
5031
+ }
5032
+ if (e.name.startsWith("_TEMPLATE"))
5033
+ continue;
5034
+ total++;
5035
+ } else if (e.isDirectory() && recursive) {
5036
+ if (e.name.startsWith(".") || e.name.startsWith("_"))
5037
+ continue;
5038
+ total += await countMarkdown2(join22(dir, e.name), recursive);
5039
+ }
5040
+ }
5041
+ return total;
5042
+ }
5043
+ var IMPORT_SKIP_DIRS = /* @__PURE__ */ new Set([
5044
+ ".git",
5045
+ "node_modules",
5046
+ ".vscode",
5047
+ ".idea",
5048
+ "dist",
5049
+ "build",
5050
+ ".obsidian",
5051
+ ".trash"
5052
+ ]);
5053
+ var IMPORT_SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
5054
+ var WORKLOG_FOLDER_NAMES = /* @__PURE__ */ new Set([
5055
+ "worklog",
5056
+ "til",
5057
+ "daily",
5058
+ "journal",
5059
+ "diary",
5060
+ "daily-notes",
5061
+ "logs",
5062
+ "log",
5063
+ "\uC77C\uC9C0",
5064
+ "\uB0A0\uC9DC\uBCC4"
5065
+ ]);
5066
+ var DECISION_FOLDER_NAMES = /* @__PURE__ */ new Set([
5067
+ "decision-log",
5068
+ "decisions",
5069
+ "decision"
5070
+ ]);
5071
+ var RUNBOOK_FOLDER_NAMES = /* @__PURE__ */ new Set(["runbooks", "runbook", "sop"]);
5072
+ var HUB_FOLDER_NAMES = /* @__PURE__ */ new Set(["hubs", "hub", "_hub"]);
5073
+ var MEMORY_FOLDER_NAMES = /* @__PURE__ */ new Set(["_memory", "memory", "memories"]);
5074
+ var LEGACY_WORKLOG_TYPES = /* @__PURE__ */ new Set([
5075
+ "til",
5076
+ "daily",
5077
+ "journal",
5078
+ "diary",
5079
+ "log"
5080
+ ]);
5081
+ var FILENAME_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}-/;
5082
+ function parseImportArgs(tokens) {
5083
+ const args = {};
5084
+ for (let i = 0; i < tokens.length; i++) {
5085
+ const t = tokens[i];
5086
+ if (t === "--dry-run") {
5087
+ args.dryRun = true;
5088
+ continue;
5089
+ }
5090
+ if (t === "--from" && i + 1 < tokens.length) {
5091
+ args.from = tokens[++i];
5092
+ continue;
5093
+ }
5094
+ }
5095
+ return args;
5096
+ }
5097
+ async function runImport(input, tokens) {
5098
+ const args = parseImportArgs(tokens);
5099
+ const { dataDir } = input.context;
5100
+ const emptyClassified = {
5101
+ worklog: 0,
5102
+ decisionLog: 0,
5103
+ runbooks: 0,
5104
+ hubs: 0,
5105
+ memory: 0,
5106
+ preserved: 0
5107
+ };
5108
+ if (!args.from) {
5109
+ return {
5110
+ subcommand: "import",
5111
+ status: "needs-input",
5112
+ totalFiles: 0,
5113
+ copied: 0,
5114
+ classified: emptyClassified,
5115
+ frontmatterInjected: 0,
5116
+ frontmatterPreserved: 0,
5117
+ systemDirsCreated: [],
5118
+ skipped: 0,
5119
+ missingInputs: [
5120
+ {
5121
+ name: "from",
5122
+ prompt: "Where is the folder you want to import? (absolute path, e.g. C:/Users/me/notes)"
5123
+ }
5124
+ ],
5125
+ nextActions: [
5126
+ "Re-run with the path:",
5127
+ ' /vortex import --from "<absolute path>"',
5128
+ "Optional: append --dry-run to preview without copying."
5129
+ ]
5130
+ };
5131
+ }
5132
+ if (!existsSync8(args.from)) {
5133
+ return {
5134
+ subcommand: "import",
5135
+ status: "source-missing",
5136
+ source: args.from,
5137
+ totalFiles: 0,
5138
+ copied: 0,
5139
+ classified: emptyClassified,
5140
+ frontmatterInjected: 0,
5141
+ frontmatterPreserved: 0,
5142
+ systemDirsCreated: [],
5143
+ skipped: 0,
5144
+ nextActions: [
5145
+ `Source folder does not exist: ${args.from}`,
5146
+ "Check the path and re-run."
5147
+ ]
5148
+ };
5149
+ }
5150
+ const systemDirs = [
5151
+ "_memory",
5152
+ "worklog",
5153
+ "decision-log",
5154
+ "runbooks",
5155
+ "hubs",
5156
+ "inbox"
5157
+ ];
5158
+ const systemDirsCreated = [];
5159
+ if (!args.dryRun) {
5160
+ for (const d2 of systemDirs) {
5161
+ const p = join22(dataDir, d2);
5162
+ if (!existsSync8(p)) {
5163
+ await mkdir5(p, { recursive: true });
5164
+ systemDirsCreated.push(d2);
5165
+ }
5166
+ }
5167
+ }
5168
+ const stats = {
5169
+ totalFiles: 0,
5170
+ copied: 0,
5171
+ classified: { ...emptyClassified },
5172
+ frontmatterInjected: 0,
5173
+ frontmatterPreserved: 0,
5174
+ skipped: 0
5175
+ };
5176
+ await walkAndImport(args.from, args.from, dataDir, args.dryRun ?? false, stats);
5177
+ let links;
5178
+ if (!args.dryRun && stats.copied > 0) {
5179
+ try {
5180
+ const check = await checkDirectory(dataDir, { caseInsensitive: true });
5181
+ links = {
5182
+ filesScanned: check.filesScanned,
5183
+ total: check.totalLinks,
5184
+ resolved: check.resolved,
5185
+ broken: check.broken.length,
5186
+ ambiguous: check.ambiguous.length
5187
+ };
5188
+ } catch {
5189
+ }
5190
+ }
5191
+ const nextActions = [];
5192
+ if (args.dryRun) {
5193
+ nextActions.push("This was a dry-run \u2014 no files were copied. Re-run without --dry-run to apply.");
5194
+ } else {
5195
+ nextActions.push("Run `/vortex status` to see the new counts.");
5196
+ if (stats.classified.preserved > 0) {
5197
+ nextActions.push(`${stats.classified.preserved} files preserved your original folder structure (under <dataDir>/<same-paths>).`);
5198
+ }
5199
+ const autoClassified = stats.classified.worklog + stats.classified.decisionLog + stats.classified.runbooks + stats.classified.hubs + stats.classified.memory;
5200
+ if (autoClassified > 0) {
5201
+ nextActions.push(`${autoClassified} files auto-classified into vortex categories (worklog/decision-log/runbooks/hubs/_memory).`);
5202
+ }
5203
+ if (links && (links.broken > 0 || links.ambiguous > 0)) {
5204
+ nextActions.push(`Wiki-link health: ${links.resolved}/${links.total} resolved across ${links.filesScanned} files; ${links.broken} broken, ${links.ambiguous} ambiguous. Review and curate links over time.`);
5205
+ } else if (links && links.total > 0) {
5206
+ nextActions.push(`Wiki-link health: ${links.resolved}/${links.total} resolved across ${links.filesScanned} files \u2014 all clean.`);
2895
5207
  }
2896
- return {
2897
- time: (/* @__PURE__ */ new Date()).toISOString(),
2898
- repoRoot,
2899
- dataDir,
2900
- counts,
2901
- missing
2902
- };
2903
5208
  }
2904
- };
2905
- async function countMarkdown(dir, recursive) {
2906
- let total = 0;
2907
- const entries = await readdir12(dir, { withFileTypes: true });
5209
+ return {
5210
+ subcommand: "import",
5211
+ status: args.dryRun ? "dry-run" : "completed",
5212
+ source: args.from,
5213
+ totalFiles: stats.totalFiles,
5214
+ copied: stats.copied,
5215
+ classified: stats.classified,
5216
+ frontmatterInjected: stats.frontmatterInjected,
5217
+ frontmatterPreserved: stats.frontmatterPreserved,
5218
+ systemDirsCreated,
5219
+ skipped: stats.skipped,
5220
+ links,
5221
+ nextActions
5222
+ };
5223
+ }
5224
+ async function walkAndImport(rootSource, currentDir, dataDir, dryRun, stats) {
5225
+ const entries = await readdir15(currentDir, { withFileTypes: true });
2908
5226
  for (const e of entries) {
2909
- if (e.isFile()) {
2910
- if (!e.name.endsWith(".md"))
5227
+ const sourcePath = join22(currentDir, e.name);
5228
+ if (e.isDirectory()) {
5229
+ if (IMPORT_SKIP_DIRS.has(e.name.toLowerCase()))
2911
5230
  continue;
2912
- if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md") {
5231
+ await walkAndImport(rootSource, sourcePath, dataDir, dryRun, stats);
5232
+ } else if (e.isFile()) {
5233
+ if (IMPORT_SKIP_FILES.has(e.name)) {
5234
+ stats.skipped++;
2913
5235
  continue;
2914
5236
  }
2915
- if (e.name.startsWith("_TEMPLATE"))
2916
- continue;
2917
- total++;
2918
- } else if (e.isDirectory() && recursive) {
2919
- if (e.name.startsWith(".") || e.name.startsWith("_"))
5237
+ if (!e.name.endsWith(".md")) {
5238
+ stats.skipped++;
2920
5239
  continue;
2921
- total += await countMarkdown(join16(dir, e.name), recursive);
5240
+ }
5241
+ stats.totalFiles++;
5242
+ const raw = await readFile17(sourcePath, "utf8");
5243
+ const parsed = parseFrontmatter(raw);
5244
+ const hasFrontmatter = Object.keys(parsed.frontmatter).length > 0;
5245
+ const category = classifyFile(sourcePath, rootSource, e.name, parsed.frontmatter);
5246
+ stats.classified[category]++;
5247
+ if (hasFrontmatter) {
5248
+ stats.frontmatterPreserved++;
5249
+ } else {
5250
+ stats.frontmatterInjected++;
5251
+ }
5252
+ if (!dryRun) {
5253
+ const fileStat = await stat8(sourcePath);
5254
+ const enhanced = enhanceFrontmatter(parsed.frontmatter, category, fileStat.birthtime, fileStat.mtime, sourcePath, rootSource);
5255
+ const targetPath = computeTargetPath(category, sourcePath, rootSource, dataDir, e.name);
5256
+ await mkdir5(dirname4(targetPath), { recursive: true });
5257
+ const out = serializeFrontmatter({
5258
+ frontmatter: enhanced,
5259
+ body: parsed.body
5260
+ });
5261
+ await writeFile10(targetPath, out, "utf8");
5262
+ stats.copied++;
5263
+ }
2922
5264
  }
2923
5265
  }
2924
- return total;
2925
5266
  }
2926
-
2927
- // ../plugins/session-rituals/dist/commands/til.js
2928
- var tilCommand = {
2929
- name: "til",
2930
- description: "Append a section to today's TIL entry.",
2931
- args: [
2932
- {
2933
- name: "section",
2934
- description: "Title for the new `## ` section (rest of the input).",
2935
- required: true
2936
- }
2937
- ],
2938
- handler: async (input) => {
2939
- const sectionTitle = input.rest.trim();
2940
- if (!sectionTitle) {
2941
- throw new Error("`/til` requires a section title.");
2942
- }
2943
- const date = todayIso2();
2944
- const store = new TilStore(`${input.context.dataDir}/til`);
2945
- const todayEntry = await store.get(date);
2946
- if (!todayEntry) {
2947
- throw new Error(`No TIL entry exists for ${date}. Create one first; this command only appends sections.`);
5267
+ function classifyFile(sourcePath, rootSource, filename, frontmatter) {
5268
+ const type = String(frontmatter.type ?? "").toLowerCase();
5269
+ if (type === "worklog" || LEGACY_WORKLOG_TYPES.has(type))
5270
+ return "worklog";
5271
+ if (type === "decision-log" || type === "decision")
5272
+ return "decisionLog";
5273
+ if (type === "runbook")
5274
+ return "runbooks";
5275
+ if (type === "hub")
5276
+ return "hubs";
5277
+ if (type === "memory" || type === "user")
5278
+ return "memory";
5279
+ if (filename.startsWith("_HUB-"))
5280
+ return "hubs";
5281
+ if (FILENAME_DATE_PATTERN.test(filename))
5282
+ return "worklog";
5283
+ const relPath = sourcePath.substring(rootSource.length).replace(/^[/\\]/, "");
5284
+ const parts = relPath.split(/[/\\]/).map((p) => p.toLowerCase());
5285
+ for (const part of parts) {
5286
+ if (WORKLOG_FOLDER_NAMES.has(part))
5287
+ return "worklog";
5288
+ if (DECISION_FOLDER_NAMES.has(part))
5289
+ return "decisionLog";
5290
+ if (RUNBOOK_FOLDER_NAMES.has(part))
5291
+ return "runbooks";
5292
+ if (HUB_FOLDER_NAMES.has(part))
5293
+ return "hubs";
5294
+ if (MEMORY_FOLDER_NAMES.has(part))
5295
+ return "memory";
5296
+ }
5297
+ return "preserved";
5298
+ }
5299
+ function computeTargetPath(category, sourcePath, rootSource, dataDir, filename) {
5300
+ if (category === "preserved") {
5301
+ const relPath = sourcePath.substring(rootSource.length).replace(/^[/\\]/, "");
5302
+ return join22(dataDir, relPath);
5303
+ }
5304
+ if (category === "worklog") {
5305
+ const match = filename.match(/^(\d{4})-(\d{2})-/);
5306
+ if (match) {
5307
+ return join22(dataDir, "worklog", match[1], match[2], filename);
2948
5308
  }
2949
- await appendSection(todayEntry, sectionTitle, "");
2950
- return {
2951
- path: todayEntry.path,
2952
- date: todayEntry.date,
2953
- keyword: todayEntry.keyword,
2954
- sectionTitle
5309
+ const d2 = /* @__PURE__ */ new Date();
5310
+ const y2 = String(d2.getFullYear());
5311
+ const m2 = String(d2.getMonth() + 1).padStart(2, "0");
5312
+ return join22(dataDir, "worklog", y2, m2, filename);
5313
+ }
5314
+ if (category === "decisionLog")
5315
+ return join22(dataDir, "decision-log", filename);
5316
+ if (category === "runbooks")
5317
+ return join22(dataDir, "runbooks", filename);
5318
+ if (category === "hubs")
5319
+ return join22(dataDir, "hubs", filename);
5320
+ if (category === "memory")
5321
+ return join22(dataDir, "_memory", filename);
5322
+ return join22(dataDir, filename);
5323
+ }
5324
+ function enhanceFrontmatter(frontmatter, category, birthtime, mtime, sourcePath, rootSource) {
5325
+ const enhanced = { ...frontmatter };
5326
+ const existingType = String(enhanced.type ?? "").toLowerCase();
5327
+ if (!existingType) {
5328
+ const typeMap = {
5329
+ worklog: "worklog",
5330
+ decisionLog: "decision-log",
5331
+ runbooks: "runbook",
5332
+ hubs: "hub",
5333
+ memory: "memory",
5334
+ preserved: "note"
2955
5335
  };
5336
+ enhanced.type = typeMap[category];
5337
+ } else if (LEGACY_WORKLOG_TYPES.has(existingType)) {
5338
+ enhanced.type = "worklog";
5339
+ }
5340
+ if (Array.isArray(enhanced.tags)) {
5341
+ enhanced.tags = enhanced.tags.map((t) => {
5342
+ const s = String(t).toLowerCase();
5343
+ if (LEGACY_WORKLOG_TYPES.has(s))
5344
+ return "worklog";
5345
+ return t;
5346
+ });
2956
5347
  }
2957
- };
2958
- function todayIso2() {
2959
- const d2 = /* @__PURE__ */ new Date();
5348
+ if (!enhanced.created) {
5349
+ enhanced.created = formatYmd3(birthtime);
5350
+ }
5351
+ if (!enhanced.updated) {
5352
+ enhanced.updated = formatYmd3(mtime);
5353
+ }
5354
+ if (!enhanced.privacy) {
5355
+ const relLower = sourcePath.substring(rootSource.length).toLowerCase();
5356
+ if (relLower.includes("personal-records") || relLower.includes("personal_records")) {
5357
+ enhanced.privacy = "personal";
5358
+ } else {
5359
+ enhanced.privacy = "internal";
5360
+ }
5361
+ }
5362
+ return enhanced;
5363
+ }
5364
+ function formatYmd3(d2) {
2960
5365
  const y2 = d2.getFullYear();
2961
5366
  const m2 = String(d2.getMonth() + 1).padStart(2, "0");
2962
5367
  const day = String(d2.getDate()).padStart(2, "0");
2963
5368
  return `${y2}-${m2}-${day}`;
2964
5369
  }
2965
-
2966
- // ../plugins/session-rituals/dist/commands/vortex.js
2967
- import { existsSync as existsSync4 } from "fs";
2968
- import { mkdir as mkdir2, writeFile as writeFile7 } from "fs/promises";
2969
- import { join as join17 } from "path";
2970
- var PLANNED_SUBS = ["status", "import", "doctor"];
2971
- var vortexCommand = {
2972
- name: "vortex",
2973
- description: "VortEX root command. Subcommands: init | status | import | doctor | help.",
2974
- args: [
2975
- {
2976
- name: "sub",
2977
- description: "Subcommand (init|status|import|doctor|help).",
2978
- required: false
5370
+ var DOCTOR_SYSTEM_DIRS = [
5371
+ "_memory",
5372
+ "worklog",
5373
+ "decision-log",
5374
+ "runbooks",
5375
+ "hubs"
5376
+ ];
5377
+ var RUNBOOK_AGING_DAYS = 90;
5378
+ var NODE_MIN_MAJOR = 18;
5379
+ async function runDoctor(input) {
5380
+ const { dataDir, repoRoot } = input.context;
5381
+ const checks = [];
5382
+ checks.push(checkSystemDirs(dataDir));
5383
+ checks.push(checkUserProfile(dataDir));
5384
+ checks.push(await checkIndexes(dataDir));
5385
+ checks.push(await checkWikilinks(dataDir));
5386
+ checks.push(await checkFrontmatterLint(dataDir));
5387
+ checks.push(await checkRunbookAging(dataDir));
5388
+ checks.push(checkNodeVersion());
5389
+ checks.push(await checkGitRemote(repoRoot));
5390
+ const summary = { pass: 0, warn: 0, fail: 0, info: 0 };
5391
+ for (const c of checks)
5392
+ summary[c.status]++;
5393
+ const status = summary.fail > 0 ? "errors" : summary.warn > 0 ? "warnings" : "ok";
5394
+ const nextActions = [];
5395
+ if (summary.fail > 0) {
5396
+ nextActions.push(`${summary.fail} check(s) failed. Address them before relying on the instance.`);
5397
+ }
5398
+ if (summary.warn > 0) {
5399
+ nextActions.push(`${summary.warn} warning(s). Review the detail lines \u2014 most are recoverable in one command.`);
5400
+ }
5401
+ if (status === "ok") {
5402
+ nextActions.push("All checks pass. Run `/vortex status` for activity counts or `/log <section>` to keep working.");
5403
+ }
5404
+ return { subcommand: "doctor", status, checks, summary, nextActions };
5405
+ }
5406
+ function checkSystemDirs(dataDir) {
5407
+ const missing = DOCTOR_SYSTEM_DIRS.filter((d2) => !existsSync8(join22(dataDir, d2)));
5408
+ if (missing.length === 0) {
5409
+ return {
5410
+ id: "system-dirs",
5411
+ label: "vortex system directories present",
5412
+ status: "pass"
5413
+ };
5414
+ }
5415
+ return {
5416
+ id: "system-dirs",
5417
+ label: "vortex system directories present",
5418
+ status: "fail",
5419
+ detail: `Missing: ${missing.join(", ")}. Run \`/vortex init\` or create them manually.`
5420
+ };
5421
+ }
5422
+ function checkUserProfile(dataDir) {
5423
+ const profilePath = join22(dataDir, "_memory", "user_profile.md");
5424
+ if (existsSync8(profilePath)) {
5425
+ return {
5426
+ id: "user-profile",
5427
+ label: "user_profile.md exists",
5428
+ status: "pass"
5429
+ };
5430
+ }
5431
+ return {
5432
+ id: "user-profile",
5433
+ label: "user_profile.md exists",
5434
+ status: "fail",
5435
+ detail: "Missing _memory/user_profile.md. Run `/vortex init` to bootstrap the instance."
5436
+ };
5437
+ }
5438
+ async function checkIndexes(dataDir) {
5439
+ const missing = [];
5440
+ for (const d2 of DOCTOR_SYSTEM_DIRS) {
5441
+ const dirPath = join22(dataDir, d2);
5442
+ if (!existsSync8(dirPath))
5443
+ continue;
5444
+ const indexPath = join22(dirPath, "_INDEX.md");
5445
+ if (!existsSync8(indexPath))
5446
+ missing.push(`${d2}/_INDEX.md`);
5447
+ }
5448
+ if (missing.length === 0) {
5449
+ return {
5450
+ id: "indexes",
5451
+ label: "_INDEX.md present in each system directory",
5452
+ status: "pass"
5453
+ };
5454
+ }
5455
+ return {
5456
+ id: "indexes",
5457
+ label: "_INDEX.md present in each system directory",
5458
+ status: "warn",
5459
+ detail: `Missing: ${missing.join(", ")}. Run \`/reindex\` to generate them.`
5460
+ };
5461
+ }
5462
+ async function checkWikilinks(dataDir) {
5463
+ try {
5464
+ const result = await checkDirectory(dataDir, { caseInsensitive: true });
5465
+ if (result.totalLinks === 0) {
5466
+ return {
5467
+ id: "wikilinks",
5468
+ label: "wiki links resolve",
5469
+ status: "pass",
5470
+ detail: "No wiki links found."
5471
+ };
2979
5472
  }
2980
- ],
2981
- handler: async (input) => {
2982
- const tokens = tokenize(input.rest);
2983
- const sub = tokens[0] ?? "help";
2984
- const restAfterSub = tokens.slice(1);
2985
- if (sub === "init")
2986
- return runInit(input, restAfterSub);
2987
- if (sub === "help" || sub === "")
2988
- return runHelp();
2989
- if (PLANNED_SUBS.includes(sub)) {
5473
+ const broken = result.broken.length;
5474
+ const ambiguous = result.ambiguous.length;
5475
+ if (broken === 0 && ambiguous === 0) {
2990
5476
  return {
2991
- subcommand: sub,
2992
- status: "not-implemented",
2993
- message: `\`/vortex ${sub}\` is reserved but not yet implemented. Planned in a future phase. Run \`/vortex help\` for available subcommands.`
5477
+ id: "wikilinks",
5478
+ label: "wiki links resolve",
5479
+ status: "pass",
5480
+ detail: `${result.resolved}/${result.totalLinks} resolved across ${result.filesScanned} files.`
2994
5481
  };
2995
5482
  }
2996
5483
  return {
2997
- subcommand: "unknown",
2998
- status: "not-implemented",
2999
- message: `Unknown subcommand "${sub}". Run \`/vortex help\` for the list.`
5484
+ id: "wikilinks",
5485
+ label: "wiki links resolve",
5486
+ status: "warn",
5487
+ detail: `${broken} broken, ${ambiguous} ambiguous out of ${result.totalLinks} total. Curate or run a rewrite pass.`
5488
+ };
5489
+ } catch (e) {
5490
+ return {
5491
+ id: "wikilinks",
5492
+ label: "wiki links resolve",
5493
+ status: "warn",
5494
+ detail: `Could not scan: ${e.message}`
5495
+ };
5496
+ }
5497
+ }
5498
+ async function checkFrontmatterLint(dataDir) {
5499
+ try {
5500
+ const report = await lintDirectory({
5501
+ dir: dataDir,
5502
+ rules: [
5503
+ requireFrontmatter({ required: ["type"] }),
5504
+ privacyValid(),
5505
+ wikiLinkResolves({ searchRoot: dataDir })
5506
+ ]
5507
+ });
5508
+ const errors = report.findings.filter((f) => f.severity === "error").length;
5509
+ const warnings = report.findings.filter((f) => f.severity === "warning").length;
5510
+ if (errors === 0 && warnings === 0) {
5511
+ return {
5512
+ id: "frontmatter-lint",
5513
+ label: "frontmatter / privacy / wiki-link rules",
5514
+ status: "pass",
5515
+ detail: `${report.filesScanned} files scanned, 0 findings.`
5516
+ };
5517
+ }
5518
+ if (errors > 0) {
5519
+ return {
5520
+ id: "frontmatter-lint",
5521
+ label: "frontmatter / privacy / wiki-link rules",
5522
+ status: "fail",
5523
+ detail: `${errors} error(s), ${warnings} warning(s) across ${report.filesScanned} files.`
5524
+ };
5525
+ }
5526
+ return {
5527
+ id: "frontmatter-lint",
5528
+ label: "frontmatter / privacy / wiki-link rules",
5529
+ status: "warn",
5530
+ detail: `${warnings} warning(s) across ${report.filesScanned} files.`
5531
+ };
5532
+ } catch (e) {
5533
+ return {
5534
+ id: "frontmatter-lint",
5535
+ label: "frontmatter / privacy / wiki-link rules",
5536
+ status: "warn",
5537
+ detail: `Could not lint: ${e.message}`
5538
+ };
5539
+ }
5540
+ }
5541
+ async function checkRunbookAging(dataDir) {
5542
+ const runbooksDir = join22(dataDir, "runbooks");
5543
+ if (!existsSync8(runbooksDir)) {
5544
+ return {
5545
+ id: "runbook-aging",
5546
+ label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
5547
+ status: "pass",
5548
+ detail: "No runbooks directory."
5549
+ };
5550
+ }
5551
+ const stale = [];
5552
+ let total = 0;
5553
+ const cutoff = Date.now() - RUNBOOK_AGING_DAYS * 24 * 60 * 60 * 1e3;
5554
+ try {
5555
+ const entries = await readdir15(runbooksDir, { withFileTypes: true });
5556
+ for (const e of entries) {
5557
+ if (!e.isFile() || !e.name.endsWith(".md"))
5558
+ continue;
5559
+ if (e.name === "README.md" || e.name === "_INDEX.md" || e.name.startsWith("_TEMPLATE")) {
5560
+ continue;
5561
+ }
5562
+ total++;
5563
+ const filePath = join22(runbooksDir, e.name);
5564
+ const raw = await readFile17(filePath, "utf8");
5565
+ const { frontmatter } = parseFrontmatter(raw);
5566
+ if (!frontmatter.last_tested) {
5567
+ stale.push(`${e.name} (no last_tested)`);
5568
+ continue;
5569
+ }
5570
+ const testedAt = new Date(String(frontmatter.last_tested)).getTime();
5571
+ if (Number.isNaN(testedAt) || testedAt < cutoff) {
5572
+ stale.push(`${e.name} (${String(frontmatter.last_tested)})`);
5573
+ }
5574
+ }
5575
+ } catch (e) {
5576
+ return {
5577
+ id: "runbook-aging",
5578
+ label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
5579
+ status: "warn",
5580
+ detail: `Could not scan: ${e.message}`
5581
+ };
5582
+ }
5583
+ if (total === 0 || stale.length === 0) {
5584
+ return {
5585
+ id: "runbook-aging",
5586
+ label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
5587
+ status: "pass",
5588
+ detail: total === 0 ? "No runbooks." : `${total} runbook(s), all fresh.`
3000
5589
  };
3001
5590
  }
3002
- };
3003
- function runHelp() {
3004
5591
  return {
3005
- subcommand: "help",
3006
- status: "ok",
3007
- subcommands: [
3008
- {
3009
- name: "init",
3010
- description: "First-time setup wizard. Creates user profile memory, first TIL, first topic hub.",
3011
- state: "active"
3012
- },
3013
- {
3014
- name: "status",
3015
- description: "Show instance state (memory count, latest TIL, missing skeletons).",
3016
- state: "planned"
3017
- },
3018
- {
3019
- name: "import",
3020
- description: "Bring an existing folder into data/imported/ for gradual sorting.",
3021
- state: "planned"
3022
- },
3023
- {
3024
- name: "doctor",
3025
- description: "Diagnose common setup issues (missing modules, broken links).",
3026
- state: "planned"
3027
- },
3028
- { name: "help", description: "Show this list.", state: "active" }
3029
- ]
5592
+ id: "runbook-aging",
5593
+ label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
5594
+ status: "warn",
5595
+ detail: `${stale.length}/${total} runbook(s) stale or untested: ${stale.slice(0, 3).join("; ")}${stale.length > 3 ? "\u2026" : ""}. Even rarely-used runbooks stay valuable \u2014 re-verify when the environment changes, do not delete.`
3030
5596
  };
3031
5597
  }
3032
- async function runInit(input, tokens) {
3033
- const args = parseInitArgs(tokens);
3034
- const { dataDir } = input.context;
3035
- const requiredDirs = ["_memory", "til", "decision-log", "hubs", "inbox"];
3036
- for (const d2 of requiredDirs) {
3037
- const p = join17(dataDir, d2);
3038
- if (!existsSync4(p))
3039
- await mkdir2(p, { recursive: true });
5598
+ function checkNodeVersion() {
5599
+ const raw = process.version;
5600
+ const match = raw.match(/^v?(\d+)\./);
5601
+ if (!match) {
5602
+ return {
5603
+ id: "node-version",
5604
+ label: `node >= ${NODE_MIN_MAJOR}`,
5605
+ status: "warn",
5606
+ detail: `Could not parse node version "${raw}".`
5607
+ };
3040
5608
  }
3041
- const profilePath = join17(dataDir, "_memory", "user_profile.md");
3042
- if (existsSync4(profilePath) && !args.force) {
5609
+ const major = Number.parseInt(match[1], 10);
5610
+ if (major >= NODE_MIN_MAJOR) {
3043
5611
  return {
3044
- subcommand: "init",
3045
- status: "already-initialized",
3046
- created: [],
3047
- nextActions: [
3048
- `VortEX instance is already initialized (${profilePath} exists).`,
3049
- "To re-run, pass `--force` (existing user_profile / first TIL / first hub will be overwritten).",
3050
- "To check current state, try `/session-start`."
3051
- ]
5612
+ id: "node-version",
5613
+ label: `node >= ${NODE_MIN_MAJOR}`,
5614
+ status: "pass",
5615
+ detail: `Running ${raw}.`
3052
5616
  };
3053
5617
  }
3054
- const missing = [];
3055
- if (!args.name) {
3056
- missing.push({
3057
- name: "name",
3058
- prompt: 'What name or handle should VortEX use for you? (e.g. "Alex" or "team-lead")'
3059
- });
5618
+ return {
5619
+ id: "node-version",
5620
+ label: `node >= ${NODE_MIN_MAJOR}`,
5621
+ status: "fail",
5622
+ detail: `Found ${raw}. TypeScript modules require node ${NODE_MIN_MAJOR} or later. Upgrade node.`
5623
+ };
5624
+ }
5625
+ async function checkGitRemote(repoRoot) {
5626
+ const gitConfig = join22(repoRoot, ".git", "config");
5627
+ if (!existsSync8(gitConfig)) {
5628
+ return {
5629
+ id: "git-remote",
5630
+ label: "git remote for sync",
5631
+ status: "info",
5632
+ detail: "Not a git repository. VortEX works fine without git, but git + a hosted remote (GitHub, Gitea, GitLab) is recommended if you want to sync across machines (work / home / USB)."
5633
+ };
3060
5634
  }
3061
- if (!args.role) {
3062
- missing.push({
3063
- name: "role",
3064
- prompt: 'What is your main role in one word? (e.g. "engineer", "researcher", "writer")'
3065
- });
5635
+ try {
5636
+ const raw = await readFile17(gitConfig, "utf8");
5637
+ const match = raw.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(.+)/);
5638
+ if (!match) {
5639
+ return {
5640
+ id: "git-remote",
5641
+ label: "git remote for sync",
5642
+ status: "info",
5643
+ detail: "Git repo present but no `origin` remote configured. Add one (`git remote add origin <url>`) if you want cross-machine sync."
5644
+ };
5645
+ }
5646
+ return {
5647
+ id: "git-remote",
5648
+ label: "git remote for sync",
5649
+ status: "pass",
5650
+ detail: `origin = ${match[1].trim()}`
5651
+ };
5652
+ } catch (e) {
5653
+ return {
5654
+ id: "git-remote",
5655
+ label: "git remote for sync",
5656
+ status: "info",
5657
+ detail: `Could not read .git/config: ${e.message}`
5658
+ };
3066
5659
  }
3067
- if (!args.task) {
3068
- missing.push({
3069
- name: "task",
3070
- prompt: "What is one thing you're working on right now? (one sentence \u2014 this becomes your first TIL seed)"
3071
- });
5660
+ }
5661
+ async function detectExternalFolders(excludePath) {
5662
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5663
+ if (!home)
5664
+ return void 0;
5665
+ const candidates = [
5666
+ join22(home, "Documents", "obsidian-vault"),
5667
+ join22(home, "Documents", "notes"),
5668
+ join22(home, "Documents", "Notebook"),
5669
+ join22(home, "notes"),
5670
+ join22(home, "Notes")
5671
+ ];
5672
+ const excludeNorm = excludePath.replace(/[/\\]+$/, "");
5673
+ const found = [];
5674
+ for (const candidate of candidates) {
5675
+ const candNorm = candidate.replace(/[/\\]+$/, "");
5676
+ if (candNorm === excludeNorm || candNorm.startsWith(excludeNorm + "/") || candNorm.startsWith(excludeNorm + "\\") || excludeNorm.startsWith(candNorm + "/") || excludeNorm.startsWith(candNorm + "\\")) {
5677
+ continue;
5678
+ }
5679
+ if (!existsSync8(candidate))
5680
+ continue;
5681
+ let mdCount = 0;
5682
+ try {
5683
+ mdCount = await countMarkdown2(candidate, true);
5684
+ } catch {
5685
+ continue;
5686
+ }
5687
+ if (mdCount === 0)
5688
+ continue;
5689
+ found.push({ path: candidate, basename: basename7(candidate), mdCount });
3072
5690
  }
3073
- if (missing.length > 0) {
5691
+ return found.length > 0 ? found : void 0;
5692
+ }
5693
+ function parseSyncArgs(tokens) {
5694
+ const args = {};
5695
+ for (const t of tokens) {
5696
+ if (t === "--skip-pull")
5697
+ args.skipPull = true;
5698
+ else if (t === "--skip-install")
5699
+ args.skipInstall = true;
5700
+ else if (t === "--skip-build")
5701
+ args.skipBuild = true;
5702
+ else if (t === "--skip-verify")
5703
+ args.skipVerify = true;
5704
+ else if (t === "--dry-run")
5705
+ args.dryRun = true;
5706
+ }
5707
+ return args;
5708
+ }
5709
+ async function runSync(input, tokens) {
5710
+ const args = parseSyncArgs(tokens);
5711
+ const { repoRoot } = input.context;
5712
+ const plan = [
5713
+ {
5714
+ id: "pull",
5715
+ label: "git pull",
5716
+ cmd: "git",
5717
+ cmdArgs: ["pull"],
5718
+ skip: args.skipPull ?? false
5719
+ },
5720
+ {
5721
+ id: "install",
5722
+ label: "npm install",
5723
+ cmd: "npm",
5724
+ cmdArgs: ["install"],
5725
+ skip: args.skipInstall ?? false
5726
+ },
5727
+ {
5728
+ id: "build",
5729
+ label: "npm run build",
5730
+ cmd: "npm",
5731
+ cmdArgs: ["run", "build"],
5732
+ skip: args.skipBuild ?? false
5733
+ },
5734
+ {
5735
+ id: "verify",
5736
+ label: "npm run verify",
5737
+ cmd: "npm",
5738
+ cmdArgs: ["run", "verify"],
5739
+ skip: args.skipVerify ?? false
5740
+ }
5741
+ ];
5742
+ if (args.dryRun) {
5743
+ const steps2 = plan.map((p) => ({
5744
+ id: p.id,
5745
+ label: p.label,
5746
+ status: p.skip ? "skipped" : "ok"
5747
+ }));
3074
5748
  return {
3075
- subcommand: "init",
3076
- status: "needs-input",
3077
- created: [],
3078
- missingInputs: missing,
5749
+ subcommand: "sync",
5750
+ status: "dry-run",
5751
+ steps: steps2,
3079
5752
  nextActions: [
3080
- "Ask the user the prompts in `missingInputs`, then re-run with the answers:",
3081
- ' /vortex init --name "<name>" --role "<role>" --task "<task>"',
3082
- "Optional: append `--force` to overwrite an already-initialized instance."
5753
+ "Dry-run only \u2014 no commands executed.",
5754
+ "Re-run without --dry-run to apply."
3083
5755
  ]
3084
5756
  };
3085
5757
  }
3086
- const today2 = todayIso3();
3087
- const created = [];
3088
- await writeFile7(profilePath, renderUserProfile(args.name, args.role, args.task, today2), "utf8");
3089
- created.push(profilePath);
3090
- const [year, month] = today2.split("-");
3091
- const tilDir = join17(dataDir, "til", year, month);
3092
- await mkdir2(tilDir, { recursive: true });
3093
- const tilPath = join17(tilDir, `${today2}-vortex-init.md`);
3094
- await writeFile7(tilPath, renderFirstTil(args.name, args.role, args.task, today2), "utf8");
3095
- created.push(tilPath);
3096
- const hubSlug = slugify(args.role);
3097
- const hubPath = join17(dataDir, "hubs", `_HUB-${hubSlug}.md`);
3098
- await writeFile7(hubPath, renderFirstHub(args.role, today2), "utf8");
3099
- created.push(hubPath);
5758
+ const steps = [];
5759
+ for (const p of plan) {
5760
+ if (p.skip) {
5761
+ steps.push({ id: p.id, label: p.label, status: "skipped" });
5762
+ continue;
5763
+ }
5764
+ const result = await runShellCommand(p.cmd, p.cmdArgs, repoRoot);
5765
+ const step = {
5766
+ id: p.id,
5767
+ label: p.label,
5768
+ status: result.exitCode === 0 ? "ok" : "failed",
5769
+ exitCode: result.exitCode,
5770
+ durationMs: result.durationMs,
5771
+ stdoutTail: result.stdoutTail,
5772
+ stderrTail: result.stderrTail
5773
+ };
5774
+ steps.push(step);
5775
+ if (step.status === "failed") {
5776
+ return {
5777
+ subcommand: "sync",
5778
+ status: "failed",
5779
+ steps,
5780
+ failedAt: p.id,
5781
+ nextActions: [
5782
+ `\`${p.label}\` failed with exit code ${result.exitCode}.`,
5783
+ "Review the stdoutTail / stderrTail and fix the underlying issue, then re-run.",
5784
+ "You can skip already-passed earlier steps with --skip-pull / --skip-install / --skip-build / --skip-verify."
5785
+ ]
5786
+ };
5787
+ }
5788
+ }
5789
+ const ranCount = steps.filter((s) => s.status === "ok").length;
3100
5790
  return {
3101
- subcommand: "init",
5791
+ subcommand: "sync",
3102
5792
  status: "completed",
3103
- created,
5793
+ steps,
3104
5794
  nextActions: [
3105
- `Done. Created ${created.length} files.`,
3106
- "Next 3 things you can try right now:",
3107
- " /til <one-line update> \u2014 append a section to today's TIL",
3108
- " /decision <slug> <title> \u2014 record a decision",
3109
- " /session-start \u2014 daily start-of-session report",
3110
- `Open ${tilPath} to see your first TIL \u2014 it already names "${args.task}".`
5795
+ `All ${ranCount} step(s) passed. Your framework checkout is up-to-date and verified.`,
5796
+ "Run `/vortex status` for an instance-side snapshot."
3111
5797
  ]
3112
5798
  };
3113
5799
  }
3114
- function parseInitArgs(tokens) {
3115
- const args = {};
3116
- for (let i = 0; i < tokens.length; i++) {
3117
- const t = tokens[i];
3118
- if (t === "--force") {
3119
- args.force = true;
5800
+ var SYNC_TAIL_LENGTH = 1e3;
5801
+ async function runShellCommand(cmd, cmdArgs, cwd) {
5802
+ const start = Date.now();
5803
+ return new Promise((resolve4) => {
5804
+ let stdout = "";
5805
+ let stderr = "";
5806
+ const child = spawn(cmd, [...cmdArgs], { cwd, shell: true });
5807
+ child.stdout?.on("data", (chunk) => {
5808
+ stdout += chunk.toString("utf8");
5809
+ });
5810
+ child.stderr?.on("data", (chunk) => {
5811
+ stderr += chunk.toString("utf8");
5812
+ });
5813
+ child.on("close", (code) => {
5814
+ resolve4({
5815
+ exitCode: code ?? -1,
5816
+ durationMs: Date.now() - start,
5817
+ stdoutTail: tailString(stdout, SYNC_TAIL_LENGTH),
5818
+ stderrTail: tailString(stderr, SYNC_TAIL_LENGTH)
5819
+ });
5820
+ });
5821
+ child.on("error", (err) => {
5822
+ resolve4({
5823
+ exitCode: -1,
5824
+ durationMs: Date.now() - start,
5825
+ stdoutTail: tailString(stdout, SYNC_TAIL_LENGTH),
5826
+ stderrTail: tailString(stderr + "\n[spawn error] " + err.message, SYNC_TAIL_LENGTH)
5827
+ });
5828
+ });
5829
+ });
5830
+ }
5831
+ function tailString(s, n) {
5832
+ if (s.length <= n)
5833
+ return s;
5834
+ return "..." + s.slice(-n);
5835
+ }
5836
+
5837
+ // ../plugins/session-rituals/dist/agenda.js
5838
+ var DEFAULT_RECENT = 7;
5839
+ var DEFAULT_MAX = 8;
5840
+ function worklogTitle(entry) {
5841
+ const m2 = entry.body.match(/^#\s+(.+)$/m);
5842
+ if (m2)
5843
+ return m2[1].trim();
5844
+ return entry.keyword || entry.date;
5845
+ }
5846
+ function extractNextUp(body, max = 8) {
5847
+ const lines = body.split(/\r?\n/);
5848
+ const headingRe = /^(#{1,6})\s+(.*)$/;
5849
+ const cueRe = /(다음\s*작업|다음\s*세션|후속|next\s*up|next|todo|to-do|📋)/i;
5850
+ let collecting = false;
5851
+ let startLevel = 0;
5852
+ const out = [];
5853
+ for (const line of lines) {
5854
+ const h = line.match(headingRe);
5855
+ if (h) {
5856
+ const level = h[1].length;
5857
+ if (collecting && level <= startLevel)
5858
+ break;
5859
+ if (!collecting && cueRe.test(h[2])) {
5860
+ collecting = true;
5861
+ startLevel = level;
5862
+ continue;
5863
+ }
3120
5864
  continue;
3121
5865
  }
3122
- if (t === "--name" && i + 1 < tokens.length) {
3123
- args.name = tokens[++i];
5866
+ if (!collecting)
3124
5867
  continue;
3125
- }
3126
- if (t === "--role" && i + 1 < tokens.length) {
3127
- args.role = tokens[++i];
5868
+ const trimmed = line.trim();
5869
+ if (trimmed.length === 0)
3128
5870
  continue;
3129
- }
3130
- if (t === "--task" && i + 1 < tokens.length) {
3131
- args.task = tokens[++i];
5871
+ if (trimmed.startsWith(">"))
3132
5872
  continue;
3133
- }
5873
+ const cleaned = trimmed.replace(/^[-*]\s+\[[ xX]\]\s+/, "").replace(/^[-*]\s+/, "").replace(/^\d+[.)]\s+/, "").trim();
5874
+ if (cleaned.length === 0)
5875
+ continue;
5876
+ out.push(cleaned);
5877
+ if (out.length >= max)
5878
+ break;
3134
5879
  }
3135
- return args;
5880
+ return out;
3136
5881
  }
3137
- function tokenize(s) {
5882
+ function extractOpenTasks(body) {
3138
5883
  const out = [];
3139
- let i = 0;
3140
- while (i < s.length) {
3141
- while (i < s.length && /\s/.test(s[i]))
3142
- i++;
3143
- if (i >= s.length)
5884
+ for (const line of body.split(/\r?\n/)) {
5885
+ const m2 = line.match(/^\s*[-*]\s+\[\s\]\s+(.+\S)\s*$/);
5886
+ if (m2)
5887
+ out.push(m2[1].trim());
5888
+ }
5889
+ return out;
5890
+ }
5891
+ async function collectAgenda(ctx, opts) {
5892
+ const recentN = opts?.recentWorklogs ?? DEFAULT_RECENT;
5893
+ const maxTasks = opts?.maxTasks ?? DEFAULT_MAX;
5894
+ const maxDecisions = opts?.maxDecisions ?? DEFAULT_MAX;
5895
+ const worklogStore = new WorklogStore(`${ctx.dataDir}/worklog`);
5896
+ const decisionStore = new DecisionStore(joinDecisionRoot(ctx));
5897
+ const allWorklogs = await worklogStore.list();
5898
+ const sortedWorklogs = [...allWorklogs].sort((a, b2) => a.date < b2.date ? 1 : a.date > b2.date ? -1 : 0);
5899
+ const recent = sortedWorklogs.slice(0, recentN);
5900
+ const lastWorklog = sortedWorklogs[0] ? { date: sortedWorklogs[0].date, title: worklogTitle(sortedWorklogs[0]), path: sortedWorklogs[0].path } : null;
5901
+ const openTasks = [];
5902
+ for (const wl of recent) {
5903
+ for (const text of extractOpenTasks(wl.body)) {
5904
+ openTasks.push({ text, fromDate: wl.date });
5905
+ if (openTasks.length >= maxTasks)
5906
+ break;
5907
+ }
5908
+ if (openTasks.length >= maxTasks)
3144
5909
  break;
3145
- const ch = s[i];
3146
- if (ch === '"' || ch === "'") {
3147
- const quote = ch;
3148
- i++;
3149
- let buf = "";
3150
- while (i < s.length && s[i] !== quote) {
3151
- buf += s[i++];
3152
- }
3153
- if (i < s.length)
3154
- i++;
3155
- out.push(buf);
3156
- } else {
3157
- let buf = "";
3158
- while (i < s.length && !/\s/.test(s[i])) {
3159
- buf += s[i++];
3160
- }
3161
- out.push(buf);
5910
+ }
5911
+ const newest = sortedWorklogs[0];
5912
+ const nextUp = newest ? extractNextUp(newest.body, maxTasks) : [];
5913
+ const nextUpFrom = newest && nextUp.length > 0 ? newest.date : null;
5914
+ const allDecisions = await decisionStore.list();
5915
+ const active = allDecisions.filter((d2) => {
5916
+ const s = (d2.frontmatter?.status ?? "active").toLowerCase();
5917
+ return s !== "archived" && s !== "template";
5918
+ });
5919
+ const sortedDecisions = [...active].sort((a, b2) => a.date < b2.date ? 1 : a.date > b2.date ? -1 : 0);
5920
+ const openDecisions = sortedDecisions.slice(0, maxDecisions).map((d2) => ({
5921
+ title: decisionTitle(d2),
5922
+ date: d2.date,
5923
+ slug: d2.slug
5924
+ }));
5925
+ const worklogCount = allWorklogs.length;
5926
+ const decisionCount = allDecisions.length;
5927
+ const isEmpty = worklogCount === 0 && decisionCount === 0;
5928
+ const nothingOpen = !isEmpty && openTasks.length === 0 && openDecisions.length === 0 && nextUp.length === 0;
5929
+ return {
5930
+ lastWorklog,
5931
+ nextUp,
5932
+ nextUpFrom,
5933
+ openTasks,
5934
+ openDecisions,
5935
+ worklogCount,
5936
+ decisionCount,
5937
+ isEmpty,
5938
+ nothingOpen
5939
+ };
5940
+ }
5941
+ function joinDecisionRoot(ctx) {
5942
+ return `${ctx.dataDir}/decision-log`;
5943
+ }
5944
+ function decisionTitle(d2) {
5945
+ const m2 = (d2.body ?? "").match(/^#\s+(.+)$/m);
5946
+ if (m2)
5947
+ return m2[1].trim();
5948
+ return d2.slug;
5949
+ }
5950
+ function renderAgenda(report) {
5951
+ const lines = ["## What should I do today?", ""];
5952
+ if (report.isEmpty) {
5953
+ lines.push("- No worklog or decisions yet \u2014 this looks like a fresh instance.");
5954
+ lines.push("- Start with `/vortex init` (if you haven't), then `/log <one-line update>` as you work.");
5955
+ lines.push("- A worklog entry per working day is the seed; everything else grows from it.");
5956
+ return lines.join("\n") + "\n";
5957
+ }
5958
+ if (report.lastWorklog) {
5959
+ lines.push(`- last active: ${report.lastWorklog.date} \u2014 ${report.lastWorklog.title}`);
5960
+ }
5961
+ if (report.nextUp.length > 0) {
5962
+ lines.push(`- next up (planned, from ${report.nextUpFrom}):`);
5963
+ for (const n of report.nextUp) {
5964
+ lines.push(` - ${n}`);
3162
5965
  }
3163
5966
  }
3164
- return out;
5967
+ if (report.openTasks.length > 0) {
5968
+ lines.push(`- open tasks (${report.openTasks.length}):`);
5969
+ for (const t of report.openTasks) {
5970
+ lines.push(` - [ ] ${t.text} (${t.fromDate})`);
5971
+ }
5972
+ }
5973
+ if (report.openDecisions.length > 0) {
5974
+ lines.push(`- open decisions (${report.openDecisions.length}):`);
5975
+ for (const d2 of report.openDecisions) {
5976
+ lines.push(` - ${d2.title} (${d2.date})`);
5977
+ }
5978
+ }
5979
+ if (report.nothingOpen) {
5980
+ lines.push(`- nothing open in recent worklogs \u2014 you're clear. ${report.worklogCount} worklog(s), ${report.decisionCount} decision(s) on record.`);
5981
+ lines.push("- Leave a `## \uB2E4\uC74C \uC791\uC5C5` (or `## Next`) section in a worklog, or `- [ ] <task>` lines, to have them surface here next time.");
5982
+ }
5983
+ return lines.join("\n") + "\n";
3165
5984
  }
3166
- function renderUserProfile(name, role, task, date) {
3167
- return `---
3168
- name: user-profile
3169
- description: Operator profile captured by /vortex init.
3170
- type: user
3171
- created: ${date}
3172
- updated: ${date}
3173
- ---
3174
-
3175
- # User Profile
3176
-
3177
- - **Name/handle**: ${name}
3178
- - **Role**: ${role}
3179
- - **Initial focus**: ${task}
3180
5985
 
3181
- This memory was created by \`/vortex init\` on ${date}. Edit freely as your role evolves.
5986
+ // ../plugins/session-rituals/dist/commands/agenda.js
5987
+ var agendaCommand = {
5988
+ name: "agenda",
5989
+ description: "What should I do today? Synthesize open tasks (- [ ] in recent worklogs), active decisions, and last activity from existing records.",
5990
+ handler: async (input) => {
5991
+ const opts = {};
5992
+ return collectAgenda(input.context, opts);
5993
+ }
5994
+ };
3182
5995
 
3183
- Linked: [[_HUB-${slugify(role)}]] \u2014 primary topic hub.
3184
- `;
5996
+ // ../plugins/session-rituals/dist/registry.js
5997
+ function createRitualRegistry(options) {
5998
+ const registry = new CommandRegistry();
5999
+ registry.register(sessionStartCommand);
6000
+ registry.register(reindexCommand);
6001
+ registry.register(decisionCommand);
6002
+ registry.register(logCommand);
6003
+ registry.register(vortexCommand);
6004
+ registry.register(agendaCommand);
6005
+ if (options?.curate) {
6006
+ registry.register(curateCommand(options.curate));
6007
+ }
6008
+ if (options?.recall) {
6009
+ registry.register(recallCommand(options.recall));
6010
+ }
6011
+ return registry;
3185
6012
  }
3186
- function renderFirstTil(name, role, task, date) {
3187
- return `---
3188
- type: til
3189
- status: active
3190
- created: ${date}
3191
- updated: ${date}
3192
- tags: [til, onboarding]
3193
- ---
3194
-
3195
- # ${date} \u2014 VortEX \uC2DC\uC791
3196
-
3197
- > First TIL, created by \`/vortex init\`. ${name} (${role}). Today's focus: ${task}
3198
-
3199
- ## What I'm working on
3200
-
3201
- ${task}
3202
-
3203
- ## Notes
3204
6013
 
3205
- (append more with \`/til <section-title>\`)
6014
+ // ../plugins/session-rituals/dist/cli-dispatch.js
6015
+ import { execFileSync } from "child_process";
6016
+ import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
6017
+ import { hostname } from "os";
6018
+ import { join as join25 } from "path";
3206
6019
 
3207
- ## Next
6020
+ // ../plugins/session-rituals/dist/session-start-report.js
6021
+ import { existsSync as existsSync9 } from "fs";
6022
+ import { readdir as readdir16, readFile as readFile18 } from "fs/promises";
6023
+ import { join as join23 } from "path";
6024
+ var COUNTED_DIRS2 = ["_memory", "worklog", "decision-log"];
6025
+ var DEFAULT_GAP_WINDOW_DAYS = 30;
6026
+ var BOOT_BANNER = String.raw`
6027
+ __ __ _ _____ __
6028
+ \ \ / ___ _ _| |_| __\ \/ /
6029
+ \ V / _ | '_| _| _| > <
6030
+ \_/\___|_| \__|___/_/\_\
6031
+ Vortex absorbs context · EX executes it`;
6032
+ async function collectSessionStartReport(ctx, opts) {
6033
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
6034
+ const counts = {};
6035
+ const missing = [];
6036
+ for (const name of COUNTED_DIRS2) {
6037
+ const dir = join23(ctx.dataDir, name);
6038
+ if (!existsSync9(dir)) {
6039
+ missing.push(name);
6040
+ counts[name] = 0;
6041
+ continue;
6042
+ }
6043
+ counts[name] = await countMarkdown3(dir, name === "worklog");
6044
+ }
6045
+ const { recent, dates } = await scanWorklog(ctx.dataDir);
6046
+ const cutoff = isoDate(addDays(now, -(opts?.gapWindowDays ?? DEFAULT_GAP_WINDOW_DAYS)));
6047
+ const recentWorklogDates = dates.filter((d2) => d2 >= cutoff);
6048
+ return {
6049
+ time: now.toISOString(),
6050
+ repoRoot: ctx.repoRoot,
6051
+ dataDir: ctx.dataDir,
6052
+ counts,
6053
+ missing,
6054
+ recentWorklog: recent,
6055
+ recentWorklogDates,
6056
+ environment: opts?.environment ?? null
6057
+ };
6058
+ }
6059
+ function detectWorklogGaps(commitDays, presentDates) {
6060
+ const present = new Set(presentDates);
6061
+ return [...new Set(commitDays)].filter((d2) => d2 && !present.has(d2)).sort();
6062
+ }
6063
+ function renderSessionStartReport(report, extras) {
6064
+ const lines = [BOOT_BANNER, ""];
6065
+ const env = report.environment ? ` \xB7 env: ${report.environment}` : "";
6066
+ lines.push(`- time: ${report.time}${env}`);
6067
+ const git = extras?.git;
6068
+ if (git?.ran) {
6069
+ lines.push(git.conflict ? `- git: \u26A0\uFE0F ${git.summary} \u2014 resolve manually (not auto-resolved)` : `- git: ${git.summary}`);
6070
+ }
6071
+ const countStr = COUNTED_DIRS2.map((d2) => `${d2} ${report.counts[d2] ?? 0}`).join(" \xB7 ");
6072
+ const miss = report.missing.length ? ` (missing: ${report.missing.join(", ")})` : "";
6073
+ lines.push(`- data: ${countStr}${miss}`);
6074
+ lines.push(report.recentWorklog ? `- last worklog: ${report.recentWorklog.title} (${report.recentWorklog.path})` : `- last worklog: none yet`);
6075
+ const gaps = extras?.missingWorklogDays ?? [];
6076
+ if (gaps.length) {
6077
+ lines.push(`- \u26A0\uFE0F work without a worklog: ${gaps.join(", ")} \u2014 backfill from that day's commits`);
6078
+ }
6079
+ const cu = extras?.catchUp;
6080
+ if (cu && (cu.ingestedLocal > 0 || cu.indexedPulled > 0 || cu.errors > 0)) {
6081
+ const parts = [];
6082
+ if (cu.ingestedLocal > 0)
6083
+ parts.push(`${cu.ingestedLocal} new`);
6084
+ if (cu.indexedPulled > 0)
6085
+ parts.push(`${cu.indexedPulled} pulled`);
6086
+ const n = cu.ingestedLocal + cu.indexedPulled;
6087
+ let line = `- caught up: archived ${parts.join(" + ")} conversation${n === 1 ? "" : "s"}`;
6088
+ if (cu.errors > 0)
6089
+ line += ` (${cu.errors} error${cu.errors === 1 ? "" : "s"})`;
6090
+ lines.push(line);
6091
+ }
6092
+ return lines.join("\n") + "\n";
6093
+ }
6094
+ async function countMarkdown3(dir, recursive) {
6095
+ let total = 0;
6096
+ const entries = await readdir16(dir, { withFileTypes: true });
6097
+ for (const e of entries) {
6098
+ if (e.isFile()) {
6099
+ if (!e.name.endsWith(".md"))
6100
+ continue;
6101
+ if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md")
6102
+ continue;
6103
+ if (e.name.startsWith("_TEMPLATE"))
6104
+ continue;
6105
+ total++;
6106
+ } else if (e.isDirectory() && recursive) {
6107
+ if (e.name.startsWith(".") || e.name.startsWith("_"))
6108
+ continue;
6109
+ total += await countMarkdown3(join23(dir, e.name), recursive);
6110
+ }
6111
+ }
6112
+ return total;
6113
+ }
6114
+ async function scanWorklog(dataDir) {
6115
+ const root = join23(dataDir, "worklog");
6116
+ if (!existsSync9(root))
6117
+ return { recent: null, dates: [] };
6118
+ let bestRel = null;
6119
+ const dates = /* @__PURE__ */ new Set();
6120
+ async function walk5(absDir, rel) {
6121
+ let entries;
6122
+ try {
6123
+ entries = await readdir16(absDir, { withFileTypes: true });
6124
+ } catch {
6125
+ return;
6126
+ }
6127
+ for (const e of entries) {
6128
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
6129
+ if (e.isDirectory()) {
6130
+ await walk5(join23(absDir, e.name), childRel);
6131
+ } else if (e.isFile()) {
6132
+ const m2 = e.name.match(/^(\d{4}-\d{2}-\d{2})-.+\.md$/);
6133
+ if (!m2)
6134
+ continue;
6135
+ dates.add(m2[1]);
6136
+ if (bestRel === null || childRel > bestRel)
6137
+ bestRel = childRel;
6138
+ }
6139
+ }
6140
+ }
6141
+ await walk5(root, "");
6142
+ const recent = bestRel === null ? null : { path: `worklog/${bestRel}`, title: await readTitle(join23(root, bestRel)) };
6143
+ return { recent, dates: [...dates] };
6144
+ }
6145
+ async function readTitle(absPath) {
6146
+ try {
6147
+ const raw = await readFile18(absPath, "utf8");
6148
+ const m2 = raw.match(/^#\s+(.+)$/m);
6149
+ if (m2)
6150
+ return m2[1].trim();
6151
+ } catch {
6152
+ }
6153
+ const base = absPath.replace(/\\/g, "/").split("/").pop() ?? absPath;
6154
+ return base.replace(/\.md$/, "");
6155
+ }
6156
+ function addDays(d2, n) {
6157
+ const out = new Date(d2);
6158
+ out.setDate(out.getDate() + n);
6159
+ return out;
6160
+ }
6161
+ function isoDate(d2) {
6162
+ const y2 = d2.getFullYear();
6163
+ const m2 = String(d2.getMonth() + 1).padStart(2, "0");
6164
+ const day = String(d2.getDate()).padStart(2, "0");
6165
+ return `${y2}-${m2}-${day}`;
6166
+ }
3208
6167
 
3209
- - [ ] Try \`/decision <slug> <title>\` for your first decision record
3210
- - [ ] Add a memory by editing \`data/_memory/user_profile.md\` or creating new ones
3211
- - [ ] Run \`/session-start\` tomorrow to see your accumulated state
3212
- `;
6168
+ // ../plugins/session-rituals/dist/worklog-write.js
6169
+ import { mkdir as mkdir6, writeFile as writeFile11 } from "fs/promises";
6170
+ import { dirname as dirname5, join as join24 } from "path";
6171
+ async function ensureWorklogEntry(ctx, opts) {
6172
+ const date = isoDate2(opts?.now ?? /* @__PURE__ */ new Date());
6173
+ const keyword = (opts?.keyword ?? "worklog").trim() || "worklog";
6174
+ const store = new WorklogStore(join24(ctx.dataDir, "worklog"));
6175
+ const existing = await store.get(date);
6176
+ if (existing) {
6177
+ return { path: existing.path, date: existing.date, keyword: existing.keyword, created: false };
6178
+ }
6179
+ const path = store.pathFor(date, keyword);
6180
+ const title = opts?.title ?? `${date} worklog`;
6181
+ await mkdir6(dirname5(path), { recursive: true });
6182
+ await writeFile11(path, renderWorklogFile(date, title, opts?.body ?? ""), "utf8");
6183
+ return { path, date, keyword, created: true };
3213
6184
  }
3214
- function renderFirstHub(role, date) {
6185
+ function renderWorklogFile(date, title, body) {
6186
+ const trimmed = body.trimEnd();
3215
6187
  return `---
3216
- type: hub
3217
- status: active
3218
- privacy: internal
6188
+ type: worklog
3219
6189
  created: ${date}
3220
6190
  updated: ${date}
3221
- tags: [hub, ${slugify(role)}]
6191
+ tags: [worklog]
3222
6192
  ---
3223
6193
 
3224
- # ${role} Hub
3225
-
3226
- > Topic hub for everything related to ${role}. Auto-created by \`/vortex init\`. Edit freely.
3227
-
3228
- ## Active TILs
3229
-
3230
- - (link your ${role}-related TILs here as you accumulate them)
3231
-
3232
- ## Decisions
3233
-
3234
- - (link Decision Log entries here)
6194
+ # ${title}
6195
+ ` + (trimmed ? `
6196
+ ${trimmed}
6197
+ ` : ``);
6198
+ }
6199
+ function isoDate2(d2) {
6200
+ const y2 = d2.getFullYear();
6201
+ const m2 = String(d2.getMonth() + 1).padStart(2, "0");
6202
+ const day = String(d2.getDate()).padStart(2, "0");
6203
+ return `${y2}-${m2}-${day}`;
6204
+ }
3235
6205
 
3236
- ## Memories
6206
+ // ../plugins/session-rituals/dist/cli-dispatch.js
6207
+ var VORTEX_SUBCOMMANDS = ["init", "status", "import", "doctor", "sync"];
6208
+ async function buildRegistry() {
6209
+ try {
6210
+ const { vector } = await import("@vortex-os/memory-extended");
6211
+ return createRitualRegistry({ recall: { embed: vector.createLocalEmbedder() } });
6212
+ } catch {
6213
+ return createRitualRegistry();
6214
+ }
6215
+ }
6216
+ function resolveRepoRoot() {
6217
+ return process.env.VORTEX_REPO_ROOT?.trim() || process.cwd();
6218
+ }
6219
+ function requote(token) {
6220
+ if (!/\s/.test(token))
6221
+ return token;
6222
+ if (token.includes('"') && !token.includes("'"))
6223
+ return `'${token}'`;
6224
+ return `"${token.replace(/"/g, "")}"`;
6225
+ }
6226
+ async function runVortexCli(argv, io) {
6227
+ const out = io?.stdout ?? ((s) => process.stdout.write(s));
6228
+ const err = io?.stderr ?? ((s) => process.stderr.write(s));
6229
+ const repoRoot = resolveRepoRoot();
6230
+ try {
6231
+ if (argv[0] === "session-start") {
6232
+ await runSessionStart(repoRoot, out);
6233
+ return 0;
6234
+ }
6235
+ if (argv[0] === "session-end") {
6236
+ await runSessionEnd(repoRoot, out);
6237
+ return 0;
6238
+ }
6239
+ const registry = await buildRegistry();
6240
+ if (argv.length === 0 || argv[0] === "--list" || argv[0] === "--help") {
6241
+ const names = registry.list().map((c) => ` ${c.name} \u2014 ${c.description}`).join("\n");
6242
+ err(`vortex \u2014 headless ritual runner
3237
6243
 
3238
- - (link \`_memory/*.md\` entries here)
6244
+ Commands:
6245
+ ${names}
6246
+ session-start \u2014 emit the start-of-session boot report (git pull + data counts + catch-up)
6247
+ session-end \u2014 worklog safety net (create today's worklog if work happened and none exists)
3239
6248
 
3240
- ## External references
6249
+ Instance shortcuts (also available as \`/vortex <sub>\`):
6250
+ init \u2014 first-time setup: routers + data/ + hooks + slash-commands
6251
+ status \u2014 instance state report
6252
+ import \u2014 bring an existing notes folder into data/
6253
+ doctor \u2014 health diagnosis
3241
6254
 
3242
- - (links, dashboards, repos, etc.)
3243
- `;
6255
+ Usage: vortex <command> [args...]
6256
+ `);
6257
+ return 0;
6258
+ }
6259
+ const name = argv[0];
6260
+ const isVortexSub = VORTEX_SUBCOMMANDS.includes(name);
6261
+ const body = (isVortexSub ? argv : argv.slice(1)).map(requote).join(" ");
6262
+ const slash = isVortexSub ? `/vortex ${body}` : `/${name} ${body}`;
6263
+ const context = makeContext(repoRoot);
6264
+ const result = await runSlash(slash.trim(), { registry, context });
6265
+ out(JSON.stringify(result, null, 2) + "\n");
6266
+ return 0;
6267
+ } catch (e) {
6268
+ if (e instanceof CommandNotFoundError) {
6269
+ err(`[vortex] unknown command: ${e.message}
6270
+ Run \`vortex --list\` to see available commands.
6271
+ `);
6272
+ } else {
6273
+ err(`[vortex] error: ${e?.message ?? e}
6274
+ `);
6275
+ }
6276
+ return 1;
6277
+ }
3244
6278
  }
3245
- function slugify(s) {
3246
- return s.toLowerCase().replace(/[^a-z0-9가-힣]+/g, "-").replace(/^-+|-+$/g, "");
6279
+ async function runSessionStart(repoRoot, out) {
6280
+ const ctx = makeContext(repoRoot);
6281
+ const config = loadVortexConfig(ctx);
6282
+ if (!config.autoRecord.sessionStart)
6283
+ return;
6284
+ const environment = resolveSessionEnvironment(ctx, config);
6285
+ let git = null;
6286
+ try {
6287
+ const remotes = gitOut(repoRoot, ["remote"]).trim();
6288
+ if (remotes) {
6289
+ try {
6290
+ const pulled = gitOut(repoRoot, ["pull", "--ff-only"]);
6291
+ const lastLine = pulled.trim().split(/\r?\n/).pop() || "up to date";
6292
+ git = { ran: true, summary: lastLine, conflict: false };
6293
+ } catch {
6294
+ git = {
6295
+ ran: true,
6296
+ summary: "fast-forward pull failed (diverged or dirty tree)",
6297
+ conflict: true
6298
+ };
6299
+ }
6300
+ }
6301
+ } catch {
6302
+ }
6303
+ const report = await collectSessionStartReport(ctx, { environment });
6304
+ let missingWorklogDays = [];
6305
+ try {
6306
+ const log = gitOut(repoRoot, ["log", "--since=7 days ago", "--pretty=%cd", "--date=short"]);
6307
+ const commitDays = log.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
6308
+ missingWorklogDays = detectWorklogGaps(commitDays, report.recentWorklogDates);
6309
+ } catch {
6310
+ }
6311
+ let catchUp = null;
6312
+ if (config.autoRecord.archive) {
6313
+ try {
6314
+ const { catchUpSessions: catchUpSessions2 } = await import("./catch-up-ZQN7HMMN.js");
6315
+ catchUp = await catchUpSessions2(ctx);
6316
+ } catch {
6317
+ }
6318
+ }
6319
+ out(renderSessionStartReport(report, {
6320
+ git,
6321
+ missingWorklogDays,
6322
+ catchUp: catchUp ?? void 0
6323
+ }));
3247
6324
  }
3248
- function todayIso3() {
3249
- const d2 = /* @__PURE__ */ new Date();
3250
- const y2 = d2.getFullYear();
3251
- const m2 = String(d2.getMonth() + 1).padStart(2, "0");
3252
- const day = String(d2.getDate()).padStart(2, "0");
3253
- return `${y2}-${m2}-${day}`;
6325
+ async function runSessionEnd(repoRoot, out) {
6326
+ const ctx = makeContext(repoRoot);
6327
+ const config = loadVortexConfig(ctx);
6328
+ if (config.autoRecord.worklog && hadActivityToday(repoRoot)) {
6329
+ const res = await ensureWorklogEntry(ctx, {
6330
+ body: "_Auto-created at session end (work detected but no worklog written). Enrich with the session's work, or remove if there is nothing to log._"
6331
+ });
6332
+ if (res.created)
6333
+ out(`VortEX: created worklog ${res.path}
6334
+ `);
6335
+ }
6336
+ }
6337
+ function gitOut(cwd, gitArgs) {
6338
+ return execFileSync("git", [...gitArgs], {
6339
+ cwd,
6340
+ encoding: "utf8",
6341
+ stdio: ["ignore", "pipe", "ignore"]
6342
+ });
6343
+ }
6344
+ function hadActivityToday(repoRoot) {
6345
+ try {
6346
+ const dirty = gitOut(repoRoot, ["status", "--porcelain"]).trim();
6347
+ if (dirty)
6348
+ return true;
6349
+ const since = /* @__PURE__ */ new Date();
6350
+ since.setHours(0, 0, 0, 0);
6351
+ const commits = gitOut(repoRoot, ["log", "--oneline", `--since=${since.toISOString()}`]).trim();
6352
+ return commits.length > 0;
6353
+ } catch {
6354
+ return false;
6355
+ }
6356
+ }
6357
+ function resolveSessionEnvironment(ctx, config) {
6358
+ let environment = resolveEnvironment(config, {
6359
+ hostname: hostname(),
6360
+ env: process.env,
6361
+ pathExists: existsSync10
6362
+ });
6363
+ if (!environment)
6364
+ environment = process.env.VORTEX_ENV?.trim() || null;
6365
+ if (!environment) {
6366
+ const envFile = join25(ctx.repoRoot, ".agent", "environment");
6367
+ if (existsSync10(envFile)) {
6368
+ environment = readFileSync2(envFile, "utf8").split(/\r?\n/)[0]?.trim() || null;
6369
+ }
6370
+ }
6371
+ return environment;
3254
6372
  }
3255
6373
 
3256
- // ../plugins/session-rituals/dist/registry.js
3257
- function createRitualRegistry() {
3258
- const registry = new CommandRegistry();
3259
- registry.register(sessionStartCommand);
3260
- registry.register(reindexCommand);
3261
- registry.register(decisionCommand);
3262
- registry.register(tilCommand);
3263
- registry.register(vortexCommand);
3264
- return registry;
6374
+ // ../plugins/session-rituals/dist/ambient-recall.js
6375
+ import { join as join26 } from "path";
6376
+ function defaultDbPath2(ctx) {
6377
+ return join26(ctx.dataDir, "_indexes", "memory.sqlite");
6378
+ }
6379
+ function createAmbientRecaller(ctx, options) {
6380
+ const resolveDb = options.dbPath ?? defaultDbPath2;
6381
+ const dbPath = resolveDb(ctx);
6382
+ return new AmbientRecaller({
6383
+ ...options.minScore !== void 0 ? { minScore: options.minScore } : {},
6384
+ ...options.maxSuggestions !== void 0 ? { maxSuggestions: options.maxSuggestions } : {},
6385
+ ...options.minQueryChars !== void 0 ? { minQueryChars: options.minQueryChars } : {},
6386
+ recall: async (query, opts) => {
6387
+ const { sqlite, vector, recall: recallEngine } = await import("@vortex-os/memory-extended");
6388
+ const sqlStore = new sqlite.MemorySqliteStore(dbPath);
6389
+ const vecStore = new vector.MemoryVectorStore({ db: dbPath });
6390
+ const chunkStore = new vector.SessionChunkStore(dbPath);
6391
+ try {
6392
+ const result = await recallEngine.recall({
6393
+ query,
6394
+ ...opts?.k !== void 0 ? { k: opts.k } : {},
6395
+ ...options.source !== void 0 ? { source: options.source } : {}
6396
+ }, { sqlite: sqlStore, vector: vecStore, embed: options.embed, sessionChunks: chunkStore });
6397
+ return { hits: result.hits };
6398
+ } finally {
6399
+ chunkStore.close();
6400
+ vecStore.close();
6401
+ sqlStore.close();
6402
+ }
6403
+ }
6404
+ });
3265
6405
  }
3266
6406
  export {
3267
6407
  dist_exports5 as aiCodingPitfalls,
@@ -3271,11 +6411,12 @@ export {
3271
6411
  dist_exports10 as indexGenerator,
3272
6412
  dist_exports12 as linkRewriter,
3273
6413
  dist_exports3 as memorySystem,
6414
+ dist_exports13 as proactiveCurator,
3274
6415
  dist_exports7 as reportGenerator,
3275
6416
  dist_exports11 as runbooks,
3276
- dist_exports13 as sessionRituals,
6417
+ dist_exports14 as sessionRituals,
3277
6418
  dist_exports2 as slashCommands,
3278
- dist_exports8 as til,
3279
- dist_exports6 as toolRules
6419
+ dist_exports6 as toolRules,
6420
+ dist_exports8 as worklog
3280
6421
  };
3281
6422
  //# sourceMappingURL=index.js.map