@locusai/cli 0.25.6 → 0.26.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.
Files changed (2) hide show
  1. package/bin/locus.js +1128 -362
  2. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -1880,6 +1880,210 @@ var init_github = __esm(() => {
1880
1880
  init_rate_limiter();
1881
1881
  });
1882
1882
 
1883
+ // src/core/memory.ts
1884
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
1885
+ import {
1886
+ appendFile,
1887
+ mkdir,
1888
+ readFile,
1889
+ stat,
1890
+ unlink,
1891
+ writeFile
1892
+ } from "node:fs/promises";
1893
+ import { join as join6 } from "node:path";
1894
+ function getMemoryDir(projectRoot) {
1895
+ return join6(projectRoot, ".locus", MEMORY_DIR);
1896
+ }
1897
+ async function ensureMemoryDir(projectRoot) {
1898
+ const dir = getMemoryDir(projectRoot);
1899
+ await mkdir(dir, { recursive: true });
1900
+ for (const category of Object.values(MEMORY_CATEGORIES)) {
1901
+ const filePath = join6(dir, category.file);
1902
+ if (!existsSync6(filePath)) {
1903
+ const header = `# ${category.title}
1904
+
1905
+ ${category.description}
1906
+
1907
+ `;
1908
+ await writeFile(filePath, header, "utf-8");
1909
+ }
1910
+ }
1911
+ }
1912
+ async function readMemoryFile(projectRoot, category) {
1913
+ const meta = MEMORY_CATEGORIES[category];
1914
+ if (!meta)
1915
+ return "";
1916
+ const filePath = join6(getMemoryDir(projectRoot), meta.file);
1917
+ try {
1918
+ return await readFile(filePath, "utf-8");
1919
+ } catch {
1920
+ return "";
1921
+ }
1922
+ }
1923
+ async function readAllMemory(projectRoot) {
1924
+ const parts = [];
1925
+ for (const category of Object.keys(MEMORY_CATEGORIES)) {
1926
+ const content = await readMemoryFile(projectRoot, category);
1927
+ if (content.trim()) {
1928
+ parts.push(content.trim());
1929
+ }
1930
+ }
1931
+ return parts.join(`
1932
+
1933
+ `);
1934
+ }
1935
+ function readAllMemorySync(projectRoot) {
1936
+ const dir = getMemoryDir(projectRoot);
1937
+ if (!existsSync6(dir))
1938
+ return "";
1939
+ const parts = [];
1940
+ for (const meta of Object.values(MEMORY_CATEGORIES)) {
1941
+ const filePath = join6(dir, meta.file);
1942
+ try {
1943
+ const content = readFileSync4(filePath, "utf-8").trim();
1944
+ if (content)
1945
+ parts.push(content);
1946
+ } catch {}
1947
+ }
1948
+ return parts.join(`
1949
+
1950
+ `);
1951
+ }
1952
+ async function appendMemoryEntries(projectRoot, entries) {
1953
+ const dir = getMemoryDir(projectRoot);
1954
+ for (const entry of entries) {
1955
+ const meta = MEMORY_CATEGORIES[entry.category];
1956
+ if (!meta)
1957
+ continue;
1958
+ const filePath = join6(dir, meta.file);
1959
+ const line = `- **[${meta.title}]**: ${entry.text}
1960
+ `;
1961
+ await appendFile(filePath, line, "utf-8");
1962
+ }
1963
+ }
1964
+ function resolveLearningsCategory(tag) {
1965
+ return LEARNINGS_CATEGORY_MAP[tag.toLowerCase()] ?? "conventions";
1966
+ }
1967
+ async function migrateFromLearnings(projectRoot) {
1968
+ const learningsPath = join6(projectRoot, ".locus", "LEARNINGS.md");
1969
+ let content;
1970
+ try {
1971
+ content = await readFile(learningsPath, "utf-8");
1972
+ } catch {
1973
+ return { migrated: 0, skipped: 0 };
1974
+ }
1975
+ if (!content.trim()) {
1976
+ return { migrated: 0, skipped: 0 };
1977
+ }
1978
+ const entryPattern = /^- \*\*\[([^\]]+)\]\*\*:\s*/;
1979
+ const lines = content.split(`
1980
+ `);
1981
+ const parsed = [];
1982
+ for (let i = 0;i < lines.length; i++) {
1983
+ const match = lines[i].match(entryPattern);
1984
+ if (!match)
1985
+ continue;
1986
+ const tag = match[1];
1987
+ const category = resolveLearningsCategory(tag);
1988
+ let text = lines[i].slice(match[0].length);
1989
+ for (let j = i + 1;j < lines.length; j++) {
1990
+ if (lines[j].match(entryPattern) || lines[j].trim() === "")
1991
+ break;
1992
+ text += `
1993
+ ${lines[j]}`;
1994
+ i = j;
1995
+ }
1996
+ parsed.push({ category, text: text.trim() });
1997
+ }
1998
+ if (parsed.length === 0) {
1999
+ return { migrated: 0, skipped: 0 };
2000
+ }
2001
+ await ensureMemoryDir(projectRoot);
2002
+ const existingContent = {};
2003
+ for (const key of Object.keys(MEMORY_CATEGORIES)) {
2004
+ existingContent[key] = await readMemoryFile(projectRoot, key);
2005
+ }
2006
+ let migrated = 0;
2007
+ let skipped = 0;
2008
+ for (const entry of parsed) {
2009
+ const existing = existingContent[entry.category] ?? "";
2010
+ if (existing.includes(entry.text)) {
2011
+ skipped++;
2012
+ continue;
2013
+ }
2014
+ await appendMemoryEntries(projectRoot, [entry]);
2015
+ existingContent[entry.category] = (existingContent[entry.category] ?? "") + `- **[${MEMORY_CATEGORIES[entry.category]?.title}]**: ${entry.text}
2016
+ `;
2017
+ migrated++;
2018
+ }
2019
+ if (migrated > 0 || skipped > 0) {
2020
+ try {
2021
+ await unlink(learningsPath);
2022
+ } catch {}
2023
+ }
2024
+ return { migrated, skipped };
2025
+ }
2026
+ async function getMemoryStats(projectRoot) {
2027
+ const dir = getMemoryDir(projectRoot);
2028
+ const result = {};
2029
+ for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
2030
+ const filePath = join6(dir, meta.file);
2031
+ try {
2032
+ const [content, fileStat] = await Promise.all([
2033
+ readFile(filePath, "utf-8"),
2034
+ stat(filePath)
2035
+ ]);
2036
+ const count = content.split(`
2037
+ `).filter((line) => line.startsWith("- ")).length;
2038
+ result[key] = {
2039
+ count,
2040
+ size: fileStat.size,
2041
+ lastModified: fileStat.mtime
2042
+ };
2043
+ } catch {
2044
+ result[key] = { count: 0, size: 0, lastModified: new Date(0) };
2045
+ }
2046
+ }
2047
+ return result;
2048
+ }
2049
+ var MEMORY_DIR = "memory", MEMORY_CATEGORIES, LEARNINGS_CATEGORY_MAP;
2050
+ var init_memory = __esm(() => {
2051
+ MEMORY_CATEGORIES = {
2052
+ architecture: {
2053
+ file: "architecture.md",
2054
+ title: "Architecture",
2055
+ description: "Package ownership, module boundaries, data flow"
2056
+ },
2057
+ conventions: {
2058
+ file: "conventions.md",
2059
+ title: "Conventions",
2060
+ description: "Code style, naming, patterns"
2061
+ },
2062
+ decisions: {
2063
+ file: "decisions.md",
2064
+ title: "Decisions",
2065
+ description: "Trade-off rationale: why X over Y"
2066
+ },
2067
+ preferences: {
2068
+ file: "preferences.md",
2069
+ title: "Preferences",
2070
+ description: "User corrections, rejected approaches"
2071
+ },
2072
+ debugging: {
2073
+ file: "debugging.md",
2074
+ title: "Debugging",
2075
+ description: "Non-obvious gotchas, environment quirks"
2076
+ }
2077
+ };
2078
+ LEARNINGS_CATEGORY_MAP = {
2079
+ architecture: "architecture",
2080
+ conventions: "conventions",
2081
+ packages: "architecture",
2082
+ "user preferences": "preferences",
2083
+ debugging: "debugging"
2084
+ };
2085
+ });
2086
+
1883
2087
  // src/types.ts
1884
2088
  var PRIORITY_LABELS, TYPE_LABELS, STATUS_LABELS, AGENT_LABEL, ALL_LABELS;
1885
2089
  var init_types = __esm(() => {
@@ -1937,8 +2141,8 @@ var exports_init = {};
1937
2141
  __export(exports_init, {
1938
2142
  initCommand: () => initCommand
1939
2143
  });
1940
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1941
- import { join as join6 } from "node:path";
2144
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
2145
+ import { join as join7 } from "node:path";
1942
2146
  async function initCommand(cwd) {
1943
2147
  const log = getLogger();
1944
2148
  process.stderr.write(`
@@ -1982,23 +2186,47 @@ ${bold2("Initializing Locus...")}
1982
2186
  }
1983
2187
  process.stderr.write(`${green("✓")} Repository: ${bold2(`${context.owner}/${context.repo}`)} (branch: ${context.defaultBranch})
1984
2188
  `);
1985
- const locusDir = join6(cwd, ".locus");
2189
+ const locusDir = join7(cwd, ".locus");
1986
2190
  const dirs = [
1987
2191
  locusDir,
1988
- join6(locusDir, "sessions"),
1989
- join6(locusDir, "discussions"),
1990
- join6(locusDir, "artifacts"),
1991
- join6(locusDir, "plans"),
1992
- join6(locusDir, "logs"),
1993
- join6(locusDir, "run-state")
2192
+ join7(locusDir, "sessions"),
2193
+ join7(locusDir, "discussions"),
2194
+ join7(locusDir, "artifacts"),
2195
+ join7(locusDir, "plans"),
2196
+ join7(locusDir, "logs"),
2197
+ join7(locusDir, "run-state")
1994
2198
  ];
1995
2199
  for (const dir of dirs) {
1996
- if (!existsSync6(dir)) {
2200
+ if (!existsSync7(dir)) {
1997
2201
  mkdirSync5(dir, { recursive: true });
1998
2202
  }
1999
2203
  }
2000
2204
  process.stderr.write(`${green("✓")} Created .locus/ directory structure
2001
2205
  `);
2206
+ const memoryDirExists = existsSync7(getMemoryDir(cwd));
2207
+ await ensureMemoryDir(cwd);
2208
+ if (!memoryDirExists) {
2209
+ process.stderr.write(`${green("✓")} Created .locus/memory/ with category files
2210
+ `);
2211
+ } else {
2212
+ process.stderr.write(`${dim2("○")} .locus/memory/ already exists (preserved)
2213
+ `);
2214
+ }
2215
+ if (existsSync7(join7(locusDir, "LEARNINGS.md"))) {
2216
+ const result = await migrateFromLearnings(cwd);
2217
+ if (result.migrated > 0) {
2218
+ process.stderr.write(`${green("✓")} Migrated ${result.migrated} entries from LEARNINGS.md to .locus/memory/
2219
+ `);
2220
+ }
2221
+ if (result.skipped > 0) {
2222
+ process.stderr.write(`${dim2("○")} Skipped ${result.skipped} duplicate entries during migration
2223
+ `);
2224
+ }
2225
+ if (result.migrated > 0 || result.skipped > 0) {
2226
+ process.stderr.write(`${green("✓")} Removed legacy LEARNINGS.md
2227
+ `);
2228
+ }
2229
+ }
2002
2230
  const isReInit = isInitialized(cwd);
2003
2231
  const config = {
2004
2232
  ...DEFAULT_CONFIG2,
@@ -2014,7 +2242,7 @@ ${bold2("Initializing Locus...")}
2014
2242
  };
2015
2243
  if (isReInit) {
2016
2244
  try {
2017
- const existing = JSON.parse(readFileSync4(join6(locusDir, "config.json"), "utf-8"));
2245
+ const existing = JSON.parse(readFileSync5(join7(locusDir, "config.json"), "utf-8"));
2018
2246
  if (existing.ai)
2019
2247
  config.ai = { ...config.ai, ...existing.ai };
2020
2248
  if (existing.agent)
@@ -2035,8 +2263,8 @@ ${bold2("Initializing Locus...")}
2035
2263
  `);
2036
2264
  }
2037
2265
  saveConfig(cwd, config);
2038
- const locusMdPath = join6(locusDir, "LOCUS.md");
2039
- if (!existsSync6(locusMdPath)) {
2266
+ const locusMdPath = join7(locusDir, "LOCUS.md");
2267
+ if (!existsSync7(locusMdPath)) {
2040
2268
  writeFileSync4(locusMdPath, LOCUS_MD_TEMPLATE, "utf-8");
2041
2269
  process.stderr.write(`${green("✓")} Generated LOCUS.md (edit to add project context)
2042
2270
  `);
@@ -2044,17 +2272,8 @@ ${bold2("Initializing Locus...")}
2044
2272
  process.stderr.write(`${dim2("○")} LOCUS.md already exists (preserved)
2045
2273
  `);
2046
2274
  }
2047
- const learningsMdPath = join6(locusDir, "LEARNINGS.md");
2048
- if (!existsSync6(learningsMdPath)) {
2049
- writeFileSync4(learningsMdPath, LEARNINGS_MD_TEMPLATE, "utf-8");
2050
- process.stderr.write(`${green("✓")} Generated LEARNINGS.md
2051
- `);
2052
- } else {
2053
- process.stderr.write(`${dim2("○")} LEARNINGS.md already exists (preserved)
2054
- `);
2055
- }
2056
- const sandboxIgnorePath = join6(cwd, ".sandboxignore");
2057
- if (!existsSync6(sandboxIgnorePath)) {
2275
+ const sandboxIgnorePath = join7(cwd, ".sandboxignore");
2276
+ if (!existsSync7(sandboxIgnorePath)) {
2058
2277
  writeFileSync4(sandboxIgnorePath, SANDBOXIGNORE_TEMPLATE, "utf-8");
2059
2278
  process.stderr.write(`${green("✓")} Generated .sandboxignore
2060
2279
  `);
@@ -2063,8 +2282,8 @@ ${bold2("Initializing Locus...")}
2063
2282
  `);
2064
2283
  }
2065
2284
  const ecosystem = detectProjectEcosystem(cwd);
2066
- const sandboxSetupPath = join6(locusDir, "sandbox-setup.sh");
2067
- if (!existsSync6(sandboxSetupPath)) {
2285
+ const sandboxSetupPath = join7(locusDir, "sandbox-setup.sh");
2286
+ if (!existsSync7(sandboxSetupPath)) {
2068
2287
  const template = generateSandboxSetupTemplate(ecosystem);
2069
2288
  if (template) {
2070
2289
  writeFileSync4(sandboxSetupPath, template, {
@@ -2087,10 +2306,10 @@ ${bold2("Initializing Locus...")}
2087
2306
  process.stderr.write(`\r${yellow2("⚠")} Some labels could not be created: ${e.message}
2088
2307
  `);
2089
2308
  }
2090
- const gitignorePath = join6(cwd, ".gitignore");
2309
+ const gitignorePath = join7(cwd, ".gitignore");
2091
2310
  let gitignoreContent = "";
2092
- if (existsSync6(gitignorePath)) {
2093
- gitignoreContent = readFileSync4(gitignorePath, "utf-8");
2311
+ if (existsSync7(gitignorePath)) {
2312
+ gitignoreContent = readFileSync5(gitignorePath, "utf-8");
2094
2313
  }
2095
2314
  const entriesToAdd = GITIGNORE_ENTRIES.filter((entry) => entry && !gitignoreContent.includes(entry.trim()));
2096
2315
  if (entriesToAdd.length > 0) {
@@ -2222,12 +2441,19 @@ When a task produces knowledge, analysis, or research output rather than (or in
2222
2441
 
2223
2442
  ## Continuous Learning (MANDATORY)
2224
2443
 
2225
- **CRITICAL: Updating \`.locus/LEARNINGS.md\` is a required step, not optional.** You MUST read it before starting work AND update it before finishing if you learned anything worth recording. Failing to update learnings when a reusable lesson was discovered is a defect — treat it with the same severity as forgetting to run tests.
2444
+ **CRITICAL: Updating \`.locus/memory/\` is a required step, not optional.** You MUST read memory files before starting work AND update them before finishing if you learned anything worth recording. Failing to update memory when a reusable lesson was discovered is a defect — treat it with the same severity as forgetting to run tests.
2445
+
2446
+ **Memory is organized into 5 category files in \`.locus/memory/\`:**
2447
+ - \`architecture.md\` — Package ownership, module boundaries, data flow
2448
+ - \`conventions.md\` — Code style, naming, patterns
2449
+ - \`decisions.md\` — Trade-off rationale: why X over Y
2450
+ - \`preferences.md\` — User corrections, rejected approaches
2451
+ - \`debugging.md\` — Non-obvious gotchas, environment quirks
2226
2452
 
2227
2453
  **Workflow:**
2228
- 1. **Read** \`.locus/LEARNINGS.md\` at the start of every task
2454
+ 1. **Read** files in \`.locus/memory/\` at the start of every task
2229
2455
  2. **During execution**, note any reusable lessons (architectural discoveries, user corrections, non-obvious constraints)
2230
- 3. **Before finishing**, append new entries to \`.locus/LEARNINGS.md\` if any were discovered. Do this as one of your final steps, alongside running tests and linters
2456
+ 3. **Before finishing**, append new entries to the appropriate category file in \`.locus/memory/\`. Do this as one of your final steps, alongside running tests and linters
2231
2457
 
2232
2458
  **The quality bar:** Ask yourself — "Would a new agent working on a completely different task benefit from knowing this?" If yes, record it. If it only matters for the current task or file, skip it.
2233
2459
 
@@ -2253,19 +2479,19 @@ When a task produces knowledge, analysis, or research output rather than (or in
2253
2479
  **Good examples:**
2254
2480
  - \`[Architecture]\`: Shared types for all packages live in \`@locusai/shared\` — never redefine them locally in CLI or API packages.
2255
2481
  - \`[User Preferences]\`: User prefers not to track low-level interrupt/signal handling patterns in learnings — focus on architectural and decision-level entries.
2256
- - \`[Packages]\`: Validation uses Zod throughout — do not introduce a second validation library.
2482
+ - \`[Conventions]\`: Validation uses Zod throughout — do not introduce a second validation library.
2257
2483
 
2258
2484
  **Bad examples (do not write these):**
2259
- - \`[Patterns]\`: \`run.ts\` must call \`registerShutdownHandlers()\` at startup. ← too local, obvious from the file.
2485
+ - \`[Conventions]\`: \`run.ts\` must call \`registerShutdownHandlers()\` at startup. ← too local, obvious from the file.
2260
2486
  - \`[Debugging]\`: Fixed a regex bug in \`image-detect.ts\`. ← one-time fix, irrelevant to future tasks.
2261
2487
 
2262
- **Format (append-only, never delete):**
2488
+ **Format (append to the appropriate category file):**
2263
2489
 
2264
2490
  \`\`\`
2265
2491
  - **[Category]**: Concise description (1-2 lines max). *Rationale if non-obvious.*
2266
2492
  \`\`\`
2267
2493
 
2268
- **Categories:** Architecture, Packages, User Preferences, Conventions, Debugging
2494
+ **Categories:** Architecture, Conventions, Decisions, Preferences, Debugging
2269
2495
 
2270
2496
  ## Error Handling
2271
2497
 
@@ -2317,12 +2543,6 @@ service-account*.json
2317
2543
 
2318
2544
  # Docker secrets
2319
2545
  docker-compose.override.yml
2320
- `, LEARNINGS_MD_TEMPLATE = `# Learnings
2321
-
2322
- This file captures important lessons, decisions, and corrections made during development.
2323
- It is read by AI agents before every task to avoid repeating mistakes and to follow established patterns.
2324
-
2325
- <!-- Add learnings below this line. Format: - **[Category]**: Description -->
2326
2546
  `, GITIGNORE_ENTRIES;
2327
2547
  var init_init = __esm(() => {
2328
2548
  init_config();
@@ -2330,9 +2550,10 @@ var init_init = __esm(() => {
2330
2550
  init_ecosystem();
2331
2551
  init_github();
2332
2552
  init_logger();
2553
+ init_memory();
2333
2554
  init_terminal();
2334
2555
  init_types();
2335
- GITIGNORE_ENTRIES = ["", "# Locus", ".locus/", "!.locus/LEARNINGS.md"];
2556
+ GITIGNORE_ENTRIES = ["", "# Locus", ".locus/", "!.locus/memory/"];
2336
2557
  });
2337
2558
 
2338
2559
  // src/commands/create.ts
@@ -2341,8 +2562,8 @@ __export(exports_create, {
2341
2562
  createCommand: () => createCommand
2342
2563
  });
2343
2564
  import { execSync as execSync5 } from "node:child_process";
2344
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2345
- import { join as join7 } from "node:path";
2565
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2566
+ import { join as join8 } from "node:path";
2346
2567
  function validateName(name) {
2347
2568
  if (!name)
2348
2569
  return "Package name is required.";
@@ -2614,8 +2835,8 @@ ${bold2("Creating package:")} ${cyan2(fullNpmName)}
2614
2835
  }
2615
2836
  process.stderr.write(`${green("✓")} Name is valid: ${bold2(name)}
2616
2837
  `);
2617
- const packagesDir = join7(process.cwd(), "packages", name);
2618
- if (existsSync7(packagesDir)) {
2838
+ const packagesDir = join8(process.cwd(), "packages", name);
2839
+ if (existsSync8(packagesDir)) {
2619
2840
  process.stderr.write(`${red2("✗")} Directory already exists: ${bold2(`packages/${name}/`)}
2620
2841
  `);
2621
2842
  process.exit(1);
@@ -2632,37 +2853,37 @@ ${bold2("Creating package:")} ${cyan2(fullNpmName)}
2632
2853
  let sdkVersion = "0.22.0";
2633
2854
  let gatewayVersion = "0.22.0";
2634
2855
  try {
2635
- const { readFileSync: readFileSync5 } = await import("node:fs");
2636
- const sdkPkgPath = join7(process.cwd(), "packages", "sdk", "package.json");
2637
- if (existsSync7(sdkPkgPath)) {
2638
- const sdkPkg = JSON.parse(readFileSync5(sdkPkgPath, "utf-8"));
2856
+ const { readFileSync: readFileSync6 } = await import("node:fs");
2857
+ const sdkPkgPath = join8(process.cwd(), "packages", "sdk", "package.json");
2858
+ if (existsSync8(sdkPkgPath)) {
2859
+ const sdkPkg = JSON.parse(readFileSync6(sdkPkgPath, "utf-8"));
2639
2860
  if (sdkPkg.version)
2640
2861
  sdkVersion = sdkPkg.version;
2641
2862
  }
2642
- const gatewayPkgPath = join7(process.cwd(), "packages", "gateway", "package.json");
2643
- if (existsSync7(gatewayPkgPath)) {
2644
- const gatewayPkg = JSON.parse(readFileSync5(gatewayPkgPath, "utf-8"));
2863
+ const gatewayPkgPath = join8(process.cwd(), "packages", "gateway", "package.json");
2864
+ if (existsSync8(gatewayPkgPath)) {
2865
+ const gatewayPkg = JSON.parse(readFileSync6(gatewayPkgPath, "utf-8"));
2645
2866
  if (gatewayPkg.version)
2646
2867
  gatewayVersion = gatewayPkg.version;
2647
2868
  }
2648
2869
  } catch {}
2649
- mkdirSync6(join7(packagesDir, "src"), { recursive: true });
2650
- mkdirSync6(join7(packagesDir, "bin"), { recursive: true });
2870
+ mkdirSync6(join8(packagesDir, "src"), { recursive: true });
2871
+ mkdirSync6(join8(packagesDir, "bin"), { recursive: true });
2651
2872
  process.stderr.write(`${green("✓")} Created directory structure
2652
2873
  `);
2653
- writeFileSync5(join7(packagesDir, "package.json"), generatePackageJson(name, displayName, description, sdkVersion, gatewayVersion), "utf-8");
2874
+ writeFileSync5(join8(packagesDir, "package.json"), generatePackageJson(name, displayName, description, sdkVersion, gatewayVersion), "utf-8");
2654
2875
  process.stderr.write(`${green("✓")} Generated package.json
2655
2876
  `);
2656
- writeFileSync5(join7(packagesDir, "tsconfig.json"), generateTsconfig(), "utf-8");
2877
+ writeFileSync5(join8(packagesDir, "tsconfig.json"), generateTsconfig(), "utf-8");
2657
2878
  process.stderr.write(`${green("✓")} Generated tsconfig.json
2658
2879
  `);
2659
- writeFileSync5(join7(packagesDir, "src", "cli.ts"), generateCliTs(), "utf-8");
2880
+ writeFileSync5(join8(packagesDir, "src", "cli.ts"), generateCliTs(), "utf-8");
2660
2881
  process.stderr.write(`${green("✓")} Generated src/cli.ts
2661
2882
  `);
2662
- writeFileSync5(join7(packagesDir, "src", "index.ts"), generateIndexTs(name), "utf-8");
2883
+ writeFileSync5(join8(packagesDir, "src", "index.ts"), generateIndexTs(name), "utf-8");
2663
2884
  process.stderr.write(`${green("✓")} Generated src/index.ts
2664
2885
  `);
2665
- writeFileSync5(join7(packagesDir, "README.md"), generateReadme(name, description), "utf-8");
2886
+ writeFileSync5(join8(packagesDir, "README.md"), generateReadme(name, description), "utf-8");
2666
2887
  process.stderr.write(`${green("✓")} Generated README.md
2667
2888
  `);
2668
2889
  process.stderr.write(`
@@ -2694,37 +2915,37 @@ var init_create = __esm(() => {
2694
2915
 
2695
2916
  // src/packages/registry.ts
2696
2917
  import {
2697
- existsSync as existsSync8,
2918
+ existsSync as existsSync9,
2698
2919
  mkdirSync as mkdirSync7,
2699
- readFileSync as readFileSync5,
2920
+ readFileSync as readFileSync6,
2700
2921
  renameSync,
2701
2922
  writeFileSync as writeFileSync6
2702
2923
  } from "node:fs";
2703
2924
  import { homedir as homedir2 } from "node:os";
2704
- import { join as join8 } from "node:path";
2925
+ import { join as join9 } from "node:path";
2705
2926
  function getPackagesDir() {
2706
2927
  const home = process.env.HOME || homedir2();
2707
- const dir = join8(home, ".locus", "packages");
2708
- if (!existsSync8(dir)) {
2928
+ const dir = join9(home, ".locus", "packages");
2929
+ if (!existsSync9(dir)) {
2709
2930
  mkdirSync7(dir, { recursive: true });
2710
2931
  }
2711
- const pkgJson = join8(dir, "package.json");
2712
- if (!existsSync8(pkgJson)) {
2932
+ const pkgJson = join9(dir, "package.json");
2933
+ if (!existsSync9(pkgJson)) {
2713
2934
  writeFileSync6(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
2714
2935
  `, "utf-8");
2715
2936
  }
2716
2937
  return dir;
2717
2938
  }
2718
2939
  function getRegistryPath() {
2719
- return join8(getPackagesDir(), "registry.json");
2940
+ return join9(getPackagesDir(), "registry.json");
2720
2941
  }
2721
2942
  function loadRegistry() {
2722
2943
  const registryPath = getRegistryPath();
2723
- if (!existsSync8(registryPath)) {
2944
+ if (!existsSync9(registryPath)) {
2724
2945
  return { packages: {} };
2725
2946
  }
2726
2947
  try {
2727
- const raw = readFileSync5(registryPath, "utf-8");
2948
+ const raw = readFileSync6(registryPath, "utf-8");
2728
2949
  const parsed = JSON.parse(raw);
2729
2950
  if (typeof parsed === "object" && parsed !== null && "packages" in parsed && typeof parsed.packages === "object") {
2730
2951
  const registry = parsed;
@@ -2758,8 +2979,8 @@ function saveRegistry(registry) {
2758
2979
  function resolvePackageBinary(packageName) {
2759
2980
  const fullName = normalizePackageName(packageName);
2760
2981
  const binName = fullName.includes("/") ? fullName.split("/").pop() : fullName;
2761
- const binPath = join8(getPackagesDir(), "node_modules", ".bin", binName);
2762
- return existsSync8(binPath) ? binPath : null;
2982
+ const binPath = join9(getPackagesDir(), "node_modules", ".bin", binName);
2983
+ return existsSync9(binPath) ? binPath : null;
2763
2984
  }
2764
2985
  function normalizePackageName(input) {
2765
2986
  if (input.startsWith(SCOPED_PREFIX)) {
@@ -2785,7 +3006,7 @@ __export(exports_pkg, {
2785
3006
  listInstalledPackages: () => listInstalledPackages
2786
3007
  });
2787
3008
  import { spawn } from "node:child_process";
2788
- import { existsSync as existsSync9 } from "node:fs";
3009
+ import { existsSync as existsSync10 } from "node:fs";
2789
3010
  function listInstalledPackages() {
2790
3011
  const registry = loadRegistry();
2791
3012
  const entries = Object.values(registry.packages);
@@ -2864,7 +3085,7 @@ async function pkgCommand(args, _flags) {
2864
3085
  return;
2865
3086
  }
2866
3087
  let binaryPath = entry.binaryPath;
2867
- if (!binaryPath || !existsSync9(binaryPath)) {
3088
+ if (!binaryPath || !existsSync10(binaryPath)) {
2868
3089
  const resolved = resolvePackageBinary(packageName);
2869
3090
  if (resolved) {
2870
3091
  binaryPath = resolved;
@@ -2997,8 +3218,8 @@ __export(exports_install, {
2997
3218
  installCommand: () => installCommand
2998
3219
  });
2999
3220
  import { spawnSync as spawnSync2 } from "node:child_process";
3000
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "node:fs";
3001
- import { join as join9 } from "node:path";
3221
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
3222
+ import { join as join10 } from "node:path";
3002
3223
  function parsePackageArg(raw) {
3003
3224
  if (raw.startsWith("@")) {
3004
3225
  const slashIdx = raw.indexOf("/");
@@ -3075,8 +3296,8 @@ ${red2("✗")} Failed to install ${bold2(packageSpec)}.
3075
3296
  process.exit(1);
3076
3297
  return;
3077
3298
  }
3078
- const installedPkgJsonPath = join9(packagesDir, "node_modules", packageName, "package.json");
3079
- if (!existsSync10(installedPkgJsonPath)) {
3299
+ const installedPkgJsonPath = join10(packagesDir, "node_modules", packageName, "package.json");
3300
+ if (!existsSync11(installedPkgJsonPath)) {
3080
3301
  process.stderr.write(`
3081
3302
  ${red2("✗")} Package installed but package.json not found at:
3082
3303
  `);
@@ -3087,7 +3308,7 @@ ${red2("✗")} Package installed but package.json not found at:
3087
3308
  }
3088
3309
  let installedPkgJson;
3089
3310
  try {
3090
- installedPkgJson = JSON.parse(readFileSync6(installedPkgJsonPath, "utf-8"));
3311
+ installedPkgJson = JSON.parse(readFileSync7(installedPkgJsonPath, "utf-8"));
3091
3312
  } catch {
3092
3313
  process.stderr.write(`
3093
3314
  ${red2("✗")} Could not parse installed package.json.
@@ -3329,19 +3550,19 @@ var REGISTRY_REPO = "asgarovf/locusai", REGISTRY_BRANCH = "master", SKILLS_LOCK_
3329
3550
 
3330
3551
  // src/skills/lock.ts
3331
3552
  import { createHash } from "node:crypto";
3332
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
3333
- import { join as join10 } from "node:path";
3553
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
3554
+ import { join as join11 } from "node:path";
3334
3555
  function readLockFile(projectRoot) {
3335
- const filePath = join10(projectRoot, SKILLS_LOCK_FILENAME);
3556
+ const filePath = join11(projectRoot, SKILLS_LOCK_FILENAME);
3336
3557
  try {
3337
- const raw = readFileSync7(filePath, "utf-8");
3558
+ const raw = readFileSync8(filePath, "utf-8");
3338
3559
  return JSON.parse(raw);
3339
3560
  } catch {
3340
3561
  return { ...DEFAULT_LOCK_FILE, skills: {} };
3341
3562
  }
3342
3563
  }
3343
3564
  function writeLockFile(projectRoot, lockFile) {
3344
- const filePath = join10(projectRoot, SKILLS_LOCK_FILENAME);
3565
+ const filePath = join11(projectRoot, SKILLS_LOCK_FILENAME);
3345
3566
  writeFileSync7(filePath, `${JSON.stringify(lockFile, null, 2)}
3346
3567
  `, "utf-8");
3347
3568
  }
@@ -3355,29 +3576,29 @@ var init_lock = __esm(() => {
3355
3576
 
3356
3577
  // src/skills/installer.ts
3357
3578
  import {
3358
- existsSync as existsSync11,
3579
+ existsSync as existsSync12,
3359
3580
  mkdirSync as mkdirSync8,
3360
- readFileSync as readFileSync8,
3581
+ readFileSync as readFileSync9,
3361
3582
  renameSync as renameSync2,
3362
3583
  rmSync,
3363
3584
  writeFileSync as writeFileSync8
3364
3585
  } from "node:fs";
3365
3586
  import { tmpdir } from "node:os";
3366
- import { join as join11 } from "node:path";
3587
+ import { join as join12 } from "node:path";
3367
3588
  async function installSkill(projectRoot, name, content, source) {
3368
- const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
3369
- const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
3370
- const stagingDir = join11(tmpdir(), `locus-skill-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
3371
- const stagingClaudeDir = join11(stagingDir, "claude");
3372
- const stagingAgentsDir = join11(stagingDir, "agents");
3589
+ const claudeDir = join12(projectRoot, CLAUDE_SKILLS_DIR, name);
3590
+ const agentsDir = join12(projectRoot, AGENTS_SKILLS_DIR, name);
3591
+ const stagingDir = join12(tmpdir(), `locus-skill-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
3592
+ const stagingClaudeDir = join12(stagingDir, "claude");
3593
+ const stagingAgentsDir = join12(stagingDir, "agents");
3373
3594
  let wroteClaudeDir = false;
3374
3595
  let wroteAgentsDir = false;
3375
3596
  try {
3376
3597
  try {
3377
3598
  mkdirSync8(stagingClaudeDir, { recursive: true });
3378
3599
  mkdirSync8(stagingAgentsDir, { recursive: true });
3379
- writeFileSync8(join11(stagingClaudeDir, "SKILL.md"), content, "utf-8");
3380
- writeFileSync8(join11(stagingAgentsDir, "SKILL.md"), content, "utf-8");
3600
+ writeFileSync8(join12(stagingClaudeDir, "SKILL.md"), content, "utf-8");
3601
+ writeFileSync8(join12(stagingAgentsDir, "SKILL.md"), content, "utf-8");
3381
3602
  } catch (err) {
3382
3603
  throw new SkillInstallError(name, "stage", err);
3383
3604
  }
@@ -3386,7 +3607,7 @@ async function installSkill(projectRoot, name, content, source) {
3386
3607
  throw new Error("Skill content is empty");
3387
3608
  }
3388
3609
  const expectedHash = computeSkillHash(content);
3389
- const stagedContent = readFileSync8(join11(stagingClaudeDir, "SKILL.md"), "utf-8");
3610
+ const stagedContent = readFileSync9(join12(stagingClaudeDir, "SKILL.md"), "utf-8");
3390
3611
  const stagedHash = computeSkillHash(stagedContent);
3391
3612
  if (stagedHash !== expectedHash) {
3392
3613
  throw new Error("Staged file hash does not match expected content");
@@ -3397,12 +3618,12 @@ async function installSkill(projectRoot, name, content, source) {
3397
3618
  throw new SkillInstallError(name, "validate", err);
3398
3619
  }
3399
3620
  try {
3400
- mkdirSync8(join11(projectRoot, CLAUDE_SKILLS_DIR), { recursive: true });
3401
- mkdirSync8(join11(projectRoot, AGENTS_SKILLS_DIR), { recursive: true });
3402
- if (existsSync11(claudeDir)) {
3621
+ mkdirSync8(join12(projectRoot, CLAUDE_SKILLS_DIR), { recursive: true });
3622
+ mkdirSync8(join12(projectRoot, AGENTS_SKILLS_DIR), { recursive: true });
3623
+ if (existsSync12(claudeDir)) {
3403
3624
  rmSync(claudeDir, { recursive: true, force: true });
3404
3625
  }
3405
- if (existsSync11(agentsDir)) {
3626
+ if (existsSync12(agentsDir)) {
3406
3627
  rmSync(agentsDir, { recursive: true, force: true });
3407
3628
  }
3408
3629
  try {
@@ -3410,7 +3631,7 @@ async function installSkill(projectRoot, name, content, source) {
3410
3631
  wroteClaudeDir = true;
3411
3632
  } catch {
3412
3633
  mkdirSync8(claudeDir, { recursive: true });
3413
- writeFileSync8(join11(claudeDir, "SKILL.md"), content, "utf-8");
3634
+ writeFileSync8(join12(claudeDir, "SKILL.md"), content, "utf-8");
3414
3635
  wroteClaudeDir = true;
3415
3636
  }
3416
3637
  try {
@@ -3418,7 +3639,7 @@ async function installSkill(projectRoot, name, content, source) {
3418
3639
  wroteAgentsDir = true;
3419
3640
  } catch {
3420
3641
  mkdirSync8(agentsDir, { recursive: true });
3421
- writeFileSync8(join11(agentsDir, "SKILL.md"), content, "utf-8");
3642
+ writeFileSync8(join12(agentsDir, "SKILL.md"), content, "utf-8");
3422
3643
  wroteAgentsDir = true;
3423
3644
  }
3424
3645
  } catch (err) {
@@ -3438,26 +3659,26 @@ async function installSkill(projectRoot, name, content, source) {
3438
3659
  throw new SkillInstallError(name, "register", err);
3439
3660
  }
3440
3661
  } catch (err) {
3441
- if (wroteClaudeDir && existsSync11(claudeDir)) {
3662
+ if (wroteClaudeDir && existsSync12(claudeDir)) {
3442
3663
  rmSync(claudeDir, { recursive: true, force: true });
3443
3664
  }
3444
- if (wroteAgentsDir && existsSync11(agentsDir)) {
3665
+ if (wroteAgentsDir && existsSync12(agentsDir)) {
3445
3666
  rmSync(agentsDir, { recursive: true, force: true });
3446
3667
  }
3447
3668
  throw err;
3448
3669
  } finally {
3449
- if (existsSync11(stagingDir)) {
3670
+ if (existsSync12(stagingDir)) {
3450
3671
  rmSync(stagingDir, { recursive: true, force: true });
3451
3672
  }
3452
3673
  }
3453
3674
  }
3454
3675
  async function removeSkill(projectRoot, name) {
3455
- const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
3456
- const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
3676
+ const claudeDir = join12(projectRoot, CLAUDE_SKILLS_DIR, name);
3677
+ const agentsDir = join12(projectRoot, AGENTS_SKILLS_DIR, name);
3457
3678
  const lock = readLockFile(projectRoot);
3458
3679
  const inLockFile = name in lock.skills;
3459
- const hasClaude = existsSync11(claudeDir);
3460
- const hasAgents = existsSync11(agentsDir);
3680
+ const hasClaude = existsSync12(claudeDir);
3681
+ const hasAgents = existsSync12(agentsDir);
3461
3682
  if (!inLockFile && !hasClaude && !hasAgents) {
3462
3683
  console.warn(`Skill "${name}" is not installed.`);
3463
3684
  return;
@@ -3478,10 +3699,10 @@ function isSkillInstalled(projectRoot, name) {
3478
3699
  return name in lock.skills;
3479
3700
  }
3480
3701
  function hasOrphanedSkillFiles(projectRoot, name) {
3481
- const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
3482
- const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
3702
+ const claudeDir = join12(projectRoot, CLAUDE_SKILLS_DIR, name);
3703
+ const agentsDir = join12(projectRoot, AGENTS_SKILLS_DIR, name);
3483
3704
  const inLockFile = isSkillInstalled(projectRoot, name);
3484
- return !inLockFile && (existsSync11(claudeDir) || existsSync11(agentsDir));
3705
+ return !inLockFile && (existsSync12(claudeDir) || existsSync12(agentsDir));
3485
3706
  }
3486
3707
  var SkillInstallError;
3487
3708
  var init_installer = __esm(() => {
@@ -4092,16 +4313,16 @@ __export(exports_logs, {
4092
4313
  logsCommand: () => logsCommand
4093
4314
  });
4094
4315
  import {
4095
- existsSync as existsSync12,
4316
+ existsSync as existsSync13,
4096
4317
  readdirSync as readdirSync2,
4097
- readFileSync as readFileSync9,
4318
+ readFileSync as readFileSync10,
4098
4319
  statSync as statSync2,
4099
4320
  unlinkSync as unlinkSync2
4100
4321
  } from "node:fs";
4101
- import { join as join12 } from "node:path";
4322
+ import { join as join13 } from "node:path";
4102
4323
  async function logsCommand(cwd, options) {
4103
- const logsDir = join12(cwd, ".locus", "logs");
4104
- if (!existsSync12(logsDir)) {
4324
+ const logsDir = join13(cwd, ".locus", "logs");
4325
+ if (!existsSync13(logsDir)) {
4105
4326
  process.stderr.write(`${dim2("No logs found.")}
4106
4327
  `);
4107
4328
  return;
@@ -4121,7 +4342,7 @@ async function logsCommand(cwd, options) {
4121
4342
  return viewLog(logFiles[0], options.level, options.lines ?? 50);
4122
4343
  }
4123
4344
  function viewLog(logFile, levelFilter, maxLines) {
4124
- const content = readFileSync9(logFile, "utf-8");
4345
+ const content = readFileSync10(logFile, "utf-8");
4125
4346
  const lines = content.trim().split(`
4126
4347
  `).filter(Boolean);
4127
4348
  process.stderr.write(`
@@ -4156,9 +4377,9 @@ async function tailLog(logFile, levelFilter) {
4156
4377
  process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
4157
4378
 
4158
4379
  `);
4159
- let lastSize = existsSync12(logFile) ? statSync2(logFile).size : 0;
4160
- if (existsSync12(logFile)) {
4161
- const content = readFileSync9(logFile, "utf-8");
4380
+ let lastSize = existsSync13(logFile) ? statSync2(logFile).size : 0;
4381
+ if (existsSync13(logFile)) {
4382
+ const content = readFileSync10(logFile, "utf-8");
4162
4383
  const lines = content.trim().split(`
4163
4384
  `).filter(Boolean);
4164
4385
  const recent = lines.slice(-10);
@@ -4176,12 +4397,12 @@ async function tailLog(logFile, levelFilter) {
4176
4397
  }
4177
4398
  return new Promise((resolve) => {
4178
4399
  const interval = setInterval(() => {
4179
- if (!existsSync12(logFile))
4400
+ if (!existsSync13(logFile))
4180
4401
  return;
4181
4402
  const currentSize = statSync2(logFile).size;
4182
4403
  if (currentSize <= lastSize)
4183
4404
  return;
4184
- const content = readFileSync9(logFile, "utf-8");
4405
+ const content = readFileSync10(logFile, "utf-8");
4185
4406
  const allLines = content.trim().split(`
4186
4407
  `).filter(Boolean);
4187
4408
  const oldContent = content.slice(0, lastSize);
@@ -4236,7 +4457,7 @@ function cleanLogs(logsDir) {
4236
4457
  `);
4237
4458
  }
4238
4459
  function getLogFiles(logsDir) {
4239
- return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join12(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
4460
+ return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join13(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
4240
4461
  }
4241
4462
  function formatEntry(entry) {
4242
4463
  const time = dim2(new Date(entry.ts).toLocaleTimeString());
@@ -4546,9 +4767,9 @@ var init_stream_renderer = __esm(() => {
4546
4767
 
4547
4768
  // src/repl/clipboard.ts
4548
4769
  import { execSync as execSync6 } from "node:child_process";
4549
- import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
4770
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9 } from "node:fs";
4550
4771
  import { tmpdir as tmpdir2 } from "node:os";
4551
- import { join as join13 } from "node:path";
4772
+ import { join as join14 } from "node:path";
4552
4773
  function readClipboardImage() {
4553
4774
  if (process.platform === "darwin") {
4554
4775
  return readMacOSClipboardImage();
@@ -4559,14 +4780,14 @@ function readClipboardImage() {
4559
4780
  return null;
4560
4781
  }
4561
4782
  function ensureStableDir() {
4562
- if (!existsSync13(STABLE_DIR)) {
4783
+ if (!existsSync14(STABLE_DIR)) {
4563
4784
  mkdirSync9(STABLE_DIR, { recursive: true });
4564
4785
  }
4565
4786
  }
4566
4787
  function readMacOSClipboardImage() {
4567
4788
  try {
4568
4789
  ensureStableDir();
4569
- const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
4790
+ const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
4570
4791
  const script = [
4571
4792
  `set destPath to POSIX file "${destPath}"`,
4572
4793
  "try",
@@ -4590,7 +4811,7 @@ function readMacOSClipboardImage() {
4590
4811
  timeout: 5000,
4591
4812
  stdio: ["pipe", "pipe", "pipe"]
4592
4813
  }).trim();
4593
- if (result === "ok" && existsSync13(destPath)) {
4814
+ if (result === "ok" && existsSync14(destPath)) {
4594
4815
  return destPath;
4595
4816
  }
4596
4817
  } catch {}
@@ -4603,9 +4824,9 @@ function readLinuxClipboardImage() {
4603
4824
  return null;
4604
4825
  }
4605
4826
  ensureStableDir();
4606
- const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
4827
+ const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
4607
4828
  execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
4608
- if (existsSync13(destPath)) {
4829
+ if (existsSync14(destPath)) {
4609
4830
  return destPath;
4610
4831
  }
4611
4832
  } catch {}
@@ -4613,13 +4834,13 @@ function readLinuxClipboardImage() {
4613
4834
  }
4614
4835
  var STABLE_DIR;
4615
4836
  var init_clipboard = __esm(() => {
4616
- STABLE_DIR = join13(tmpdir2(), "locus-images");
4837
+ STABLE_DIR = join14(tmpdir2(), "locus-images");
4617
4838
  });
4618
4839
 
4619
4840
  // src/repl/image-detect.ts
4620
- import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10 } from "node:fs";
4841
+ import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "node:fs";
4621
4842
  import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
4622
- import { basename, extname, join as join14, resolve } from "node:path";
4843
+ import { basename, extname, join as join15, resolve } from "node:path";
4623
4844
  function detectImages(input) {
4624
4845
  const detected = [];
4625
4846
  const byResolved = new Map;
@@ -4713,15 +4934,15 @@ function collectReferencedAttachments(input, attachments) {
4713
4934
  return dedupeByResolvedPath(selected);
4714
4935
  }
4715
4936
  function relocateImages(images, projectRoot) {
4716
- const targetDir = join14(projectRoot, ".locus", "tmp", "images");
4937
+ const targetDir = join15(projectRoot, ".locus", "tmp", "images");
4717
4938
  for (const img of images) {
4718
4939
  if (!img.exists)
4719
4940
  continue;
4720
4941
  try {
4721
- if (!existsSync14(targetDir)) {
4942
+ if (!existsSync15(targetDir)) {
4722
4943
  mkdirSync10(targetDir, { recursive: true });
4723
4944
  }
4724
- const dest = join14(targetDir, basename(img.stablePath));
4945
+ const dest = join15(targetDir, basename(img.stablePath));
4725
4946
  copyFileSync(img.stablePath, dest);
4726
4947
  img.stablePath = dest;
4727
4948
  } catch {}
@@ -4733,7 +4954,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
4733
4954
  return;
4734
4955
  let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
4735
4956
  if (resolved.startsWith("~/")) {
4736
- resolved = join14(homedir3(), resolved.slice(2));
4957
+ resolved = join15(homedir3(), resolved.slice(2));
4737
4958
  }
4738
4959
  resolved = resolve(resolved);
4739
4960
  const existing = byResolved.get(resolved);
@@ -4746,7 +4967,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
4746
4967
  ]);
4747
4968
  return;
4748
4969
  }
4749
- const exists = existsSync14(resolved);
4970
+ const exists = existsSync15(resolved);
4750
4971
  let stablePath = resolved;
4751
4972
  if (exists) {
4752
4973
  stablePath = copyToStable(resolved);
@@ -4800,10 +5021,10 @@ function dedupeByResolvedPath(images) {
4800
5021
  }
4801
5022
  function copyToStable(sourcePath) {
4802
5023
  try {
4803
- if (!existsSync14(STABLE_DIR2)) {
5024
+ if (!existsSync15(STABLE_DIR2)) {
4804
5025
  mkdirSync10(STABLE_DIR2, { recursive: true });
4805
5026
  }
4806
- const dest = join14(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
5027
+ const dest = join15(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
4807
5028
  copyFileSync(sourcePath, dest);
4808
5029
  return dest;
4809
5030
  } catch {
@@ -4823,7 +5044,7 @@ var init_image_detect = __esm(() => {
4823
5044
  ".tif",
4824
5045
  ".tiff"
4825
5046
  ]);
4826
- STABLE_DIR2 = join14(tmpdir3(), "locus-images");
5047
+ STABLE_DIR2 = join15(tmpdir3(), "locus-images");
4827
5048
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
4828
5049
  });
4829
5050
 
@@ -5832,21 +6053,21 @@ var init_claude = __esm(() => {
5832
6053
  import { exec } from "node:child_process";
5833
6054
  import {
5834
6055
  cpSync,
5835
- existsSync as existsSync15,
6056
+ existsSync as existsSync16,
5836
6057
  mkdirSync as mkdirSync11,
5837
6058
  mkdtempSync,
5838
6059
  readdirSync as readdirSync3,
5839
- readFileSync as readFileSync10,
6060
+ readFileSync as readFileSync11,
5840
6061
  rmSync as rmSync2,
5841
6062
  statSync as statSync3
5842
6063
  } from "node:fs";
5843
6064
  import { tmpdir as tmpdir4 } from "node:os";
5844
- import { dirname as dirname3, join as join15, relative } from "node:path";
6065
+ import { dirname as dirname3, join as join16, relative } from "node:path";
5845
6066
  import { promisify } from "node:util";
5846
6067
  function parseIgnoreFile(filePath) {
5847
- if (!existsSync15(filePath))
6068
+ if (!existsSync16(filePath))
5848
6069
  return [];
5849
- const content = readFileSync10(filePath, "utf-8");
6070
+ const content = readFileSync11(filePath, "utf-8");
5850
6071
  const rules = [];
5851
6072
  for (const rawLine of content.split(`
5852
6073
  `)) {
@@ -5914,14 +6135,14 @@ function findIgnoredPaths(projectRoot, rules) {
5914
6135
  for (const name of entries) {
5915
6136
  if (SKIP_DIRS.has(name))
5916
6137
  continue;
5917
- const fullPath = join15(dir, name);
5918
- let stat = null;
6138
+ const fullPath = join16(dir, name);
6139
+ let stat2 = null;
5919
6140
  try {
5920
- stat = statSync3(fullPath);
6141
+ stat2 = statSync3(fullPath);
5921
6142
  } catch {
5922
6143
  continue;
5923
6144
  }
5924
- const isDir = stat.isDirectory();
6145
+ const isDir = stat2.isDirectory();
5925
6146
  let matched = false;
5926
6147
  for (const m of positiveMatchers) {
5927
6148
  if (m.isDirectory && !isDir)
@@ -5948,7 +6169,7 @@ function findIgnoredPaths(projectRoot, rules) {
5948
6169
  }
5949
6170
  function backupIgnoredFiles(projectRoot) {
5950
6171
  const log = getLogger();
5951
- const ignorePath = join15(projectRoot, ".sandboxignore");
6172
+ const ignorePath = join16(projectRoot, ".sandboxignore");
5952
6173
  const rules = parseIgnoreFile(ignorePath);
5953
6174
  if (rules.length === 0)
5954
6175
  return NOOP_BACKUP;
@@ -5957,7 +6178,7 @@ function backupIgnoredFiles(projectRoot) {
5957
6178
  return NOOP_BACKUP;
5958
6179
  let backupDir;
5959
6180
  try {
5960
- backupDir = mkdtempSync(join15(tmpdir4(), "locus-sandbox-backup-"));
6181
+ backupDir = mkdtempSync(join16(tmpdir4(), "locus-sandbox-backup-"));
5961
6182
  } catch (err) {
5962
6183
  log.debug("Failed to create sandbox backup dir", {
5963
6184
  error: err instanceof Error ? err.message : String(err)
@@ -5967,7 +6188,7 @@ function backupIgnoredFiles(projectRoot) {
5967
6188
  const backed = [];
5968
6189
  for (const src of paths) {
5969
6190
  const rel = relative(projectRoot, src);
5970
- const dest = join15(backupDir, rel);
6191
+ const dest = join16(backupDir, rel);
5971
6192
  try {
5972
6193
  mkdirSync11(dirname3(dest), { recursive: true });
5973
6194
  cpSync(src, dest, { recursive: true, preserveTimestamps: true });
@@ -6009,7 +6230,7 @@ function backupIgnoredFiles(projectRoot) {
6009
6230
  }
6010
6231
  async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
6011
6232
  const log = getLogger();
6012
- const ignorePath = join15(projectRoot, ".sandboxignore");
6233
+ const ignorePath = join16(projectRoot, ".sandboxignore");
6013
6234
  const rules = parseIgnoreFile(ignorePath);
6014
6235
  if (rules.length === 0)
6015
6236
  return;
@@ -8175,8 +8396,8 @@ var init_sprint = __esm(() => {
8175
8396
 
8176
8397
  // src/core/prompt-builder.ts
8177
8398
  import { execSync as execSync9 } from "node:child_process";
8178
- import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
8179
- import { join as join16 } from "node:path";
8399
+ import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
8400
+ import { join as join17 } from "node:path";
8180
8401
  function buildExecutionPrompt(ctx) {
8181
8402
  const sections = [];
8182
8403
  sections.push(buildSystemContext(ctx.projectRoot));
@@ -8212,15 +8433,22 @@ function buildFeedbackPrompt(ctx) {
8212
8433
  }
8213
8434
  function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
8214
8435
  const sections = [];
8215
- const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
8436
+ const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
8216
8437
  if (locusmd) {
8217
8438
  sections.push(`<project-instructions>
8218
8439
  ${locusmd}
8219
8440
  </project-instructions>`);
8220
8441
  }
8221
- sections.push(`<past-learnings>
8222
- Past learnings are located in \`.locus/LEARNINGS.md\`.</past-learnings>`);
8223
- sections.push(`<learnings-reminder>IMPORTANT: If during this interaction you discover reusable lessons (architectural patterns, non-obvious constraints, user corrections), you MUST append them to \`.locus/LEARNINGS.md\` before finishing. This is mandatory — see the "Continuous Learning" section in project instructions.</learnings-reminder>`);
8442
+ const memory = loadMemoryContent(projectRoot);
8443
+ if (memory) {
8444
+ sections.push(`<past-learnings>
8445
+ ${memory}
8446
+ </past-learnings>`);
8447
+ } else {
8448
+ sections.push(`<past-learnings>
8449
+ No past learnings recorded yet.</past-learnings>`);
8450
+ }
8451
+ sections.push(`<learnings-reminder>IMPORTANT: If during this interaction you discover reusable lessons (architectural patterns, non-obvious constraints, user corrections), record them in the appropriate category file in \`.locus/memory/\` before finishing. This is mandatory — see the "Continuous Learning" section in project instructions.</learnings-reminder>`);
8224
8452
  if (previousMessages && previousMessages.length > 0) {
8225
8453
  const recent = previousMessages.slice(-10);
8226
8454
  const historyLines = recent.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`);
@@ -8239,22 +8467,41 @@ ${userMessage}
8239
8467
 
8240
8468
  `);
8241
8469
  }
8470
+ function loadMemoryContent(projectRoot) {
8471
+ const memoryDir = getMemoryDir(projectRoot);
8472
+ if (existsSync17(memoryDir)) {
8473
+ const content = readAllMemorySync(projectRoot);
8474
+ if (content.trim()) {
8475
+ return content.length > MEMORY_MAX_CHARS ? `${content.slice(0, MEMORY_MAX_CHARS)}
8476
+
8477
+ ...(truncated)` : content;
8478
+ }
8479
+ }
8480
+ return "";
8481
+ }
8242
8482
  function buildSystemContext(projectRoot) {
8243
8483
  const parts = [];
8244
- const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
8484
+ const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
8245
8485
  if (locusmd) {
8246
8486
  parts.push(`<project-instructions>
8247
8487
  ${locusmd}
8248
8488
  </project-instructions>`);
8249
8489
  }
8250
- parts.push(`<past-learnings>
8251
- Past learnings are located in \`.locus/LEARNINGS.md\`.</past-learnings>`);
8252
- const discussionsDir = join16(projectRoot, ".locus", "discussions");
8253
- if (existsSync16(discussionsDir)) {
8490
+ const memory = loadMemoryContent(projectRoot);
8491
+ if (memory) {
8492
+ parts.push(`<past-learnings>
8493
+ ${memory}
8494
+ </past-learnings>`);
8495
+ } else {
8496
+ parts.push(`<past-learnings>
8497
+ No past learnings recorded yet.</past-learnings>`);
8498
+ }
8499
+ const discussionsDir = join17(projectRoot, ".locus", "discussions");
8500
+ if (existsSync17(discussionsDir)) {
8254
8501
  try {
8255
8502
  const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
8256
8503
  for (const file of files) {
8257
- const content = readFileSafe(join16(discussionsDir, file));
8504
+ const content = readFileSafe(join17(discussionsDir, file));
8258
8505
  if (content) {
8259
8506
  const name = file.replace(".md", "");
8260
8507
  parts.push(`<discussion name="${name}">
@@ -8363,7 +8610,7 @@ function buildExecutionRules(config) {
8363
8610
  1. **Commit format:** Use conventional commits: \`feat: <title> (#<issue>)\`, \`fix: ...\`, \`chore: ...\`. Every commit message MUST be multi-line: the first line is the title, then a blank line, then \`Co-Authored-By: LocusAgent <agent@locusai.team>\` as a Git trailer. Use \`git commit -m "<title>" -m "Co-Authored-By: LocusAgent <agent@locusai.team>"\` (two separate -m flags) to ensure the trailer is on its own line.
8364
8611
  2. **Code quality:** Follow existing code style. Run linters/formatters if available.
8365
8612
  3. **Testing:** If test files exist for modified code, update them accordingly.
8366
- 4. **Update learnings:** Before finishing, if you discovered any reusable lessons (architectural patterns, non-obvious constraints, user corrections), append them to \`.locus/LEARNINGS.md\`. This is mandatory — see the "Continuous Learning" section in project instructions.
8613
+ 4. **Update memory:** Before finishing, if you discovered any reusable lessons (architectural patterns, non-obvious constraints, user corrections), record them in the appropriate category file in \`.locus/memory/\` (architecture.md, conventions.md, decisions.md, preferences.md, debugging.md). This is mandatory — see the "Continuous Learning" section in project instructions.
8367
8614
  5. **Do NOT:**
8368
8615
  - Run \`git push\` (the orchestrator handles pushing)
8369
8616
  - Modify files outside the scope of this issue
@@ -8401,13 +8648,13 @@ function buildFeedbackInstructions() {
8401
8648
  2. Make targeted changes — do NOT rewrite code from scratch.
8402
8649
  3. If a reviewer comment is unclear, make your best judgment and note your interpretation.
8403
8650
  4. Push changes to the same branch — do NOT create a new PR.
8404
- 5. If you learned any reusable lessons from this feedback (non-obvious constraints, architectural patterns), append them to \`.locus/LEARNINGS.md\`.
8651
+ 5. If you learned any reusable lessons from this feedback (non-obvious constraints, architectural patterns), record them in the appropriate category file in \`.locus/memory/\`.
8405
8652
  6. When done, summarize what you changed in response to each comment.
8406
8653
  </instructions>`;
8407
8654
  }
8408
8655
  function buildSkillsContext(projectRoot) {
8409
- const skillsDir = join16(projectRoot, CLAUDE_SKILLS_DIR);
8410
- if (!existsSync16(skillsDir))
8656
+ const skillsDir = join17(projectRoot, CLAUDE_SKILLS_DIR);
8657
+ if (!existsSync17(skillsDir))
8411
8658
  return null;
8412
8659
  let dirs;
8413
8660
  try {
@@ -8419,7 +8666,7 @@ function buildSkillsContext(projectRoot) {
8419
8666
  return null;
8420
8667
  const entries = [];
8421
8668
  for (const dir of dirs) {
8422
- const skillPath = join16(skillsDir, dir, "SKILL.md");
8669
+ const skillPath = join17(skillsDir, dir, "SKILL.md");
8423
8670
  const content = readFileSafe(skillPath);
8424
8671
  if (!content)
8425
8672
  continue;
@@ -8456,14 +8703,17 @@ function parseFrontmatter(content) {
8456
8703
  }
8457
8704
  function readFileSafe(path) {
8458
8705
  try {
8459
- if (!existsSync16(path))
8706
+ if (!existsSync17(path))
8460
8707
  return null;
8461
- return readFileSync11(path, "utf-8");
8708
+ return readFileSync12(path, "utf-8");
8462
8709
  } catch {
8463
8710
  return null;
8464
8711
  }
8465
8712
  }
8466
- var init_prompt_builder = () => {};
8713
+ var MEMORY_MAX_CHARS = 4000;
8714
+ var init_prompt_builder = __esm(() => {
8715
+ init_memory();
8716
+ });
8467
8717
 
8468
8718
  // src/display/json-stream.ts
8469
8719
  class JsonStream {
@@ -8555,6 +8805,180 @@ class JsonStream {
8555
8805
  }
8556
8806
  }
8557
8807
 
8808
+ // src/core/memory-capture.ts
8809
+ import { spawn as spawn6 } from "node:child_process";
8810
+ function prepareTranscript(messages) {
8811
+ if (!messages || messages.length === 0)
8812
+ return "";
8813
+ const lines = [];
8814
+ for (const msg of messages) {
8815
+ const label = msg.role === "user" ? "User" : "Assistant";
8816
+ lines.push(`### ${label}
8817
+ ${msg.content}`);
8818
+ }
8819
+ let transcript = lines.join(`
8820
+
8821
+ `);
8822
+ if (transcript.length > TRANSCRIPT_MAX_CHARS) {
8823
+ transcript = `${transcript.slice(0, TRANSCRIPT_MAX_CHARS)}
8824
+
8825
+ ...(truncated)`;
8826
+ }
8827
+ return transcript;
8828
+ }
8829
+ function buildExtractionPrompt(transcript, existingMemory) {
8830
+ const categoryList = Object.entries(MEMORY_CATEGORIES).map(([key, meta]) => `- "${key}": ${meta.title} — ${meta.description}`).join(`
8831
+ `);
8832
+ return `You are a memory extraction assistant. Extract project-level reusable lessons from the following session transcript.
8833
+
8834
+ ## Valid Categories
8835
+
8836
+ ${categoryList}
8837
+
8838
+ ## Existing Memory (for deduplication)
8839
+
8840
+ Do NOT extract entries that duplicate or closely overlap with these existing entries:
8841
+
8842
+ <existing-memory>
8843
+ ${existingMemory || "(none)"}
8844
+ </existing-memory>
8845
+
8846
+ ## Quality Bar
8847
+
8848
+ Only extract entries that would help a new agent on a future task. Skip:
8849
+ - Session-specific details or in-progress work
8850
+ - One-time fixes or trivial observations
8851
+ - Speculative or unverified conclusions
8852
+ - Anything that duplicates existing memory entries above
8853
+
8854
+ ## Output Format
8855
+
8856
+ Respond with ONLY a JSON array. No markdown fencing, no explanation. Example:
8857
+ [{"category": "architecture", "text": "SDK types are shared via @locusai/shared package"}]
8858
+
8859
+ If there are no extractable lessons, respond with: []
8860
+
8861
+ ## Session Transcript
8862
+
8863
+ ${transcript}`;
8864
+ }
8865
+ function callExtractionAI(prompt, model) {
8866
+ return new Promise((resolve2) => {
8867
+ const args = [
8868
+ "--print",
8869
+ "--dangerously-skip-permissions",
8870
+ "--no-session-persistence"
8871
+ ];
8872
+ if (model) {
8873
+ args.push("--model", model);
8874
+ }
8875
+ const env = { ...process.env };
8876
+ delete env.CLAUDECODE;
8877
+ delete env.CLAUDE_CODE;
8878
+ const proc = spawn6("claude", args, {
8879
+ stdio: ["pipe", "pipe", "pipe"],
8880
+ env
8881
+ });
8882
+ let output = "";
8883
+ let errorOutput = "";
8884
+ proc.stdout?.on("data", (chunk) => {
8885
+ output += chunk.toString();
8886
+ });
8887
+ proc.stderr?.on("data", (chunk) => {
8888
+ errorOutput += chunk.toString();
8889
+ });
8890
+ proc.on("close", (code) => {
8891
+ if (code === 0) {
8892
+ resolve2({ success: true, output });
8893
+ } else {
8894
+ resolve2({
8895
+ success: false,
8896
+ output,
8897
+ error: errorOutput || `claude exited with code ${code}`
8898
+ });
8899
+ }
8900
+ });
8901
+ proc.on("error", (err) => {
8902
+ resolve2({
8903
+ success: false,
8904
+ output: "",
8905
+ error: `Failed to spawn claude: ${err.message}`
8906
+ });
8907
+ });
8908
+ proc.stdin?.write(prompt);
8909
+ proc.stdin?.end();
8910
+ });
8911
+ }
8912
+ function parseExtractionResponse(raw) {
8913
+ let text = raw.trim();
8914
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
8915
+ if (fenceMatch) {
8916
+ text = fenceMatch[1].trim();
8917
+ }
8918
+ const arrayMatch = text.match(/\[[\s\S]*\]/);
8919
+ if (!arrayMatch)
8920
+ return [];
8921
+ const parsed = JSON.parse(arrayMatch[0]);
8922
+ if (!Array.isArray(parsed))
8923
+ return [];
8924
+ return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.category === "string" && typeof item.text === "string");
8925
+ }
8926
+ async function captureMemoryFromSession(projectRoot, transcript, config) {
8927
+ const log = getLogger();
8928
+ try {
8929
+ if (!transcript.trim()) {
8930
+ return { captured: 0 };
8931
+ }
8932
+ const existingMemory = await readAllMemory(projectRoot);
8933
+ const prompt = buildExtractionPrompt(transcript, existingMemory);
8934
+ const result = await callExtractionAI(prompt, config.model);
8935
+ if (!result.success) {
8936
+ log.warn("Memory capture: AI call failed", { error: result.error });
8937
+ return { captured: 0 };
8938
+ }
8939
+ let entries;
8940
+ try {
8941
+ entries = parseExtractionResponse(result.output);
8942
+ } catch (e) {
8943
+ log.warn("Memory capture: failed to parse AI response", {
8944
+ error: e instanceof Error ? e.message : String(e)
8945
+ });
8946
+ return { captured: 0 };
8947
+ }
8948
+ if (entries.length === 0) {
8949
+ return { captured: 0 };
8950
+ }
8951
+ const validEntries = entries.filter((entry) => {
8952
+ if (!VALID_CATEGORIES.has(entry.category)) {
8953
+ log.debug("Memory capture: skipping invalid category", {
8954
+ category: entry.category
8955
+ });
8956
+ return false;
8957
+ }
8958
+ return true;
8959
+ });
8960
+ if (validEntries.length === 0) {
8961
+ return { captured: 0 };
8962
+ }
8963
+ await appendMemoryEntries(projectRoot, validEntries);
8964
+ log.debug("Memory capture: extracted entries", {
8965
+ count: validEntries.length
8966
+ });
8967
+ return { captured: validEntries.length };
8968
+ } catch (e) {
8969
+ log.warn("Memory capture: unexpected error", {
8970
+ error: e instanceof Error ? e.message : String(e)
8971
+ });
8972
+ return { captured: 0 };
8973
+ }
8974
+ }
8975
+ var TRANSCRIPT_MAX_CHARS = 32000, VALID_CATEGORIES;
8976
+ var init_memory_capture = __esm(() => {
8977
+ init_logger();
8978
+ init_memory();
8979
+ VALID_CATEGORIES = new Set(Object.keys(MEMORY_CATEGORIES));
8980
+ });
8981
+
8558
8982
  // src/display/diff-renderer.ts
8559
8983
  function renderDiff(diff, options = {}) {
8560
8984
  const { maxLines, lineNumbers = true } = options;
@@ -8936,7 +9360,7 @@ var init_commands = __esm(() => {
8936
9360
 
8937
9361
  // src/repl/completions.ts
8938
9362
  import { readdirSync as readdirSync5 } from "node:fs";
8939
- import { basename as basename2, dirname as dirname4, join as join17 } from "node:path";
9363
+ import { basename as basename2, dirname as dirname4, join as join18 } from "node:path";
8940
9364
 
8941
9365
  class SlashCommandCompletion {
8942
9366
  commands;
@@ -8991,7 +9415,7 @@ class FilePathCompletion {
8991
9415
  }
8992
9416
  findMatches(partial) {
8993
9417
  try {
8994
- const dir = partial.includes("/") ? join17(this.projectRoot, dirname4(partial)) : this.projectRoot;
9418
+ const dir = partial.includes("/") ? join18(this.projectRoot, dirname4(partial)) : this.projectRoot;
8995
9419
  const prefix = basename2(partial);
8996
9420
  const entries = readdirSync5(dir, { withFileTypes: true });
8997
9421
  return entries.filter((e) => {
@@ -9027,14 +9451,14 @@ class CombinedCompletion {
9027
9451
  var init_completions = () => {};
9028
9452
 
9029
9453
  // src/repl/input-history.ts
9030
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
9031
- import { dirname as dirname5, join as join18 } from "node:path";
9454
+ import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
9455
+ import { dirname as dirname5, join as join19 } from "node:path";
9032
9456
 
9033
9457
  class InputHistory {
9034
9458
  entries = [];
9035
9459
  filePath;
9036
9460
  constructor(projectRoot) {
9037
- this.filePath = join18(projectRoot, ".locus", "sessions", ".input-history");
9461
+ this.filePath = join19(projectRoot, ".locus", "sessions", ".input-history");
9038
9462
  this.load();
9039
9463
  }
9040
9464
  add(text) {
@@ -9073,9 +9497,9 @@ class InputHistory {
9073
9497
  }
9074
9498
  load() {
9075
9499
  try {
9076
- if (!existsSync17(this.filePath))
9500
+ if (!existsSync18(this.filePath))
9077
9501
  return;
9078
- const content = readFileSync12(this.filePath, "utf-8");
9502
+ const content = readFileSync13(this.filePath, "utf-8");
9079
9503
  this.entries = content.split(`
9080
9504
  `).map((line) => this.unescape(line)).filter(Boolean);
9081
9505
  } catch {}
@@ -9083,7 +9507,7 @@ class InputHistory {
9083
9507
  save() {
9084
9508
  try {
9085
9509
  const dir = dirname5(this.filePath);
9086
- if (!existsSync17(dir)) {
9510
+ if (!existsSync18(dir)) {
9087
9511
  mkdirSync12(dir, { recursive: true });
9088
9512
  }
9089
9513
  const content = this.entries.map((e) => this.escape(e)).join(`
@@ -9114,20 +9538,20 @@ var init_model_config = __esm(() => {
9114
9538
 
9115
9539
  // src/repl/session-manager.ts
9116
9540
  import {
9117
- existsSync as existsSync18,
9541
+ existsSync as existsSync19,
9118
9542
  mkdirSync as mkdirSync13,
9119
9543
  readdirSync as readdirSync6,
9120
- readFileSync as readFileSync13,
9544
+ readFileSync as readFileSync14,
9121
9545
  unlinkSync as unlinkSync3,
9122
9546
  writeFileSync as writeFileSync10
9123
9547
  } from "node:fs";
9124
- import { basename as basename3, join as join19 } from "node:path";
9548
+ import { basename as basename3, join as join20 } from "node:path";
9125
9549
 
9126
9550
  class SessionManager {
9127
9551
  sessionsDir;
9128
9552
  constructor(projectRoot) {
9129
- this.sessionsDir = join19(projectRoot, ".locus", "sessions");
9130
- if (!existsSync18(this.sessionsDir)) {
9553
+ this.sessionsDir = join20(projectRoot, ".locus", "sessions");
9554
+ if (!existsSync19(this.sessionsDir)) {
9131
9555
  mkdirSync13(this.sessionsDir, { recursive: true });
9132
9556
  }
9133
9557
  }
@@ -9153,14 +9577,14 @@ class SessionManager {
9153
9577
  }
9154
9578
  isPersisted(sessionOrId) {
9155
9579
  const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
9156
- return existsSync18(this.getSessionPath(sessionId));
9580
+ return existsSync19(this.getSessionPath(sessionId));
9157
9581
  }
9158
9582
  load(idOrPrefix) {
9159
9583
  const files = this.listSessionFiles();
9160
9584
  const exactPath = this.getSessionPath(idOrPrefix);
9161
- if (existsSync18(exactPath)) {
9585
+ if (existsSync19(exactPath)) {
9162
9586
  try {
9163
- return JSON.parse(readFileSync13(exactPath, "utf-8"));
9587
+ return JSON.parse(readFileSync14(exactPath, "utf-8"));
9164
9588
  } catch {
9165
9589
  return null;
9166
9590
  }
@@ -9168,7 +9592,7 @@ class SessionManager {
9168
9592
  const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
9169
9593
  if (matches.length === 1) {
9170
9594
  try {
9171
- return JSON.parse(readFileSync13(matches[0], "utf-8"));
9595
+ return JSON.parse(readFileSync14(matches[0], "utf-8"));
9172
9596
  } catch {
9173
9597
  return null;
9174
9598
  }
@@ -9193,7 +9617,7 @@ class SessionManager {
9193
9617
  const sessions = [];
9194
9618
  for (const file of files) {
9195
9619
  try {
9196
- const session = JSON.parse(readFileSync13(file, "utf-8"));
9620
+ const session = JSON.parse(readFileSync14(file, "utf-8"));
9197
9621
  sessions.push({
9198
9622
  id: session.id,
9199
9623
  created: session.created,
@@ -9208,7 +9632,7 @@ class SessionManager {
9208
9632
  }
9209
9633
  delete(sessionId) {
9210
9634
  const path = this.getSessionPath(sessionId);
9211
- if (existsSync18(path)) {
9635
+ if (existsSync19(path)) {
9212
9636
  unlinkSync3(path);
9213
9637
  return true;
9214
9638
  }
@@ -9220,7 +9644,7 @@ class SessionManager {
9220
9644
  let pruned = 0;
9221
9645
  const withStats = files.map((f) => {
9222
9646
  try {
9223
- const session = JSON.parse(readFileSync13(f, "utf-8"));
9647
+ const session = JSON.parse(readFileSync14(f, "utf-8"));
9224
9648
  return { path: f, updated: new Date(session.updated).getTime() };
9225
9649
  } catch {
9226
9650
  return { path: f, updated: 0 };
@@ -9238,7 +9662,7 @@ class SessionManager {
9238
9662
  const remaining = withStats.length - pruned;
9239
9663
  if (remaining > MAX_SESSIONS) {
9240
9664
  const toRemove = remaining - MAX_SESSIONS;
9241
- const alive = withStats.filter((e) => existsSync18(e.path));
9665
+ const alive = withStats.filter((e) => existsSync19(e.path));
9242
9666
  for (let i = 0;i < toRemove && i < alive.length; i++) {
9243
9667
  try {
9244
9668
  unlinkSync3(alive[i].path);
@@ -9253,7 +9677,7 @@ class SessionManager {
9253
9677
  }
9254
9678
  listSessionFiles() {
9255
9679
  try {
9256
- return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join19(this.sessionsDir, f));
9680
+ return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join20(this.sessionsDir, f));
9257
9681
  } catch {
9258
9682
  return [];
9259
9683
  }
@@ -9262,7 +9686,7 @@ class SessionManager {
9262
9686
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
9263
9687
  }
9264
9688
  getSessionPath(sessionId) {
9265
- return join19(this.sessionsDir, `${sessionId}.json`);
9689
+ return join20(this.sessionsDir, `${sessionId}.json`);
9266
9690
  }
9267
9691
  }
9268
9692
  var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
@@ -9272,12 +9696,12 @@ var init_session_manager = __esm(() => {
9272
9696
  });
9273
9697
 
9274
9698
  // src/repl/voice.ts
9275
- import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
9276
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
9699
+ import { execSync as execSync11, spawn as spawn7 } from "node:child_process";
9700
+ import { existsSync as existsSync20, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
9277
9701
  import { cpus, homedir as homedir4, platform, tmpdir as tmpdir5 } from "node:os";
9278
- import { join as join20 } from "node:path";
9702
+ import { join as join21 } from "node:path";
9279
9703
  function getWhisperModelPath() {
9280
- return join20(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
9704
+ return join21(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
9281
9705
  }
9282
9706
  function commandExists(cmd) {
9283
9707
  try {
@@ -9295,16 +9719,16 @@ function findWhisperBinary() {
9295
9719
  return name;
9296
9720
  }
9297
9721
  for (const name of candidates) {
9298
- const fullPath = join20(LOCUS_BIN_DIR, name);
9299
- if (existsSync19(fullPath))
9722
+ const fullPath = join21(LOCUS_BIN_DIR, name);
9723
+ if (existsSync20(fullPath))
9300
9724
  return fullPath;
9301
9725
  }
9302
9726
  if (platform() === "darwin") {
9303
9727
  const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
9304
9728
  for (const dir of brewDirs) {
9305
9729
  for (const name of candidates) {
9306
- const fullPath = join20(dir, name);
9307
- if (existsSync19(fullPath))
9730
+ const fullPath = join21(dir, name);
9731
+ if (existsSync20(fullPath))
9308
9732
  return fullPath;
9309
9733
  }
9310
9734
  }
@@ -9319,11 +9743,11 @@ function findSoxRecBinary() {
9319
9743
  if (platform() === "darwin") {
9320
9744
  const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
9321
9745
  for (const dir of brewDirs) {
9322
- const recPath = join20(dir, "rec");
9323
- if (existsSync19(recPath))
9746
+ const recPath = join21(dir, "rec");
9747
+ if (existsSync20(recPath))
9324
9748
  return recPath;
9325
- const soxPath = join20(dir, "sox");
9326
- if (existsSync19(soxPath))
9749
+ const soxPath = join21(dir, "sox");
9750
+ if (existsSync20(soxPath))
9327
9751
  return soxPath;
9328
9752
  }
9329
9753
  }
@@ -9332,7 +9756,7 @@ function findSoxRecBinary() {
9332
9756
  function checkDependencies() {
9333
9757
  const soxBinary = findSoxRecBinary();
9334
9758
  const whisperBinary = findWhisperBinary();
9335
- const modelDownloaded = existsSync19(getWhisperModelPath());
9759
+ const modelDownloaded = existsSync20(getWhisperModelPath());
9336
9760
  return {
9337
9761
  sox: soxBinary !== null,
9338
9762
  whisper: whisperBinary !== null,
@@ -9486,7 +9910,7 @@ function ensureBuildDeps(pm) {
9486
9910
  }
9487
9911
  function buildWhisperFromSource(pm) {
9488
9912
  const out = process.stderr;
9489
- const buildDir = join20(tmpdir5(), `locus-whisper-build-${process.pid}`);
9913
+ const buildDir = join21(tmpdir5(), `locus-whisper-build-${process.pid}`);
9490
9914
  if (!ensureBuildDeps(pm)) {
9491
9915
  out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
9492
9916
  `);
@@ -9497,8 +9921,8 @@ function buildWhisperFromSource(pm) {
9497
9921
  mkdirSync14(LOCUS_BIN_DIR, { recursive: true });
9498
9922
  out.write(` ${dim2("Cloning whisper.cpp...")}
9499
9923
  `);
9500
- execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join20(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
9501
- const srcDir = join20(buildDir, "whisper.cpp");
9924
+ execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join21(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
9925
+ const srcDir = join21(buildDir, "whisper.cpp");
9502
9926
  const numCpus = cpus().length || 2;
9503
9927
  out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
9504
9928
  `);
@@ -9512,13 +9936,13 @@ function buildWhisperFromSource(pm) {
9512
9936
  stdio: ["pipe", "pipe", "pipe"],
9513
9937
  timeout: 600000
9514
9938
  });
9515
- const destPath = join20(LOCUS_BIN_DIR, "whisper-cli");
9939
+ const destPath = join21(LOCUS_BIN_DIR, "whisper-cli");
9516
9940
  const binaryCandidates = [
9517
- join20(srcDir, "build", "bin", "whisper-cli"),
9518
- join20(srcDir, "build", "bin", "main")
9941
+ join21(srcDir, "build", "bin", "whisper-cli"),
9942
+ join21(srcDir, "build", "bin", "main")
9519
9943
  ];
9520
9944
  for (const candidate of binaryCandidates) {
9521
- if (existsSync19(candidate)) {
9945
+ if (existsSync20(candidate)) {
9522
9946
  execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
9523
9947
  stdio: "pipe"
9524
9948
  });
@@ -9597,7 +10021,7 @@ ${bold2("Installing voice dependencies...")}
9597
10021
  }
9598
10022
  function downloadModel() {
9599
10023
  const modelPath = getWhisperModelPath();
9600
- if (existsSync19(modelPath))
10024
+ if (existsSync20(modelPath))
9601
10025
  return true;
9602
10026
  mkdirSync14(WHISPER_MODELS_DIR, { recursive: true });
9603
10027
  const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
@@ -9644,7 +10068,7 @@ class VoiceController {
9644
10068
  onStateChange;
9645
10069
  constructor(options) {
9646
10070
  this.onStateChange = options.onStateChange;
9647
- this.tempFile = join20(tmpdir5(), `locus-voice-${process.pid}.wav`);
10071
+ this.tempFile = join21(tmpdir5(), `locus-voice-${process.pid}.wav`);
9648
10072
  this.deps = checkDependencies();
9649
10073
  }
9650
10074
  getState() {
@@ -9680,7 +10104,7 @@ class VoiceController {
9680
10104
  return false;
9681
10105
  }
9682
10106
  const spawnArgs = binary === "rec" ? args : ["-d", ...args];
9683
- this.recordProcess = spawn6(binary, spawnArgs, {
10107
+ this.recordProcess = spawn7(binary, spawnArgs, {
9684
10108
  stdio: ["pipe", "pipe", "pipe"]
9685
10109
  });
9686
10110
  this.recordProcess.on("error", (err) => {
@@ -9712,7 +10136,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
9712
10136
  this.recordProcess = null;
9713
10137
  this.setState("idle");
9714
10138
  await sleep2(200);
9715
- if (!existsSync19(this.tempFile)) {
10139
+ if (!existsSync20(this.tempFile)) {
9716
10140
  return null;
9717
10141
  }
9718
10142
  try {
@@ -9747,7 +10171,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
9747
10171
  "--language",
9748
10172
  WHISPER_MODEL.endsWith(".en") ? "en" : "auto"
9749
10173
  ];
9750
- const proc = spawn6(binary, args, {
10174
+ const proc = spawn7(binary, args, {
9751
10175
  stdio: ["pipe", "pipe", "pipe"]
9752
10176
  });
9753
10177
  let stdout = "";
@@ -9803,8 +10227,8 @@ function sleep2(ms) {
9803
10227
  var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
9804
10228
  var init_voice = __esm(() => {
9805
10229
  init_terminal();
9806
- WHISPER_MODELS_DIR = join20(homedir4(), ".locus", "whisper-models");
9807
- LOCUS_BIN_DIR = join20(homedir4(), ".locus", "bin");
10230
+ WHISPER_MODELS_DIR = join21(homedir4(), ".locus", "whisper-models");
10231
+ LOCUS_BIN_DIR = join21(homedir4(), ".locus", "bin");
9808
10232
  });
9809
10233
 
9810
10234
  // src/repl/repl.ts
@@ -9842,6 +10266,16 @@ async function startRepl(options) {
9842
10266
  }
9843
10267
  if (options.prompt) {
9844
10268
  await executeOneShotPrompt(options.prompt, session, sessionManager, options);
10269
+ if (session.messages.length >= 2) {
10270
+ const log = getLogger();
10271
+ const transcript = prepareTranscript(session.messages);
10272
+ captureMemoryFromSession(projectRoot, transcript, {
10273
+ model: config.ai?.model
10274
+ }).then((result) => {
10275
+ if (result.captured > 0)
10276
+ log.info(`Captured ${result.captured} memory entries`);
10277
+ }).catch(() => {});
10278
+ }
9845
10279
  return;
9846
10280
  }
9847
10281
  if (!process.stdin.isTTY) {
@@ -9906,6 +10340,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
9906
10340
  });
9907
10341
  printWelcome(session);
9908
10342
  let shouldExit = false;
10343
+ let wasInterrupted = false;
9909
10344
  let currentProvider = inferProviderFromModel(config.ai.model) || config.ai.provider;
9910
10345
  let currentModel = config.ai.model;
9911
10346
  let verbose = true;
@@ -10021,6 +10456,7 @@ ${red2("✗")} ${msg}
10021
10456
  break;
10022
10457
  }
10023
10458
  case "interrupt":
10459
+ wasInterrupted = true;
10024
10460
  shouldExit = true;
10025
10461
  break;
10026
10462
  case "exit":
@@ -10031,6 +10467,16 @@ ${red2("✗")} ${msg}
10031
10467
  }
10032
10468
  }
10033
10469
  voice.cancel();
10470
+ if (!wasInterrupted && session.messages.length >= 2) {
10471
+ const log = getLogger();
10472
+ const transcript = prepareTranscript(session.messages);
10473
+ captureMemoryFromSession(projectRoot, transcript, {
10474
+ model: config.ai?.model
10475
+ }).then((result) => {
10476
+ if (result.captured > 0)
10477
+ log.info(`Captured ${result.captured} memory entries`);
10478
+ }).catch(() => {});
10479
+ }
10034
10480
  const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
10035
10481
  if (shouldPersistOnExit) {
10036
10482
  sessionManager.save(session);
@@ -10107,6 +10553,8 @@ var init_repl = __esm(() => {
10107
10553
  init_run_ai();
10108
10554
  init_runner();
10109
10555
  init_ai_models();
10556
+ init_logger();
10557
+ init_memory_capture();
10110
10558
  init_prompt_builder();
10111
10559
  init_sandbox();
10112
10560
  init_terminal();
@@ -10316,8 +10764,8 @@ var init_exec = __esm(() => {
10316
10764
 
10317
10765
  // src/core/submodule.ts
10318
10766
  import { execSync as execSync13 } from "node:child_process";
10319
- import { existsSync as existsSync20 } from "node:fs";
10320
- import { join as join21 } from "node:path";
10767
+ import { existsSync as existsSync21 } from "node:fs";
10768
+ import { join as join22 } from "node:path";
10321
10769
  function git2(args, cwd) {
10322
10770
  return execSync13(`git ${args}`, {
10323
10771
  cwd,
@@ -10333,7 +10781,7 @@ function gitSafe(args, cwd) {
10333
10781
  }
10334
10782
  }
10335
10783
  function hasSubmodules(cwd) {
10336
- return existsSync20(join21(cwd, ".gitmodules"));
10784
+ return existsSync21(join22(cwd, ".gitmodules"));
10337
10785
  }
10338
10786
  function listSubmodules(cwd) {
10339
10787
  if (!hasSubmodules(cwd))
@@ -10353,7 +10801,7 @@ function listSubmodules(cwd) {
10353
10801
  continue;
10354
10802
  submodules.push({
10355
10803
  path,
10356
- absolutePath: join21(cwd, path),
10804
+ absolutePath: join22(cwd, path),
10357
10805
  dirty
10358
10806
  });
10359
10807
  }
@@ -10366,7 +10814,7 @@ function getDirtySubmodules(cwd) {
10366
10814
  const submodules = listSubmodules(cwd);
10367
10815
  const dirty = [];
10368
10816
  for (const sub of submodules) {
10369
- if (!existsSync20(sub.absolutePath))
10817
+ if (!existsSync21(sub.absolutePath))
10370
10818
  continue;
10371
10819
  const status = gitSafe("status --porcelain", sub.absolutePath);
10372
10820
  if (status && status.trim().length > 0) {
@@ -10453,7 +10901,7 @@ function pushSubmoduleBranches(cwd) {
10453
10901
  const log = getLogger();
10454
10902
  const submodules = listSubmodules(cwd);
10455
10903
  for (const sub of submodules) {
10456
- if (!existsSync20(sub.absolutePath))
10904
+ if (!existsSync21(sub.absolutePath))
10457
10905
  continue;
10458
10906
  const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
10459
10907
  if (!branch || branch === "HEAD")
@@ -10605,6 +11053,19 @@ ${summary}
10605
11053
  Duration: ${timer.formatted()}${prNumber ? `
10606
11054
  PR: #${prNumber}` : ""}`, { cwd: projectRoot });
10607
11055
  } catch {}
11056
+ const transcript = prepareTranscript([
11057
+ {
11058
+ role: "user",
11059
+ content: `Issue #${issueNumber}: ${issue.title}
11060
+
11061
+ ${issue.body}`
11062
+ },
11063
+ { role: "assistant", content: output }
11064
+ ]);
11065
+ captureMemoryFromSession(projectRoot, transcript, { model: config.ai?.model }).then((result) => {
11066
+ if (result.captured > 0)
11067
+ log.info(`Captured ${result.captured} memory entries`);
11068
+ }).catch(() => {});
10608
11069
  return {
10609
11070
  issueNumber,
10610
11071
  success: true,
@@ -10786,6 +11247,7 @@ var init_agent = __esm(() => {
10786
11247
  init_config();
10787
11248
  init_github();
10788
11249
  init_logger();
11250
+ init_memory_capture();
10789
11251
  init_prompt_builder();
10790
11252
  init_sandbox();
10791
11253
  init_submodule();
@@ -10923,15 +11385,15 @@ var init_conflict = __esm(() => {
10923
11385
 
10924
11386
  // src/core/run-state.ts
10925
11387
  import {
10926
- existsSync as existsSync21,
11388
+ existsSync as existsSync22,
10927
11389
  mkdirSync as mkdirSync15,
10928
- readFileSync as readFileSync14,
11390
+ readFileSync as readFileSync15,
10929
11391
  unlinkSync as unlinkSync5,
10930
11392
  writeFileSync as writeFileSync11
10931
11393
  } from "node:fs";
10932
- import { dirname as dirname6, join as join22 } from "node:path";
11394
+ import { dirname as dirname6, join as join23 } from "node:path";
10933
11395
  function getRunStateDir(projectRoot) {
10934
- return join22(projectRoot, ".locus", "run-state");
11396
+ return join23(projectRoot, ".locus", "run-state");
10935
11397
  }
10936
11398
  function sprintSlug(name) {
10937
11399
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -10939,16 +11401,16 @@ function sprintSlug(name) {
10939
11401
  function getRunStatePath(projectRoot, sprintName) {
10940
11402
  const dir = getRunStateDir(projectRoot);
10941
11403
  if (sprintName) {
10942
- return join22(dir, `${sprintSlug(sprintName)}.json`);
11404
+ return join23(dir, `${sprintSlug(sprintName)}.json`);
10943
11405
  }
10944
- return join22(dir, "_parallel.json");
11406
+ return join23(dir, "_parallel.json");
10945
11407
  }
10946
11408
  function loadRunState(projectRoot, sprintName) {
10947
11409
  const path = getRunStatePath(projectRoot, sprintName);
10948
- if (!existsSync21(path))
11410
+ if (!existsSync22(path))
10949
11411
  return null;
10950
11412
  try {
10951
- return JSON.parse(readFileSync14(path, "utf-8"));
11413
+ return JSON.parse(readFileSync15(path, "utf-8"));
10952
11414
  } catch {
10953
11415
  getLogger().warn("Corrupted run state file, ignoring");
10954
11416
  return null;
@@ -10957,7 +11419,7 @@ function loadRunState(projectRoot, sprintName) {
10957
11419
  function saveRunState(projectRoot, state) {
10958
11420
  const path = getRunStatePath(projectRoot, state.sprint);
10959
11421
  const dir = dirname6(path);
10960
- if (!existsSync21(dir)) {
11422
+ if (!existsSync22(dir)) {
10961
11423
  mkdirSync15(dir, { recursive: true });
10962
11424
  }
10963
11425
  writeFileSync11(path, `${JSON.stringify(state, null, 2)}
@@ -10965,7 +11427,7 @@ function saveRunState(projectRoot, state) {
10965
11427
  }
10966
11428
  function clearRunState(projectRoot, sprintName) {
10967
11429
  const path = getRunStatePath(projectRoot, sprintName);
10968
- if (existsSync21(path)) {
11430
+ if (existsSync22(path)) {
10969
11431
  unlinkSync5(path);
10970
11432
  }
10971
11433
  }
@@ -11123,23 +11585,23 @@ __export(exports_worktree, {
11123
11585
  import { execSync as execSync16 } from "node:child_process";
11124
11586
  import {
11125
11587
  cpSync as cpSync2,
11126
- existsSync as existsSync22,
11588
+ existsSync as existsSync23,
11127
11589
  mkdirSync as mkdirSync16,
11128
11590
  readdirSync as readdirSync7,
11129
11591
  realpathSync,
11130
11592
  statSync as statSync4
11131
11593
  } from "node:fs";
11132
- import { join as join23 } from "node:path";
11594
+ import { join as join24 } from "node:path";
11133
11595
  function copyLocusDir(projectRoot, worktreePath) {
11134
- const srcLocus = join23(projectRoot, ".locus");
11135
- if (!existsSync22(srcLocus))
11596
+ const srcLocus = join24(projectRoot, ".locus");
11597
+ if (!existsSync23(srcLocus))
11136
11598
  return;
11137
- const destLocus = join23(worktreePath, ".locus");
11599
+ const destLocus = join24(worktreePath, ".locus");
11138
11600
  mkdirSync16(destLocus, { recursive: true });
11139
11601
  for (const entry of readdirSync7(srcLocus)) {
11140
11602
  if (entry === "worktrees")
11141
11603
  continue;
11142
- cpSync2(join23(srcLocus, entry), join23(destLocus, entry), { recursive: true });
11604
+ cpSync2(join24(srcLocus, entry), join24(destLocus, entry), { recursive: true });
11143
11605
  }
11144
11606
  }
11145
11607
  function git4(args, cwd) {
@@ -11157,13 +11619,13 @@ function gitSafe3(args, cwd) {
11157
11619
  }
11158
11620
  }
11159
11621
  function getWorktreeDir(projectRoot) {
11160
- return join23(projectRoot, ".locus", "worktrees");
11622
+ return join24(projectRoot, ".locus", "worktrees");
11161
11623
  }
11162
11624
  function getWorktreePath(projectRoot, issueNumber) {
11163
- return join23(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
11625
+ return join24(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
11164
11626
  }
11165
11627
  function getSprintWorktreePath(projectRoot, sprintSlug2) {
11166
- return join23(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
11628
+ return join24(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
11167
11629
  }
11168
11630
  function generateBranchName(issueNumber) {
11169
11631
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -11176,7 +11638,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
11176
11638
  const log = getLogger();
11177
11639
  const slug = sprintSlug2(sprintName);
11178
11640
  const worktreePath = getSprintWorktreePath(projectRoot, slug);
11179
- if (existsSync22(worktreePath)) {
11641
+ if (existsSync23(worktreePath)) {
11180
11642
  log.verbose(`Sprint worktree already exists for "${sprintName}"`);
11181
11643
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
11182
11644
  return { path: worktreePath, branch: existingBranch };
@@ -11197,7 +11659,7 @@ function removeSprintWorktree(projectRoot, sprintName) {
11197
11659
  const log = getLogger();
11198
11660
  const slug = sprintSlug2(sprintName);
11199
11661
  const worktreePath = getSprintWorktreePath(projectRoot, slug);
11200
- if (!existsSync22(worktreePath)) {
11662
+ if (!existsSync23(worktreePath)) {
11201
11663
  log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
11202
11664
  return;
11203
11665
  }
@@ -11227,7 +11689,7 @@ function getWorktreeBranch(worktreePath) {
11227
11689
  function createWorktree(projectRoot, issueNumber, baseBranch) {
11228
11690
  const log = getLogger();
11229
11691
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11230
- if (existsSync22(worktreePath)) {
11692
+ if (existsSync23(worktreePath)) {
11231
11693
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
11232
11694
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
11233
11695
  return {
@@ -11256,7 +11718,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
11256
11718
  function removeWorktree(projectRoot, issueNumber) {
11257
11719
  const log = getLogger();
11258
11720
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11259
- if (!existsSync22(worktreePath)) {
11721
+ if (!existsSync23(worktreePath)) {
11260
11722
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
11261
11723
  return;
11262
11724
  }
@@ -11275,7 +11737,7 @@ function removeWorktree(projectRoot, issueNumber) {
11275
11737
  function listWorktrees(projectRoot) {
11276
11738
  const log = getLogger();
11277
11739
  const worktreeDir = getWorktreeDir(projectRoot);
11278
- if (!existsSync22(worktreeDir)) {
11740
+ if (!existsSync23(worktreeDir)) {
11279
11741
  return [];
11280
11742
  }
11281
11743
  const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
@@ -11295,7 +11757,7 @@ function listWorktrees(projectRoot) {
11295
11757
  if (!match)
11296
11758
  continue;
11297
11759
  const issueNumber = Number.parseInt(match[1], 10);
11298
- const path = join23(worktreeDir, entry);
11760
+ const path = join24(worktreeDir, entry);
11299
11761
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
11300
11762
  let resolvedPath;
11301
11763
  try {
@@ -11335,7 +11797,7 @@ function cleanupStaleWorktrees(projectRoot) {
11335
11797
  }
11336
11798
  function pushWorktreeBranch(projectRoot, issueNumber) {
11337
11799
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11338
- if (!existsSync22(worktreePath)) {
11800
+ if (!existsSync23(worktreePath)) {
11339
11801
  throw new Error(`Worktree for issue #${issueNumber} does not exist`);
11340
11802
  }
11341
11803
  const branch = getWorktreeBranch(worktreePath);
@@ -11347,18 +11809,18 @@ function pushWorktreeBranch(projectRoot, issueNumber) {
11347
11809
  }
11348
11810
  function hasWorktreeChanges(projectRoot, issueNumber) {
11349
11811
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11350
- if (!existsSync22(worktreePath))
11812
+ if (!existsSync23(worktreePath))
11351
11813
  return false;
11352
11814
  const status = gitSafe3("status --porcelain", worktreePath);
11353
11815
  return status !== null && status.trim().length > 0;
11354
11816
  }
11355
11817
  function getWorktreeAge(projectRoot, issueNumber) {
11356
11818
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11357
- if (!existsSync22(worktreePath))
11819
+ if (!existsSync23(worktreePath))
11358
11820
  return 0;
11359
11821
  try {
11360
- const stat = statSync4(worktreePath);
11361
- return Date.now() - stat.ctimeMs;
11822
+ const stat2 = statSync4(worktreePath);
11823
+ return Date.now() - stat2.ctimeMs;
11362
11824
  } catch {
11363
11825
  return 0;
11364
11826
  }
@@ -11374,8 +11836,8 @@ __export(exports_run, {
11374
11836
  runCommand: () => runCommand
11375
11837
  });
11376
11838
  import { execSync as execSync17 } from "node:child_process";
11377
- import { existsSync as existsSync23 } from "node:fs";
11378
- import { join as join24 } from "node:path";
11839
+ import { existsSync as existsSync24 } from "node:fs";
11840
+ import { join as join25 } from "node:path";
11379
11841
  function resolveExecutionContext(config, modelOverride) {
11380
11842
  const model = modelOverride ?? config.ai.model;
11381
11843
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -11933,8 +12395,8 @@ async function handleResume(projectRoot, config, sandboxed, flags) {
11933
12395
  const sprintsToResume = [];
11934
12396
  try {
11935
12397
  const { readdirSync: readdirSync8 } = await import("node:fs");
11936
- const runStateDir = join24(projectRoot, ".locus", "run-state");
11937
- if (existsSync23(runStateDir)) {
12398
+ const runStateDir = join25(projectRoot, ".locus", "run-state");
12399
+ if (existsSync24(runStateDir)) {
11938
12400
  const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
11939
12401
  for (const file of files) {
11940
12402
  const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
@@ -11978,9 +12440,9 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` ($
11978
12440
  let workDir = projectRoot;
11979
12441
  if (state.type === "sprint" && state.sprint) {
11980
12442
  const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
11981
- const { existsSync: existsSync24 } = await import("node:fs");
12443
+ const { existsSync: existsSync25 } = await import("node:fs");
11982
12444
  const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
11983
- if (existsSync24(wtPath)) {
12445
+ if (existsSync25(wtPath)) {
11984
12446
  workDir = wtPath;
11985
12447
  } else if (state.branch) {
11986
12448
  try {
@@ -12210,8 +12672,8 @@ __export(exports_status, {
12210
12672
  statusCommand: () => statusCommand
12211
12673
  });
12212
12674
  import { execSync as execSync18 } from "node:child_process";
12213
- import { existsSync as existsSync24 } from "node:fs";
12214
- import { dirname as dirname7, join as join25 } from "node:path";
12675
+ import { existsSync as existsSync25 } from "node:fs";
12676
+ import { dirname as dirname7, join as join26 } from "node:path";
12215
12677
  async function statusCommand(projectRoot) {
12216
12678
  const config = loadConfig(projectRoot);
12217
12679
  const spinner = new Spinner;
@@ -12343,13 +12805,13 @@ ${drawBox(lines, { title: "Locus Status" })}
12343
12805
  `);
12344
12806
  }
12345
12807
  function getPm2Bin() {
12346
- const pkgsBin = join25(getPackagesDir(), "node_modules", ".bin", "pm2");
12347
- if (existsSync24(pkgsBin))
12808
+ const pkgsBin = join26(getPackagesDir(), "node_modules", ".bin", "pm2");
12809
+ if (existsSync25(pkgsBin))
12348
12810
  return pkgsBin;
12349
12811
  let dir = process.cwd();
12350
12812
  while (dir !== dirname7(dir)) {
12351
- const candidate = join25(dir, "node_modules", ".bin", "pm2");
12352
- if (existsSync24(candidate))
12813
+ const candidate = join26(dir, "node_modules", ".bin", "pm2");
12814
+ if (existsSync25(candidate))
12353
12815
  return candidate;
12354
12816
  dir = dirname7(dir);
12355
12817
  }
@@ -12545,13 +13007,13 @@ __export(exports_plan, {
12545
13007
  parsePlanArgs: () => parsePlanArgs
12546
13008
  });
12547
13009
  import {
12548
- existsSync as existsSync25,
13010
+ existsSync as existsSync26,
12549
13011
  mkdirSync as mkdirSync17,
12550
13012
  readdirSync as readdirSync8,
12551
- readFileSync as readFileSync15,
13013
+ readFileSync as readFileSync16,
12552
13014
  writeFileSync as writeFileSync12
12553
13015
  } from "node:fs";
12554
- import { join as join26 } from "node:path";
13016
+ import { join as join27 } from "node:path";
12555
13017
  function printHelp2() {
12556
13018
  process.stderr.write(`
12557
13019
  ${bold2("locus plan")} — AI-powered sprint planning
@@ -12583,11 +13045,11 @@ function normalizeSprintName(name) {
12583
13045
  return name.trim().toLowerCase();
12584
13046
  }
12585
13047
  function getPlansDir(projectRoot) {
12586
- return join26(projectRoot, ".locus", "plans");
13048
+ return join27(projectRoot, ".locus", "plans");
12587
13049
  }
12588
13050
  function ensurePlansDir(projectRoot) {
12589
13051
  const dir = getPlansDir(projectRoot);
12590
- if (!existsSync25(dir)) {
13052
+ if (!existsSync26(dir)) {
12591
13053
  mkdirSync17(dir, { recursive: true });
12592
13054
  }
12593
13055
  return dir;
@@ -12597,14 +13059,14 @@ function generateId() {
12597
13059
  }
12598
13060
  function loadPlanFile(projectRoot, id) {
12599
13061
  const dir = getPlansDir(projectRoot);
12600
- if (!existsSync25(dir))
13062
+ if (!existsSync26(dir))
12601
13063
  return null;
12602
13064
  const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
12603
13065
  const match = files.find((f) => f.startsWith(id));
12604
13066
  if (!match)
12605
13067
  return null;
12606
13068
  try {
12607
- const content = readFileSync15(join26(dir, match), "utf-8");
13069
+ const content = readFileSync16(join27(dir, match), "utf-8");
12608
13070
  return JSON.parse(content);
12609
13071
  } catch {
12610
13072
  return null;
@@ -12671,7 +13133,7 @@ async function planCommand(projectRoot, args, flags = {}) {
12671
13133
  }
12672
13134
  function handleListPlans(projectRoot) {
12673
13135
  const dir = getPlansDir(projectRoot);
12674
- if (!existsSync25(dir)) {
13136
+ if (!existsSync26(dir)) {
12675
13137
  process.stderr.write(`${dim2("No saved plans yet.")}
12676
13138
  `);
12677
13139
  return;
@@ -12689,7 +13151,7 @@ ${bold2("Saved Plans:")}
12689
13151
  for (const file of files) {
12690
13152
  const id = file.replace(".json", "");
12691
13153
  try {
12692
- const content = readFileSync15(join26(dir, file), "utf-8");
13154
+ const content = readFileSync16(join27(dir, file), "utf-8");
12693
13155
  const plan = JSON.parse(content);
12694
13156
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
12695
13157
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -12757,7 +13219,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
12757
13219
  return;
12758
13220
  }
12759
13221
  const config = loadConfig(projectRoot);
12760
- const planPath = join26(getPlansDir(projectRoot), `${plan.id}.json`);
13222
+ const planPath = join27(getPlansDir(projectRoot), `${plan.id}.json`);
12761
13223
  const planPathRelative = `.locus/plans/${plan.id}.json`;
12762
13224
  process.stderr.write(`
12763
13225
  ${bold2("Refining plan:")} ${cyan2(plan.directive)}
@@ -12796,7 +13258,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
12796
13258
  `);
12797
13259
  return;
12798
13260
  }
12799
- if (!existsSync25(planPath)) {
13261
+ if (!existsSync26(planPath)) {
12800
13262
  process.stderr.write(`
12801
13263
  ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
12802
13264
  `);
@@ -12804,7 +13266,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
12804
13266
  }
12805
13267
  let updatedPlan;
12806
13268
  try {
12807
- const content = readFileSync15(planPath, "utf-8");
13269
+ const content = readFileSync16(planPath, "utf-8");
12808
13270
  updatedPlan = JSON.parse(content);
12809
13271
  } catch {
12810
13272
  process.stderr.write(`
@@ -12894,7 +13356,7 @@ ${bold2("Approving plan:")}
12894
13356
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
12895
13357
  const id = generateId();
12896
13358
  const plansDir = ensurePlansDir(projectRoot);
12897
- const planPath = join26(plansDir, `${id}.json`);
13359
+ const planPath = join27(plansDir, `${id}.json`);
12898
13360
  const planPathRelative = `.locus/plans/${id}.json`;
12899
13361
  const displayDirective = directive;
12900
13362
  process.stderr.write(`
@@ -12937,7 +13399,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
12937
13399
  `);
12938
13400
  return;
12939
13401
  }
12940
- if (!existsSync25(planPath)) {
13402
+ if (!existsSync26(planPath)) {
12941
13403
  process.stderr.write(`
12942
13404
  ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
12943
13405
  `);
@@ -12947,7 +13409,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
12947
13409
  }
12948
13410
  let plan;
12949
13411
  try {
12950
- const content = readFileSync15(planPath, "utf-8");
13412
+ const content = readFileSync16(planPath, "utf-8");
12951
13413
  plan = JSON.parse(content);
12952
13414
  } catch {
12953
13415
  process.stderr.write(`
@@ -13109,6 +13571,18 @@ ${bold2("Suggested Order:")}
13109
13571
  `);
13110
13572
  }
13111
13573
  }
13574
+ function loadPastMemory(projectRoot) {
13575
+ const memoryDir = getMemoryDir(projectRoot);
13576
+ if (existsSync26(memoryDir)) {
13577
+ const content = readAllMemorySync(projectRoot);
13578
+ if (content.trim()) {
13579
+ return content.length > MEMORY_MAX_CHARS2 ? `${content.slice(0, MEMORY_MAX_CHARS2)}
13580
+
13581
+ ...(truncated)` : content;
13582
+ }
13583
+ }
13584
+ return "";
13585
+ }
13112
13586
  function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
13113
13587
  const parts = [];
13114
13588
  parts.push(`<role>
@@ -13119,18 +13593,17 @@ ${directive}${sprintName ? `
13119
13593
 
13120
13594
  **Sprint:** ${sprintName}` : ""}
13121
13595
  </directive>`);
13122
- const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
13123
- if (existsSync25(locusPath)) {
13124
- const content = readFileSync15(locusPath, "utf-8");
13596
+ const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13597
+ if (existsSync26(locusPath)) {
13598
+ const content = readFileSync16(locusPath, "utf-8");
13125
13599
  parts.push(`<project-context>
13126
13600
  ${content.slice(0, 3000)}
13127
13601
  </project-context>`);
13128
13602
  }
13129
- const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
13130
- if (existsSync25(learningsPath)) {
13131
- const content = readFileSync15(learningsPath, "utf-8");
13603
+ const memoryContent = loadPastMemory(projectRoot);
13604
+ if (memoryContent) {
13132
13605
  parts.push(`<past-learnings>
13133
- ${content.slice(0, 2000)}
13606
+ ${memoryContent}
13134
13607
  </past-learnings>`);
13135
13608
  }
13136
13609
  parts.push(`<task>
@@ -13180,9 +13653,9 @@ ${JSON.stringify(plan, null, 2)}
13180
13653
  parts.push(`<feedback>
13181
13654
  ${feedback}
13182
13655
  </feedback>`);
13183
- const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
13184
- if (existsSync25(locusPath)) {
13185
- const content = readFileSync15(locusPath, "utf-8");
13656
+ const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13657
+ if (existsSync26(locusPath)) {
13658
+ const content = readFileSync16(locusPath, "utf-8");
13186
13659
  parts.push(`<project-context>
13187
13660
  ${content.slice(0, 3000)}
13188
13661
  </project-context>`);
@@ -13350,10 +13823,12 @@ ${green("✓")} Created ${planned.length} issues.${milestoneTitle ? ` Sprint: ${
13350
13823
 
13351
13824
  `);
13352
13825
  }
13826
+ var MEMORY_MAX_CHARS2 = 2000;
13353
13827
  var init_plan = __esm(() => {
13354
13828
  init_run_ai();
13355
13829
  init_config();
13356
13830
  init_github();
13831
+ init_memory();
13357
13832
  init_sandbox();
13358
13833
  init_terminal();
13359
13834
  });
@@ -13364,8 +13839,8 @@ __export(exports_review, {
13364
13839
  reviewCommand: () => reviewCommand
13365
13840
  });
13366
13841
  import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
13367
- import { existsSync as existsSync26, readFileSync as readFileSync16 } from "node:fs";
13368
- import { join as join27 } from "node:path";
13842
+ import { existsSync as existsSync27, readFileSync as readFileSync17 } from "node:fs";
13843
+ import { join as join28 } from "node:path";
13369
13844
  function printHelp3() {
13370
13845
  process.stderr.write(`
13371
13846
  ${bold2("locus review")} — AI-powered code review
@@ -13534,9 +14009,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
13534
14009
  parts.push(`<role>
13535
14010
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
13536
14011
  </role>`);
13537
- const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13538
- if (existsSync26(locusPath)) {
13539
- const content = readFileSync16(locusPath, "utf-8");
14012
+ const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
14013
+ if (existsSync27(locusPath)) {
14014
+ const content = readFileSync17(locusPath, "utf-8");
13540
14015
  parts.push(`<project-context>
13541
14016
  ${content.slice(0, 2000)}
13542
14017
  </project-context>`);
@@ -13851,14 +14326,14 @@ __export(exports_discuss, {
13851
14326
  discussCommand: () => discussCommand
13852
14327
  });
13853
14328
  import {
13854
- existsSync as existsSync27,
14329
+ existsSync as existsSync28,
13855
14330
  mkdirSync as mkdirSync18,
13856
14331
  readdirSync as readdirSync9,
13857
- readFileSync as readFileSync17,
14332
+ readFileSync as readFileSync18,
13858
14333
  unlinkSync as unlinkSync6,
13859
14334
  writeFileSync as writeFileSync13
13860
14335
  } from "node:fs";
13861
- import { join as join28 } from "node:path";
14336
+ import { join as join29 } from "node:path";
13862
14337
  function printHelp5() {
13863
14338
  process.stderr.write(`
13864
14339
  ${bold2("locus discuss")} — AI-powered architectural discussions
@@ -13880,11 +14355,11 @@ ${bold2("Examples:")}
13880
14355
  `);
13881
14356
  }
13882
14357
  function getDiscussionsDir(projectRoot) {
13883
- return join28(projectRoot, ".locus", "discussions");
14358
+ return join29(projectRoot, ".locus", "discussions");
13884
14359
  }
13885
14360
  function ensureDiscussionsDir(projectRoot) {
13886
14361
  const dir = getDiscussionsDir(projectRoot);
13887
- if (!existsSync27(dir)) {
14362
+ if (!existsSync28(dir)) {
13888
14363
  mkdirSync18(dir, { recursive: true });
13889
14364
  }
13890
14365
  return dir;
@@ -13919,7 +14394,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
13919
14394
  }
13920
14395
  function listDiscussions(projectRoot) {
13921
14396
  const dir = getDiscussionsDir(projectRoot);
13922
- if (!existsSync27(dir)) {
14397
+ if (!existsSync28(dir)) {
13923
14398
  process.stderr.write(`${dim2("No discussions yet.")}
13924
14399
  `);
13925
14400
  return;
@@ -13936,7 +14411,7 @@ ${bold2("Discussions:")}
13936
14411
  `);
13937
14412
  for (const file of files) {
13938
14413
  const id = file.replace(".md", "");
13939
- const content = readFileSync17(join28(dir, file), "utf-8");
14414
+ const content = readFileSync18(join29(dir, file), "utf-8");
13940
14415
  const titleMatch = content.match(/^#\s+(.+)/m);
13941
14416
  const title = titleMatch ? titleMatch[1] : id;
13942
14417
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -13954,7 +14429,7 @@ function showDiscussion(projectRoot, id) {
13954
14429
  return;
13955
14430
  }
13956
14431
  const dir = getDiscussionsDir(projectRoot);
13957
- if (!existsSync27(dir)) {
14432
+ if (!existsSync28(dir)) {
13958
14433
  process.stderr.write(`${red2("✗")} No discussions found.
13959
14434
  `);
13960
14435
  return;
@@ -13966,7 +14441,7 @@ function showDiscussion(projectRoot, id) {
13966
14441
  `);
13967
14442
  return;
13968
14443
  }
13969
- const content = readFileSync17(join28(dir, match), "utf-8");
14444
+ const content = readFileSync18(join29(dir, match), "utf-8");
13970
14445
  process.stdout.write(`${content}
13971
14446
  `);
13972
14447
  }
@@ -13977,7 +14452,7 @@ function deleteDiscussion(projectRoot, id) {
13977
14452
  return;
13978
14453
  }
13979
14454
  const dir = getDiscussionsDir(projectRoot);
13980
- if (!existsSync27(dir)) {
14455
+ if (!existsSync28(dir)) {
13981
14456
  process.stderr.write(`${red2("✗")} No discussions found.
13982
14457
  `);
13983
14458
  return;
@@ -13989,7 +14464,7 @@ function deleteDiscussion(projectRoot, id) {
13989
14464
  `);
13990
14465
  return;
13991
14466
  }
13992
- unlinkSync6(join28(dir, match));
14467
+ unlinkSync6(join29(dir, match));
13993
14468
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
13994
14469
  `);
13995
14470
  }
@@ -14002,7 +14477,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
14002
14477
  return;
14003
14478
  }
14004
14479
  const dir = getDiscussionsDir(projectRoot);
14005
- if (!existsSync27(dir)) {
14480
+ if (!existsSync28(dir)) {
14006
14481
  process.stderr.write(`${red2("✗")} No discussions found.
14007
14482
  `);
14008
14483
  return;
@@ -14014,7 +14489,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
14014
14489
  `);
14015
14490
  return;
14016
14491
  }
14017
- const content = readFileSync17(join28(dir, match), "utf-8");
14492
+ const content = readFileSync18(join29(dir, match), "utf-8");
14018
14493
  const titleMatch = content.match(/^#\s+(.+)/m);
14019
14494
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
14020
14495
  await planCommand(projectRoot, [
@@ -14140,7 +14615,7 @@ ${turn.content}`;
14140
14615
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
14141
14616
  ].join(`
14142
14617
  `);
14143
- writeFileSync13(join28(dir, `${id}.md`), markdown, "utf-8");
14618
+ writeFileSync13(join29(dir, `${id}.md`), markdown, "utf-8");
14144
14619
  process.stderr.write(`
14145
14620
  ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
14146
14621
  `);
@@ -14150,23 +14625,34 @@ ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
14150
14625
 
14151
14626
  `);
14152
14627
  }
14628
+ function loadPastMemory2(projectRoot) {
14629
+ const memoryDir = getMemoryDir(projectRoot);
14630
+ if (existsSync28(memoryDir)) {
14631
+ const content = readAllMemorySync(projectRoot);
14632
+ if (content.trim()) {
14633
+ return content.length > MEMORY_MAX_CHARS3 ? `${content.slice(0, MEMORY_MAX_CHARS3)}
14634
+
14635
+ ...(truncated)` : content;
14636
+ }
14637
+ }
14638
+ return "";
14639
+ }
14153
14640
  function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
14154
14641
  const parts = [];
14155
14642
  parts.push(`<role>
14156
14643
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
14157
14644
  </role>`);
14158
- const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
14159
- if (existsSync27(locusPath)) {
14160
- const content = readFileSync17(locusPath, "utf-8");
14645
+ const locusPath = join29(projectRoot, ".locus", "LOCUS.md");
14646
+ if (existsSync28(locusPath)) {
14647
+ const content = readFileSync18(locusPath, "utf-8");
14161
14648
  parts.push(`<project-context>
14162
14649
  ${content.slice(0, 3000)}
14163
14650
  </project-context>`);
14164
14651
  }
14165
- const learningsPath = join28(projectRoot, ".locus", "LEARNINGS.md");
14166
- if (existsSync27(learningsPath)) {
14167
- const content = readFileSync17(learningsPath, "utf-8");
14652
+ const memoryContent = loadPastMemory2(projectRoot);
14653
+ if (memoryContent) {
14168
14654
  parts.push(`<past-learnings>
14169
- ${content.slice(0, 2000)}
14655
+ ${memoryContent}
14170
14656
  </past-learnings>`);
14171
14657
  }
14172
14658
  parts.push(`<discussion-topic>
@@ -14215,10 +14701,11 @@ If you still need key information to give a good answer:
14215
14701
 
14216
14702
  `);
14217
14703
  }
14218
- var MAX_DISCUSSION_ROUNDS = 5;
14704
+ var MAX_DISCUSSION_ROUNDS = 5, MEMORY_MAX_CHARS3 = 2000;
14219
14705
  var init_discuss = __esm(() => {
14220
14706
  init_run_ai();
14221
14707
  init_config();
14708
+ init_memory();
14222
14709
  init_sandbox();
14223
14710
  init_progress();
14224
14711
  init_terminal();
@@ -14235,8 +14722,8 @@ __export(exports_artifacts, {
14235
14722
  formatDate: () => formatDate2,
14236
14723
  artifactsCommand: () => artifactsCommand
14237
14724
  });
14238
- import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync18, statSync as statSync5 } from "node:fs";
14239
- import { join as join29 } from "node:path";
14725
+ import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync19, statSync as statSync5 } from "node:fs";
14726
+ import { join as join30 } from "node:path";
14240
14727
  function printHelp6() {
14241
14728
  process.stderr.write(`
14242
14729
  ${bold2("locus artifacts")} — View and manage AI-generated artifacts
@@ -14256,37 +14743,37 @@ ${dim2("Artifact names support partial matching.")}
14256
14743
  `);
14257
14744
  }
14258
14745
  function getArtifactsDir(projectRoot) {
14259
- return join29(projectRoot, ".locus", "artifacts");
14746
+ return join30(projectRoot, ".locus", "artifacts");
14260
14747
  }
14261
14748
  function listArtifacts(projectRoot) {
14262
14749
  const dir = getArtifactsDir(projectRoot);
14263
- if (!existsSync28(dir))
14750
+ if (!existsSync29(dir))
14264
14751
  return [];
14265
14752
  return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
14266
- const filePath = join29(dir, fileName);
14267
- const stat = statSync5(filePath);
14753
+ const filePath = join30(dir, fileName);
14754
+ const stat2 = statSync5(filePath);
14268
14755
  return {
14269
14756
  name: fileName.replace(/\.md$/, ""),
14270
14757
  fileName,
14271
- createdAt: stat.birthtime,
14272
- size: stat.size
14758
+ createdAt: stat2.birthtime,
14759
+ size: stat2.size
14273
14760
  };
14274
14761
  }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
14275
14762
  }
14276
14763
  function readArtifact(projectRoot, name) {
14277
14764
  const dir = getArtifactsDir(projectRoot);
14278
14765
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
14279
- const filePath = join29(dir, fileName);
14280
- if (!existsSync28(filePath))
14766
+ const filePath = join30(dir, fileName);
14767
+ if (!existsSync29(filePath))
14281
14768
  return null;
14282
- const stat = statSync5(filePath);
14769
+ const stat2 = statSync5(filePath);
14283
14770
  return {
14284
- content: readFileSync18(filePath, "utf-8"),
14771
+ content: readFileSync19(filePath, "utf-8"),
14285
14772
  info: {
14286
14773
  name: fileName.replace(/\.md$/, ""),
14287
14774
  fileName,
14288
- createdAt: stat.birthtime,
14289
- size: stat.size
14775
+ createdAt: stat2.birthtime,
14776
+ size: stat2.size
14290
14777
  }
14291
14778
  };
14292
14779
  }
@@ -14581,7 +15068,7 @@ Co-Authored-By: LocusAgent <agent@locusai.team>`;
14581
15068
  `);
14582
15069
  }
14583
15070
  }
14584
- function buildCommitPrompt(diff, stat, recentCommits) {
15071
+ function buildCommitPrompt(diff, stat2, recentCommits) {
14585
15072
  const maxDiffLength = 15000;
14586
15073
  const truncatedDiff = diff.length > maxDiffLength ? `${diff.slice(0, maxDiffLength)}
14587
15074
 
@@ -14604,7 +15091,7 @@ ${recentCommits}
14604
15091
  `;
14605
15092
  }
14606
15093
  prompt += `File summary:
14607
- ${stat}
15094
+ ${stat2}
14608
15095
 
14609
15096
  Diff:
14610
15097
  ${truncatedDiff}`;
@@ -14638,10 +15125,10 @@ __export(exports_sandbox2, {
14638
15125
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
14639
15126
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
14640
15127
  });
14641
- import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
15128
+ import { execSync as execSync22, spawn as spawn8 } from "node:child_process";
14642
15129
  import { createHash as createHash2 } from "node:crypto";
14643
- import { existsSync as existsSync29, readFileSync as readFileSync19 } from "node:fs";
14644
- import { basename as basename4, join as join30 } from "node:path";
15130
+ import { existsSync as existsSync30, readFileSync as readFileSync20 } from "node:fs";
15131
+ import { basename as basename4, join as join31 } from "node:path";
14645
15132
  import { createInterface as createInterface3 } from "node:readline";
14646
15133
  function printSandboxHelp() {
14647
15134
  process.stderr.write(`
@@ -14830,7 +15317,7 @@ async function handleAgentLogin(projectRoot, agent) {
14830
15317
  process.stderr.write(`${dim2("Login and then exit when ready.")}
14831
15318
 
14832
15319
  `);
14833
- const child = spawn7("docker", [
15320
+ const child = spawn8("docker", [
14834
15321
  "sandbox",
14835
15322
  "exec",
14836
15323
  "--privileged",
@@ -15171,7 +15658,7 @@ async function handleLogs(projectRoot, args) {
15171
15658
  }
15172
15659
  function detectPackageManager2(projectRoot) {
15173
15660
  try {
15174
- const raw = readFileSync19(join30(projectRoot, "package.json"), "utf-8");
15661
+ const raw = readFileSync20(join31(projectRoot, "package.json"), "utf-8");
15175
15662
  const pkgJson = JSON.parse(raw);
15176
15663
  if (typeof pkgJson.packageManager === "string") {
15177
15664
  const name = pkgJson.packageManager.split("@")[0];
@@ -15180,13 +15667,13 @@ function detectPackageManager2(projectRoot) {
15180
15667
  }
15181
15668
  }
15182
15669
  } catch {}
15183
- if (existsSync29(join30(projectRoot, "bun.lock")) || existsSync29(join30(projectRoot, "bun.lockb"))) {
15670
+ if (existsSync30(join31(projectRoot, "bun.lock")) || existsSync30(join31(projectRoot, "bun.lockb"))) {
15184
15671
  return "bun";
15185
15672
  }
15186
- if (existsSync29(join30(projectRoot, "yarn.lock"))) {
15673
+ if (existsSync30(join31(projectRoot, "yarn.lock"))) {
15187
15674
  return "yarn";
15188
15675
  }
15189
- if (existsSync29(join30(projectRoot, "pnpm-lock.yaml"))) {
15676
+ if (existsSync30(join31(projectRoot, "pnpm-lock.yaml"))) {
15190
15677
  return "pnpm";
15191
15678
  }
15192
15679
  return "npm";
@@ -15289,9 +15776,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
15289
15776
  ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
15290
15777
  `);
15291
15778
  }
15292
- const setupScript = join30(projectRoot, ".locus", "sandbox-setup.sh");
15293
- const containerSetupScript = containerWorkdir ? join30(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
15294
- if (existsSync29(setupScript)) {
15779
+ const setupScript = join31(projectRoot, ".locus", "sandbox-setup.sh");
15780
+ const containerSetupScript = containerWorkdir ? join31(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
15781
+ if (existsSync30(setupScript)) {
15295
15782
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
15296
15783
  `);
15297
15784
  const hookOk = await runInteractiveCommand("docker", [
@@ -15373,7 +15860,7 @@ function getActiveProviderSandbox(projectRoot, provider) {
15373
15860
  }
15374
15861
  function runInteractiveCommand(command, args) {
15375
15862
  return new Promise((resolve2) => {
15376
- const child = spawn7(command, args, { stdio: "inherit" });
15863
+ const child = spawn8(command, args, { stdio: "inherit" });
15377
15864
  child.on("close", (code) => resolve2(code === 0));
15378
15865
  child.on("error", () => resolve2(false));
15379
15866
  });
@@ -15458,23 +15945,295 @@ var init_sandbox2 = __esm(() => {
15458
15945
  PROVIDERS = ["claude", "codex"];
15459
15946
  });
15460
15947
 
15948
+ // src/commands/memory.ts
15949
+ var exports_memory = {};
15950
+ __export(exports_memory, {
15951
+ memoryCommand: () => memoryCommand
15952
+ });
15953
+ import { existsSync as existsSync31 } from "node:fs";
15954
+ import { writeFile as writeFile2 } from "node:fs/promises";
15955
+ import { join as join32 } from "node:path";
15956
+ import { createInterface as createInterface4 } from "node:readline";
15957
+ function printHelp7() {
15958
+ process.stderr.write(`
15959
+ ${bold2("locus memory")} — Inspect, search, and manage structured memory
15960
+
15961
+ ${bold2("Usage:")}
15962
+ locus memory list [--category <name>] List all memory entries
15963
+ locus memory search <query> Search entries by keyword
15964
+ locus memory stats Show per-category statistics
15965
+ locus memory reset [--confirm] Clear all entries (preserve headers)
15966
+ locus memory migrate Migrate legacy LEARNINGS.md → memory/
15967
+
15968
+ ${bold2("Categories:")}
15969
+ architecture, conventions, decisions, preferences, debugging
15970
+
15971
+ ${bold2("Examples:")}
15972
+ locus memory list ${dim2("# Show all entries")}
15973
+ locus memory list --category debugging ${dim2("# Show only debugging entries")}
15974
+ locus memory search "sandbox" ${dim2("# Search for 'sandbox'")}
15975
+ locus memory stats ${dim2("# Show entry counts and sizes")}
15976
+ locus memory reset --confirm ${dim2("# Clear all entries")}
15977
+ locus memory migrate ${dim2("# Migrate legacy LEARNINGS.md")}
15978
+ `);
15979
+ }
15980
+ async function memoryCommand(projectRoot, args) {
15981
+ const subcommand = args[0];
15982
+ if (!subcommand || subcommand === "help") {
15983
+ printHelp7();
15984
+ return;
15985
+ }
15986
+ const memoryDir = getMemoryDir(projectRoot);
15987
+ if (!existsSync31(memoryDir) && subcommand !== "migrate") {
15988
+ process.stderr.write(`${red2("✗")} Memory directory not found at ${dim2(".locus/memory/")}
15989
+ `);
15990
+ process.stderr.write(` Run ${bold2("locus init")} to create the memory directory.
15991
+ `);
15992
+ process.exit(1);
15993
+ }
15994
+ switch (subcommand) {
15995
+ case "list":
15996
+ await handleList(projectRoot, args.slice(1));
15997
+ break;
15998
+ case "search":
15999
+ await handleSearch(projectRoot, args.slice(1));
16000
+ break;
16001
+ case "stats":
16002
+ await handleStats(projectRoot);
16003
+ break;
16004
+ case "reset":
16005
+ await handleReset(projectRoot, args.slice(1));
16006
+ break;
16007
+ case "migrate":
16008
+ await handleMigrate(projectRoot);
16009
+ break;
16010
+ default:
16011
+ process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
16012
+ `);
16013
+ process.stderr.write(` Run ${bold2("locus memory help")} for available subcommands.
16014
+ `);
16015
+ process.exit(1);
16016
+ }
16017
+ }
16018
+ async function handleList(projectRoot, args) {
16019
+ let categoryFilter;
16020
+ const catIdx = args.indexOf("--category");
16021
+ if (catIdx !== -1) {
16022
+ categoryFilter = args[catIdx + 1];
16023
+ if (!categoryFilter || !MEMORY_CATEGORIES[categoryFilter]) {
16024
+ const valid = Object.keys(MEMORY_CATEGORIES).join(", ");
16025
+ process.stderr.write(`${red2("✗")} Invalid category. Valid categories: ${valid}
16026
+ `);
16027
+ process.exit(1);
16028
+ }
16029
+ }
16030
+ const categories = categoryFilter ? [categoryFilter] : Object.keys(MEMORY_CATEGORIES);
16031
+ let totalEntries = 0;
16032
+ for (const category of categories) {
16033
+ const content = await readMemoryFile(projectRoot, category);
16034
+ const entries = parseEntries(content);
16035
+ if (entries.length === 0)
16036
+ continue;
16037
+ const meta = MEMORY_CATEGORIES[category];
16038
+ process.stderr.write(`
16039
+ ${bold2(cyan2(meta.title))} ${dim2(`(${meta.description})`)}
16040
+ `);
16041
+ for (const entry of entries) {
16042
+ process.stderr.write(` ${entry}
16043
+ `);
16044
+ }
16045
+ totalEntries += entries.length;
16046
+ }
16047
+ if (totalEntries === 0) {
16048
+ process.stderr.write(`
16049
+ ${dim2("No memory entries found.")}
16050
+ `);
16051
+ } else {
16052
+ process.stderr.write(`
16053
+ ${dim2(`${totalEntries} entries total`)}
16054
+ `);
16055
+ }
16056
+ }
16057
+ async function handleSearch(projectRoot, args) {
16058
+ const query = args.join(" ").trim();
16059
+ if (!query) {
16060
+ process.stderr.write(`${red2("✗")} Usage: locus memory search <query>
16061
+ `);
16062
+ process.exit(1);
16063
+ }
16064
+ const queryLower = query.toLowerCase();
16065
+ let matchCount = 0;
16066
+ for (const [category, meta] of Object.entries(MEMORY_CATEGORIES)) {
16067
+ const content = await readMemoryFile(projectRoot, category);
16068
+ const entries = parseEntries(content);
16069
+ const matches = entries.filter((entry) => entry.toLowerCase().includes(queryLower));
16070
+ if (matches.length === 0)
16071
+ continue;
16072
+ process.stderr.write(`
16073
+ ${bold2(cyan2(meta.title))}
16074
+ `);
16075
+ for (const entry of matches) {
16076
+ const highlighted = highlightMatch(entry, query);
16077
+ process.stderr.write(` ${highlighted}
16078
+ `);
16079
+ }
16080
+ matchCount += matches.length;
16081
+ }
16082
+ if (matchCount === 0) {
16083
+ process.stderr.write(`
16084
+ ${dim2("No matches found for")} "${query}"
16085
+ `);
16086
+ } else {
16087
+ process.stderr.write(`
16088
+ ${green(`${matchCount} match${matchCount === 1 ? "" : "es"}`)} found for "${query}"
16089
+ `);
16090
+ }
16091
+ }
16092
+ async function handleStats(projectRoot) {
16093
+ const stats = await getMemoryStats(projectRoot);
16094
+ process.stderr.write(`
16095
+ ${bold2("Memory Statistics")}
16096
+
16097
+ `);
16098
+ let totalEntries = 0;
16099
+ let totalSize = 0;
16100
+ for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
16101
+ const s = stats[key];
16102
+ const countStr = String(s.count).padStart(3);
16103
+ const sizeStr = formatSize2(s.size).padStart(8);
16104
+ const dateStr = s.size > 0 ? dim2(s.lastModified.toLocaleDateString("en-US", {
16105
+ month: "short",
16106
+ day: "numeric",
16107
+ year: "numeric"
16108
+ })) : dim2("—");
16109
+ const icon = s.count > 0 ? green("●") : dim2("○");
16110
+ process.stderr.write(` ${icon} ${cyan2(meta.title.padEnd(14))} ${countStr} entries ${sizeStr} ${dateStr}
16111
+ `);
16112
+ totalEntries += s.count;
16113
+ totalSize += s.size;
16114
+ }
16115
+ process.stderr.write(`
16116
+ ${bold2("Total:")} ${totalEntries} entries, ${formatSize2(totalSize)}
16117
+ `);
16118
+ process.stderr.write(`
16119
+ `);
16120
+ }
16121
+ async function handleReset(projectRoot, args) {
16122
+ const confirmed = args.includes("--confirm");
16123
+ if (!confirmed) {
16124
+ const rl = createInterface4({
16125
+ input: process.stdin,
16126
+ output: process.stderr
16127
+ });
16128
+ const answer = await new Promise((resolve2) => {
16129
+ rl.question(`${yellow2("⚠")} This will clear all memory entries. Continue? (y/N) `, (ans) => {
16130
+ rl.close();
16131
+ resolve2(ans.trim().toLowerCase());
16132
+ });
16133
+ });
16134
+ if (answer !== "y" && answer !== "yes") {
16135
+ process.stderr.write(`${dim2("Cancelled.")}
16136
+ `);
16137
+ return;
16138
+ }
16139
+ }
16140
+ const stats = await getMemoryStats(projectRoot);
16141
+ const memoryDir = getMemoryDir(projectRoot);
16142
+ let totalCleared = 0;
16143
+ for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
16144
+ const count = stats[key].count;
16145
+ const filePath = join32(memoryDir, meta.file);
16146
+ const header = `# ${meta.title}
16147
+
16148
+ ${meta.description}
16149
+
16150
+ `;
16151
+ await writeFile2(filePath, header, "utf-8");
16152
+ if (count > 0) {
16153
+ process.stderr.write(` ${dim2("○")} ${meta.title}: cleared ${count} entries
16154
+ `);
16155
+ totalCleared += count;
16156
+ }
16157
+ }
16158
+ if (totalCleared > 0) {
16159
+ process.stderr.write(`
16160
+ ${green("✓")} Cleared ${totalCleared} entries (file headers preserved)
16161
+ `);
16162
+ } else {
16163
+ process.stderr.write(`${dim2("No entries to clear.")}
16164
+ `);
16165
+ }
16166
+ }
16167
+ async function handleMigrate(projectRoot) {
16168
+ const learningsPath = join32(projectRoot, ".locus", "LEARNINGS.md");
16169
+ if (!existsSync31(learningsPath)) {
16170
+ process.stderr.write(`${dim2("○")} No LEARNINGS.md found — nothing to migrate.
16171
+ `);
16172
+ return;
16173
+ }
16174
+ process.stderr.write(`Migrating entries from LEARNINGS.md...
16175
+ `);
16176
+ const result = await migrateFromLearnings(projectRoot);
16177
+ if (result.migrated > 0) {
16178
+ process.stderr.write(`${green("✓")} Migrated ${result.migrated} entries to .locus/memory/
16179
+ `);
16180
+ }
16181
+ if (result.skipped > 0) {
16182
+ process.stderr.write(`${dim2("○")} Skipped ${result.skipped} duplicate entries
16183
+ `);
16184
+ }
16185
+ if (result.migrated > 0 || result.skipped > 0) {
16186
+ process.stderr.write(`${green("✓")} Removed legacy LEARNINGS.md
16187
+ `);
16188
+ }
16189
+ if (result.migrated === 0 && result.skipped === 0) {
16190
+ process.stderr.write(`${dim2("○")} No entries found in LEARNINGS.md to migrate.
16191
+ `);
16192
+ }
16193
+ }
16194
+ function parseEntries(content) {
16195
+ if (!content)
16196
+ return [];
16197
+ return content.split(`
16198
+ `).filter((line) => line.startsWith("- "));
16199
+ }
16200
+ function highlightMatch(text, query) {
16201
+ const regex = new RegExp(`(${escapeRegex(query)})`, "gi");
16202
+ return text.replace(regex, (match) => bold2(match));
16203
+ }
16204
+ function escapeRegex(str) {
16205
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16206
+ }
16207
+ function formatSize2(bytes) {
16208
+ if (bytes === 0)
16209
+ return "0 B";
16210
+ if (bytes < 1024)
16211
+ return `${bytes} B`;
16212
+ const kb = bytes / 1024;
16213
+ return `${kb.toFixed(1)} KB`;
16214
+ }
16215
+ var init_memory2 = __esm(() => {
16216
+ init_memory();
16217
+ init_terminal();
16218
+ });
16219
+
15461
16220
  // src/cli.ts
15462
16221
  init_config();
15463
16222
  init_context();
15464
16223
  init_logger();
15465
16224
  init_rate_limiter();
15466
16225
  init_terminal();
15467
- import { existsSync as existsSync30, readFileSync as readFileSync20 } from "node:fs";
15468
- import { join as join31 } from "node:path";
16226
+ import { existsSync as existsSync32, readFileSync as readFileSync21 } from "node:fs";
16227
+ import { join as join33 } from "node:path";
15469
16228
  import { fileURLToPath } from "node:url";
15470
16229
  function getCliVersion() {
15471
16230
  const fallbackVersion = "0.0.0";
15472
- const packageJsonPath = join31(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
15473
- if (!existsSync30(packageJsonPath)) {
16231
+ const packageJsonPath = join33(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
16232
+ if (!existsSync32(packageJsonPath)) {
15474
16233
  return fallbackVersion;
15475
16234
  }
15476
16235
  try {
15477
- const parsed = JSON.parse(readFileSync20(packageJsonPath, "utf-8"));
16236
+ const parsed = JSON.parse(readFileSync21(packageJsonPath, "utf-8"));
15478
16237
  return parsed.version ?? fallbackVersion;
15479
16238
  } catch {
15480
16239
  return fallbackVersion;
@@ -15636,7 +16395,7 @@ function printLogo() {
15636
16395
  `);
15637
16396
  }
15638
16397
  }
15639
- function printHelp7() {
16398
+ function printHelp8() {
15640
16399
  printLogo();
15641
16400
  process.stderr.write(`
15642
16401
 
@@ -15664,6 +16423,7 @@ ${bold2("Commands:")}
15664
16423
  ${cyan2("packages")} Manage installed packages (list, outdated)
15665
16424
  ${cyan2("pkg")} ${dim2("<name> [cmd]")} Run a command from an installed package
15666
16425
  ${cyan2("skills")} Discover and manage agent skills
16426
+ ${cyan2("memory")} Inspect, search, and manage memory
15667
16427
  ${cyan2("sandbox")} Manage Docker sandbox lifecycle
15668
16428
  ${cyan2("upgrade")} Check for and install updates
15669
16429
 
@@ -15746,7 +16506,7 @@ async function main() {
15746
16506
  process.exit(0);
15747
16507
  }
15748
16508
  if (parsed.flags.help && !parsed.command) {
15749
- printHelp7();
16509
+ printHelp8();
15750
16510
  process.exit(0);
15751
16511
  }
15752
16512
  const command = resolveAlias(parsed.command);
@@ -15757,7 +16517,7 @@ async function main() {
15757
16517
  try {
15758
16518
  const root = getGitRoot(cwd);
15759
16519
  if (isInitialized(root)) {
15760
- logDir = join31(root, ".locus", "logs");
16520
+ logDir = join33(root, ".locus", "logs");
15761
16521
  getRateLimiter(root);
15762
16522
  }
15763
16523
  } catch {}
@@ -15778,7 +16538,7 @@ async function main() {
15778
16538
  printVersionNotice = startVersionCheck2(VERSION);
15779
16539
  }
15780
16540
  if (!command) {
15781
- printHelp7();
16541
+ printHelp8();
15782
16542
  process.exit(0);
15783
16543
  }
15784
16544
  if (command === "init") {
@@ -15974,6 +16734,12 @@ async function main() {
15974
16734
  await sandboxCommand2(projectRoot, sandboxArgs);
15975
16735
  break;
15976
16736
  }
16737
+ case "memory": {
16738
+ const { memoryCommand: memoryCommand2 } = await Promise.resolve().then(() => (init_memory2(), exports_memory));
16739
+ const memoryArgs = parsed.flags.help ? ["help"] : parsed.args;
16740
+ await memoryCommand2(projectRoot, memoryArgs);
16741
+ break;
16742
+ }
15977
16743
  case "upgrade": {
15978
16744
  const { upgradeCommand: upgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade(), exports_upgrade));
15979
16745
  await upgradeCommand2(projectRoot, parsed.args, {