@locusai/cli 0.25.6 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/locus.js +1341 -375
  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(() => {
@@ -3880,6 +4101,80 @@ ${bold2("Skill Information")}
3880
4101
  process.stderr.write(`
3881
4102
  `);
3882
4103
  }
4104
+ async function searchSkills(query, flags) {
4105
+ if (!query && !flags.tag) {
4106
+ process.stderr.write(`${red2("✗")} Please provide a search query.
4107
+ `);
4108
+ process.stderr.write(` Usage: ${bold2("locus skills search <query>")} or ${bold2("locus skills search --tag <tag>")}
4109
+ `);
4110
+ process.exit(1);
4111
+ }
4112
+ let registry;
4113
+ try {
4114
+ registry = await fetchRegistry();
4115
+ } catch (err) {
4116
+ process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
4117
+ `);
4118
+ process.stderr.write(` ${dim2(err.message)}
4119
+ `);
4120
+ process.exit(1);
4121
+ }
4122
+ const tagFilter = flags.tag?.toLowerCase();
4123
+ const queryLower = query?.toLowerCase() ?? "";
4124
+ const matches = registry.skills.filter((s) => {
4125
+ if (tagFilter) {
4126
+ return s.tags.some((t) => t.toLowerCase() === tagFilter);
4127
+ }
4128
+ const nameMatch = s.name.toLowerCase().includes(queryLower);
4129
+ const descMatch = s.description.toLowerCase().includes(queryLower);
4130
+ const tagMatch = s.tags.some((t) => t.toLowerCase().includes(queryLower));
4131
+ return nameMatch || descMatch || tagMatch;
4132
+ });
4133
+ if (matches.length === 0) {
4134
+ process.stderr.write(`
4135
+ ${yellow2("⚠")} No skills found matching "${bold2(tagFilter || query)}"
4136
+ `);
4137
+ process.stderr.write(` Run ${bold2("locus skills list")} to see all available skills.
4138
+
4139
+ `);
4140
+ return;
4141
+ }
4142
+ const cwd = process.cwd();
4143
+ const lockFile = readLockFile(cwd);
4144
+ process.stderr.write(`
4145
+ ${bold2("Search Results")} for "${cyan2(tagFilter || query)}"
4146
+
4147
+ `);
4148
+ const columns = [
4149
+ { key: "name", header: "Name", minWidth: 12, maxWidth: 24 },
4150
+ { key: "description", header: "Description", minWidth: 20, maxWidth: 44 },
4151
+ {
4152
+ key: "tags",
4153
+ header: "Tags",
4154
+ minWidth: 10,
4155
+ maxWidth: 30,
4156
+ format: (val) => dim2(val.join(", "))
4157
+ },
4158
+ {
4159
+ key: "status",
4160
+ header: "Status",
4161
+ minWidth: 8,
4162
+ maxWidth: 12
4163
+ }
4164
+ ];
4165
+ const rows = matches.map((s) => ({
4166
+ name: cyan2(s.name),
4167
+ description: s.description,
4168
+ tags: s.tags,
4169
+ status: s.name in lockFile.skills ? green("installed") : dim2("available")
4170
+ }));
4171
+ process.stderr.write(`${renderTable(columns, rows)}
4172
+
4173
+ `);
4174
+ process.stderr.write(` ${dim2(`${matches.length} skill(s) found.`)} Install with: ${bold2("locus skills install <name>")}
4175
+
4176
+ `);
4177
+ }
3883
4178
  function printSkillsHelp() {
3884
4179
  process.stderr.write(`
3885
4180
  ${bold2("Usage:")}
@@ -3888,6 +4183,7 @@ ${bold2("Usage:")}
3888
4183
  ${bold2("Subcommands:")}
3889
4184
  ${cyan2("list")} List available skills from the registry
3890
4185
  ${cyan2("list")} ${dim2("--installed")} List locally installed skills
4186
+ ${cyan2("search")} ${dim2("<query>")} Search skills by name, description, or tags
3891
4187
  ${cyan2("install")} ${dim2("<name>")} Install a skill from the registry
3892
4188
  ${cyan2("remove")} ${dim2("<name>")} Remove an installed skill (alias: ${cyan2("uninstall")})
3893
4189
  ${cyan2("update")} ${dim2("[name]")} Update installed skill(s) from registry
@@ -3896,6 +4192,8 @@ ${bold2("Subcommands:")}
3896
4192
  ${bold2("Examples:")}
3897
4193
  locus skills list ${dim2("# Browse available skills")}
3898
4194
  locus skills list --installed ${dim2("# Show installed skills")}
4195
+ locus skills search "code review" ${dim2("# Search by keyword")}
4196
+ locus skills search --tag testing ${dim2("# Search by tag")}
3899
4197
  locus skills install code-review ${dim2("# Install a skill")}
3900
4198
  locus skills remove code-review ${dim2("# Remove a skill")}
3901
4199
  locus skills update ${dim2("# Update all installed skills")}
@@ -3940,10 +4238,15 @@ async function skillsCommand(args, flags) {
3940
4238
  await infoSkill(skillName);
3941
4239
  break;
3942
4240
  }
4241
+ case "search": {
4242
+ const searchQuery = args.slice(1).join(" ");
4243
+ await searchSkills(searchQuery, flags);
4244
+ break;
4245
+ }
3943
4246
  default:
3944
4247
  process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
3945
4248
  `);
3946
- process.stderr.write(` Available: ${bold2("list")}, ${bold2("install")}, ${bold2("remove")} (${bold2("uninstall")}), ${bold2("update")}, ${bold2("info")}
4249
+ process.stderr.write(` Available: ${bold2("list")}, ${bold2("search")}, ${bold2("install")}, ${bold2("remove")} (${bold2("uninstall")}), ${bold2("update")}, ${bold2("info")}
3947
4250
  `);
3948
4251
  process.exit(1);
3949
4252
  }
@@ -4092,16 +4395,16 @@ __export(exports_logs, {
4092
4395
  logsCommand: () => logsCommand
4093
4396
  });
4094
4397
  import {
4095
- existsSync as existsSync12,
4398
+ existsSync as existsSync13,
4096
4399
  readdirSync as readdirSync2,
4097
- readFileSync as readFileSync9,
4400
+ readFileSync as readFileSync10,
4098
4401
  statSync as statSync2,
4099
4402
  unlinkSync as unlinkSync2
4100
4403
  } from "node:fs";
4101
- import { join as join12 } from "node:path";
4404
+ import { join as join13 } from "node:path";
4102
4405
  async function logsCommand(cwd, options) {
4103
- const logsDir = join12(cwd, ".locus", "logs");
4104
- if (!existsSync12(logsDir)) {
4406
+ const logsDir = join13(cwd, ".locus", "logs");
4407
+ if (!existsSync13(logsDir)) {
4105
4408
  process.stderr.write(`${dim2("No logs found.")}
4106
4409
  `);
4107
4410
  return;
@@ -4121,7 +4424,7 @@ async function logsCommand(cwd, options) {
4121
4424
  return viewLog(logFiles[0], options.level, options.lines ?? 50);
4122
4425
  }
4123
4426
  function viewLog(logFile, levelFilter, maxLines) {
4124
- const content = readFileSync9(logFile, "utf-8");
4427
+ const content = readFileSync10(logFile, "utf-8");
4125
4428
  const lines = content.trim().split(`
4126
4429
  `).filter(Boolean);
4127
4430
  process.stderr.write(`
@@ -4156,9 +4459,9 @@ async function tailLog(logFile, levelFilter) {
4156
4459
  process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
4157
4460
 
4158
4461
  `);
4159
- let lastSize = existsSync12(logFile) ? statSync2(logFile).size : 0;
4160
- if (existsSync12(logFile)) {
4161
- const content = readFileSync9(logFile, "utf-8");
4462
+ let lastSize = existsSync13(logFile) ? statSync2(logFile).size : 0;
4463
+ if (existsSync13(logFile)) {
4464
+ const content = readFileSync10(logFile, "utf-8");
4162
4465
  const lines = content.trim().split(`
4163
4466
  `).filter(Boolean);
4164
4467
  const recent = lines.slice(-10);
@@ -4176,12 +4479,12 @@ async function tailLog(logFile, levelFilter) {
4176
4479
  }
4177
4480
  return new Promise((resolve) => {
4178
4481
  const interval = setInterval(() => {
4179
- if (!existsSync12(logFile))
4482
+ if (!existsSync13(logFile))
4180
4483
  return;
4181
4484
  const currentSize = statSync2(logFile).size;
4182
4485
  if (currentSize <= lastSize)
4183
4486
  return;
4184
- const content = readFileSync9(logFile, "utf-8");
4487
+ const content = readFileSync10(logFile, "utf-8");
4185
4488
  const allLines = content.trim().split(`
4186
4489
  `).filter(Boolean);
4187
4490
  const oldContent = content.slice(0, lastSize);
@@ -4236,7 +4539,7 @@ function cleanLogs(logsDir) {
4236
4539
  `);
4237
4540
  }
4238
4541
  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);
4542
+ 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
4543
  }
4241
4544
  function formatEntry(entry) {
4242
4545
  const time = dim2(new Date(entry.ts).toLocaleTimeString());
@@ -4546,9 +4849,9 @@ var init_stream_renderer = __esm(() => {
4546
4849
 
4547
4850
  // src/repl/clipboard.ts
4548
4851
  import { execSync as execSync6 } from "node:child_process";
4549
- import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
4852
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9 } from "node:fs";
4550
4853
  import { tmpdir as tmpdir2 } from "node:os";
4551
- import { join as join13 } from "node:path";
4854
+ import { join as join14 } from "node:path";
4552
4855
  function readClipboardImage() {
4553
4856
  if (process.platform === "darwin") {
4554
4857
  return readMacOSClipboardImage();
@@ -4559,14 +4862,14 @@ function readClipboardImage() {
4559
4862
  return null;
4560
4863
  }
4561
4864
  function ensureStableDir() {
4562
- if (!existsSync13(STABLE_DIR)) {
4865
+ if (!existsSync14(STABLE_DIR)) {
4563
4866
  mkdirSync9(STABLE_DIR, { recursive: true });
4564
4867
  }
4565
4868
  }
4566
4869
  function readMacOSClipboardImage() {
4567
4870
  try {
4568
4871
  ensureStableDir();
4569
- const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
4872
+ const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
4570
4873
  const script = [
4571
4874
  `set destPath to POSIX file "${destPath}"`,
4572
4875
  "try",
@@ -4590,7 +4893,7 @@ function readMacOSClipboardImage() {
4590
4893
  timeout: 5000,
4591
4894
  stdio: ["pipe", "pipe", "pipe"]
4592
4895
  }).trim();
4593
- if (result === "ok" && existsSync13(destPath)) {
4896
+ if (result === "ok" && existsSync14(destPath)) {
4594
4897
  return destPath;
4595
4898
  }
4596
4899
  } catch {}
@@ -4603,9 +4906,9 @@ function readLinuxClipboardImage() {
4603
4906
  return null;
4604
4907
  }
4605
4908
  ensureStableDir();
4606
- const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
4909
+ const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
4607
4910
  execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
4608
- if (existsSync13(destPath)) {
4911
+ if (existsSync14(destPath)) {
4609
4912
  return destPath;
4610
4913
  }
4611
4914
  } catch {}
@@ -4613,13 +4916,13 @@ function readLinuxClipboardImage() {
4613
4916
  }
4614
4917
  var STABLE_DIR;
4615
4918
  var init_clipboard = __esm(() => {
4616
- STABLE_DIR = join13(tmpdir2(), "locus-images");
4919
+ STABLE_DIR = join14(tmpdir2(), "locus-images");
4617
4920
  });
4618
4921
 
4619
4922
  // src/repl/image-detect.ts
4620
- import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10 } from "node:fs";
4923
+ import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "node:fs";
4621
4924
  import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
4622
- import { basename, extname, join as join14, resolve } from "node:path";
4925
+ import { basename, extname, join as join15, resolve } from "node:path";
4623
4926
  function detectImages(input) {
4624
4927
  const detected = [];
4625
4928
  const byResolved = new Map;
@@ -4713,15 +5016,15 @@ function collectReferencedAttachments(input, attachments) {
4713
5016
  return dedupeByResolvedPath(selected);
4714
5017
  }
4715
5018
  function relocateImages(images, projectRoot) {
4716
- const targetDir = join14(projectRoot, ".locus", "tmp", "images");
5019
+ const targetDir = join15(projectRoot, ".locus", "tmp", "images");
4717
5020
  for (const img of images) {
4718
5021
  if (!img.exists)
4719
5022
  continue;
4720
5023
  try {
4721
- if (!existsSync14(targetDir)) {
5024
+ if (!existsSync15(targetDir)) {
4722
5025
  mkdirSync10(targetDir, { recursive: true });
4723
5026
  }
4724
- const dest = join14(targetDir, basename(img.stablePath));
5027
+ const dest = join15(targetDir, basename(img.stablePath));
4725
5028
  copyFileSync(img.stablePath, dest);
4726
5029
  img.stablePath = dest;
4727
5030
  } catch {}
@@ -4733,7 +5036,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
4733
5036
  return;
4734
5037
  let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
4735
5038
  if (resolved.startsWith("~/")) {
4736
- resolved = join14(homedir3(), resolved.slice(2));
5039
+ resolved = join15(homedir3(), resolved.slice(2));
4737
5040
  }
4738
5041
  resolved = resolve(resolved);
4739
5042
  const existing = byResolved.get(resolved);
@@ -4746,7 +5049,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
4746
5049
  ]);
4747
5050
  return;
4748
5051
  }
4749
- const exists = existsSync14(resolved);
5052
+ const exists = existsSync15(resolved);
4750
5053
  let stablePath = resolved;
4751
5054
  if (exists) {
4752
5055
  stablePath = copyToStable(resolved);
@@ -4800,10 +5103,10 @@ function dedupeByResolvedPath(images) {
4800
5103
  }
4801
5104
  function copyToStable(sourcePath) {
4802
5105
  try {
4803
- if (!existsSync14(STABLE_DIR2)) {
5106
+ if (!existsSync15(STABLE_DIR2)) {
4804
5107
  mkdirSync10(STABLE_DIR2, { recursive: true });
4805
5108
  }
4806
- const dest = join14(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
5109
+ const dest = join15(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
4807
5110
  copyFileSync(sourcePath, dest);
4808
5111
  return dest;
4809
5112
  } catch {
@@ -4823,7 +5126,7 @@ var init_image_detect = __esm(() => {
4823
5126
  ".tif",
4824
5127
  ".tiff"
4825
5128
  ]);
4826
- STABLE_DIR2 = join14(tmpdir3(), "locus-images");
5129
+ STABLE_DIR2 = join15(tmpdir3(), "locus-images");
4827
5130
  PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
4828
5131
  });
4829
5132
 
@@ -5832,21 +6135,21 @@ var init_claude = __esm(() => {
5832
6135
  import { exec } from "node:child_process";
5833
6136
  import {
5834
6137
  cpSync,
5835
- existsSync as existsSync15,
6138
+ existsSync as existsSync16,
5836
6139
  mkdirSync as mkdirSync11,
5837
6140
  mkdtempSync,
5838
6141
  readdirSync as readdirSync3,
5839
- readFileSync as readFileSync10,
6142
+ readFileSync as readFileSync11,
5840
6143
  rmSync as rmSync2,
5841
6144
  statSync as statSync3
5842
6145
  } from "node:fs";
5843
6146
  import { tmpdir as tmpdir4 } from "node:os";
5844
- import { dirname as dirname3, join as join15, relative } from "node:path";
6147
+ import { dirname as dirname3, join as join16, relative } from "node:path";
5845
6148
  import { promisify } from "node:util";
5846
6149
  function parseIgnoreFile(filePath) {
5847
- if (!existsSync15(filePath))
6150
+ if (!existsSync16(filePath))
5848
6151
  return [];
5849
- const content = readFileSync10(filePath, "utf-8");
6152
+ const content = readFileSync11(filePath, "utf-8");
5850
6153
  const rules = [];
5851
6154
  for (const rawLine of content.split(`
5852
6155
  `)) {
@@ -5914,14 +6217,14 @@ function findIgnoredPaths(projectRoot, rules) {
5914
6217
  for (const name of entries) {
5915
6218
  if (SKIP_DIRS.has(name))
5916
6219
  continue;
5917
- const fullPath = join15(dir, name);
5918
- let stat = null;
6220
+ const fullPath = join16(dir, name);
6221
+ let stat2 = null;
5919
6222
  try {
5920
- stat = statSync3(fullPath);
6223
+ stat2 = statSync3(fullPath);
5921
6224
  } catch {
5922
6225
  continue;
5923
6226
  }
5924
- const isDir = stat.isDirectory();
6227
+ const isDir = stat2.isDirectory();
5925
6228
  let matched = false;
5926
6229
  for (const m of positiveMatchers) {
5927
6230
  if (m.isDirectory && !isDir)
@@ -5948,7 +6251,7 @@ function findIgnoredPaths(projectRoot, rules) {
5948
6251
  }
5949
6252
  function backupIgnoredFiles(projectRoot) {
5950
6253
  const log = getLogger();
5951
- const ignorePath = join15(projectRoot, ".sandboxignore");
6254
+ const ignorePath = join16(projectRoot, ".sandboxignore");
5952
6255
  const rules = parseIgnoreFile(ignorePath);
5953
6256
  if (rules.length === 0)
5954
6257
  return NOOP_BACKUP;
@@ -5957,7 +6260,7 @@ function backupIgnoredFiles(projectRoot) {
5957
6260
  return NOOP_BACKUP;
5958
6261
  let backupDir;
5959
6262
  try {
5960
- backupDir = mkdtempSync(join15(tmpdir4(), "locus-sandbox-backup-"));
6263
+ backupDir = mkdtempSync(join16(tmpdir4(), "locus-sandbox-backup-"));
5961
6264
  } catch (err) {
5962
6265
  log.debug("Failed to create sandbox backup dir", {
5963
6266
  error: err instanceof Error ? err.message : String(err)
@@ -5967,7 +6270,7 @@ function backupIgnoredFiles(projectRoot) {
5967
6270
  const backed = [];
5968
6271
  for (const src of paths) {
5969
6272
  const rel = relative(projectRoot, src);
5970
- const dest = join15(backupDir, rel);
6273
+ const dest = join16(backupDir, rel);
5971
6274
  try {
5972
6275
  mkdirSync11(dirname3(dest), { recursive: true });
5973
6276
  cpSync(src, dest, { recursive: true, preserveTimestamps: true });
@@ -6009,7 +6312,7 @@ function backupIgnoredFiles(projectRoot) {
6009
6312
  }
6010
6313
  async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
6011
6314
  const log = getLogger();
6012
- const ignorePath = join15(projectRoot, ".sandboxignore");
6315
+ const ignorePath = join16(projectRoot, ".sandboxignore");
6013
6316
  const rules = parseIgnoreFile(ignorePath);
6014
6317
  if (rules.length === 0)
6015
6318
  return;
@@ -8173,14 +8476,113 @@ var init_sprint = __esm(() => {
8173
8476
  init_terminal();
8174
8477
  });
8175
8478
 
8479
+ // src/skills/matcher.ts
8480
+ function matchRelevantSkills(issue, skills) {
8481
+ if (skills.length <= MAX_SKILLS)
8482
+ return skills;
8483
+ const titleTokens = tokenize(issue.title);
8484
+ const bodyTokens = tokenize(issue.body).slice(0, 200);
8485
+ const labelTokens = issue.labels.flatMap((l) => tokenize(l));
8486
+ const scored = skills.map((skill) => {
8487
+ let score = 0;
8488
+ const tagSet = new Set(skill.tags.map((t) => t.toLowerCase()));
8489
+ const descTokens = new Set(tokenize(skill.description));
8490
+ for (const lt of labelTokens) {
8491
+ if (tagSet.has(lt))
8492
+ score += 5;
8493
+ }
8494
+ for (const tw of titleTokens) {
8495
+ if (tagSet.has(tw))
8496
+ score += 3;
8497
+ }
8498
+ for (const bw of bodyTokens) {
8499
+ if (tagSet.has(bw))
8500
+ score += 2;
8501
+ }
8502
+ for (const tw of titleTokens) {
8503
+ if (descTokens.has(tw))
8504
+ score += 1;
8505
+ }
8506
+ for (const bw of bodyTokens) {
8507
+ if (descTokens.has(bw))
8508
+ score += 0.5;
8509
+ }
8510
+ return { skill, score };
8511
+ });
8512
+ scored.sort((a, b) => b.score - a.score);
8513
+ const relevant = scored.filter((s) => s.score >= RELEVANCE_THRESHOLD);
8514
+ if (relevant.length > 0) {
8515
+ return relevant.slice(0, MAX_SKILLS).map((s) => s.skill);
8516
+ }
8517
+ return scored.slice(0, FALLBACK_COUNT).map((s) => s.skill);
8518
+ }
8519
+ function tokenize(text) {
8520
+ if (!text)
8521
+ return [];
8522
+ return text.toLowerCase().replace(/[^a-z0-9\-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
8523
+ }
8524
+ var RELEVANCE_THRESHOLD = 2, MAX_SKILLS = 5, FALLBACK_COUNT = 3, STOP_WORDS;
8525
+ var init_matcher = __esm(() => {
8526
+ STOP_WORDS = new Set([
8527
+ "a",
8528
+ "an",
8529
+ "the",
8530
+ "is",
8531
+ "it",
8532
+ "to",
8533
+ "in",
8534
+ "on",
8535
+ "of",
8536
+ "for",
8537
+ "and",
8538
+ "or",
8539
+ "not",
8540
+ "with",
8541
+ "this",
8542
+ "that",
8543
+ "from",
8544
+ "by",
8545
+ "as",
8546
+ "at",
8547
+ "be",
8548
+ "we",
8549
+ "i",
8550
+ "you",
8551
+ "my",
8552
+ "our",
8553
+ "do",
8554
+ "if",
8555
+ "no",
8556
+ "so",
8557
+ "up",
8558
+ "can",
8559
+ "all",
8560
+ "but",
8561
+ "has",
8562
+ "had",
8563
+ "have",
8564
+ "will",
8565
+ "should",
8566
+ "would",
8567
+ "could",
8568
+ "need",
8569
+ "want",
8570
+ "also",
8571
+ "new",
8572
+ "use",
8573
+ "add",
8574
+ "make"
8575
+ ]);
8576
+ });
8577
+
8176
8578
  // src/core/prompt-builder.ts
8177
8579
  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";
8580
+ import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
8581
+ import { join as join17 } from "node:path";
8180
8582
  function buildExecutionPrompt(ctx) {
8181
8583
  const sections = [];
8182
8584
  sections.push(buildSystemContext(ctx.projectRoot));
8183
- const skills = buildSkillsContext(ctx.projectRoot);
8585
+ const skills = buildSkillsContext(ctx.projectRoot, ctx.issue);
8184
8586
  if (skills)
8185
8587
  sections.push(skills);
8186
8588
  sections.push(buildTaskContext(ctx.issue, ctx.issueComments));
@@ -8198,7 +8600,7 @@ function buildExecutionPrompt(ctx) {
8198
8600
  function buildFeedbackPrompt(ctx) {
8199
8601
  const sections = [];
8200
8602
  sections.push(buildSystemContext(ctx.projectRoot));
8201
- const skills = buildSkillsContext(ctx.projectRoot);
8603
+ const skills = buildSkillsContext(ctx.projectRoot, ctx.issue);
8202
8604
  if (skills)
8203
8605
  sections.push(skills);
8204
8606
  sections.push(buildTaskContext(ctx.issue));
@@ -8212,15 +8614,22 @@ function buildFeedbackPrompt(ctx) {
8212
8614
  }
8213
8615
  function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
8214
8616
  const sections = [];
8215
- const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
8617
+ const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
8216
8618
  if (locusmd) {
8217
8619
  sections.push(`<project-instructions>
8218
8620
  ${locusmd}
8219
8621
  </project-instructions>`);
8220
8622
  }
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>`);
8623
+ const memory = loadMemoryContent(projectRoot);
8624
+ if (memory) {
8625
+ sections.push(`<past-learnings>
8626
+ ${memory}
8627
+ </past-learnings>`);
8628
+ } else {
8629
+ sections.push(`<past-learnings>
8630
+ No past learnings recorded yet.</past-learnings>`);
8631
+ }
8632
+ 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
8633
  if (previousMessages && previousMessages.length > 0) {
8225
8634
  const recent = previousMessages.slice(-10);
8226
8635
  const historyLines = recent.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`);
@@ -8239,22 +8648,41 @@ ${userMessage}
8239
8648
 
8240
8649
  `);
8241
8650
  }
8651
+ function loadMemoryContent(projectRoot) {
8652
+ const memoryDir = getMemoryDir(projectRoot);
8653
+ if (existsSync17(memoryDir)) {
8654
+ const content = readAllMemorySync(projectRoot);
8655
+ if (content.trim()) {
8656
+ return content.length > MEMORY_MAX_CHARS ? `${content.slice(0, MEMORY_MAX_CHARS)}
8657
+
8658
+ ...(truncated)` : content;
8659
+ }
8660
+ }
8661
+ return "";
8662
+ }
8242
8663
  function buildSystemContext(projectRoot) {
8243
8664
  const parts = [];
8244
- const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
8665
+ const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
8245
8666
  if (locusmd) {
8246
8667
  parts.push(`<project-instructions>
8247
8668
  ${locusmd}
8248
8669
  </project-instructions>`);
8249
8670
  }
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)) {
8671
+ const memory = loadMemoryContent(projectRoot);
8672
+ if (memory) {
8673
+ parts.push(`<past-learnings>
8674
+ ${memory}
8675
+ </past-learnings>`);
8676
+ } else {
8677
+ parts.push(`<past-learnings>
8678
+ No past learnings recorded yet.</past-learnings>`);
8679
+ }
8680
+ const discussionsDir = join17(projectRoot, ".locus", "discussions");
8681
+ if (existsSync17(discussionsDir)) {
8254
8682
  try {
8255
8683
  const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
8256
8684
  for (const file of files) {
8257
- const content = readFileSafe(join16(discussionsDir, file));
8685
+ const content = readFileSafe(join17(discussionsDir, file));
8258
8686
  if (content) {
8259
8687
  const name = file.replace(".md", "");
8260
8688
  parts.push(`<discussion name="${name}">
@@ -8363,7 +8791,7 @@ function buildExecutionRules(config) {
8363
8791
  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
8792
  2. **Code quality:** Follow existing code style. Run linters/formatters if available.
8365
8793
  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.
8794
+ 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
8795
  5. **Do NOT:**
8368
8796
  - Run \`git push\` (the orchestrator handles pushing)
8369
8797
  - Modify files outside the scope of this issue
@@ -8401,13 +8829,13 @@ function buildFeedbackInstructions() {
8401
8829
  2. Make targeted changes — do NOT rewrite code from scratch.
8402
8830
  3. If a reviewer comment is unclear, make your best judgment and note your interpretation.
8403
8831
  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\`.
8832
+ 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
8833
  6. When done, summarize what you changed in response to each comment.
8406
8834
  </instructions>`;
8407
8835
  }
8408
- function buildSkillsContext(projectRoot) {
8409
- const skillsDir = join16(projectRoot, CLAUDE_SKILLS_DIR);
8410
- if (!existsSync16(skillsDir))
8836
+ function buildSkillsContext(projectRoot, issue) {
8837
+ const skillsDir = join17(projectRoot, CLAUDE_SKILLS_DIR);
8838
+ if (!existsSync17(skillsDir))
8411
8839
  return null;
8412
8840
  let dirs;
8413
8841
  try {
@@ -8417,21 +8845,35 @@ function buildSkillsContext(projectRoot) {
8417
8845
  }
8418
8846
  if (dirs.length === 0)
8419
8847
  return null;
8420
- const entries = [];
8848
+ let allSkills = [];
8421
8849
  for (const dir of dirs) {
8422
- const skillPath = join16(skillsDir, dir, "SKILL.md");
8850
+ const skillPath = join17(skillsDir, dir, "SKILL.md");
8423
8851
  const content = readFileSafe(skillPath);
8424
8852
  if (!content)
8425
8853
  continue;
8426
8854
  const fm = parseFrontmatter(content);
8427
- const name = fm.name || dir;
8428
- const description = fm.description || "";
8429
- entries.push(`- **${name}**: ${description}`);
8855
+ allSkills.push({
8856
+ dir,
8857
+ name: fm.name || dir,
8858
+ description: fm.description || "",
8859
+ tags: fm.tags ? fm.tags.split(",").map((t) => t.trim()) : []
8860
+ });
8430
8861
  }
8431
- if (entries.length === 0)
8862
+ if (allSkills.length === 0)
8432
8863
  return null;
8864
+ let selectedSkills = allSkills;
8865
+ if (issue) {
8866
+ const issueCtx = {
8867
+ title: issue.title,
8868
+ body: issue.body || "",
8869
+ labels: issue.labels
8870
+ };
8871
+ const relevant = matchRelevantSkills(issueCtx, allSkills);
8872
+ selectedSkills = allSkills.filter((s) => relevant.some((r) => r.name === s.name));
8873
+ }
8874
+ const entries = selectedSkills.map((s) => `- **${s.name}**: ${s.description}`);
8433
8875
  return `<installed-skills>
8434
- The following skills are installed in this project. If a skill is relevant to the current task, read its full instructions from \`.claude/skills/<name>/SKILL.md\` before starting work.
8876
+ The following skills are installed and relevant to this task. Read the full instructions from \`.claude/skills/<name>/SKILL.md\` before using a skill.
8435
8877
 
8436
8878
  ${entries.join(`
8437
8879
  `)}
@@ -8448,22 +8890,30 @@ function parseFrontmatter(content) {
8448
8890
  if (idx === -1)
8449
8891
  continue;
8450
8892
  const key = line.slice(0, idx).trim();
8451
- const val = line.slice(idx + 1).trim();
8452
- if (key && val)
8453
- result[key] = val;
8893
+ let val = line.slice(idx + 1).trim();
8894
+ if (!key || !val)
8895
+ continue;
8896
+ if (val.startsWith("[") && val.endsWith("]")) {
8897
+ val = val.slice(1, -1);
8898
+ }
8899
+ result[key] = val;
8454
8900
  }
8455
8901
  return result;
8456
8902
  }
8457
8903
  function readFileSafe(path) {
8458
8904
  try {
8459
- if (!existsSync16(path))
8905
+ if (!existsSync17(path))
8460
8906
  return null;
8461
- return readFileSync11(path, "utf-8");
8907
+ return readFileSync12(path, "utf-8");
8462
8908
  } catch {
8463
8909
  return null;
8464
8910
  }
8465
8911
  }
8466
- var init_prompt_builder = () => {};
8912
+ var MEMORY_MAX_CHARS = 4000;
8913
+ var init_prompt_builder = __esm(() => {
8914
+ init_matcher();
8915
+ init_memory();
8916
+ });
8467
8917
 
8468
8918
  // src/display/json-stream.ts
8469
8919
  class JsonStream {
@@ -8555,6 +9005,180 @@ class JsonStream {
8555
9005
  }
8556
9006
  }
8557
9007
 
9008
+ // src/core/memory-capture.ts
9009
+ import { spawn as spawn6 } from "node:child_process";
9010
+ function prepareTranscript(messages) {
9011
+ if (!messages || messages.length === 0)
9012
+ return "";
9013
+ const lines = [];
9014
+ for (const msg of messages) {
9015
+ const label = msg.role === "user" ? "User" : "Assistant";
9016
+ lines.push(`### ${label}
9017
+ ${msg.content}`);
9018
+ }
9019
+ let transcript = lines.join(`
9020
+
9021
+ `);
9022
+ if (transcript.length > TRANSCRIPT_MAX_CHARS) {
9023
+ transcript = `${transcript.slice(0, TRANSCRIPT_MAX_CHARS)}
9024
+
9025
+ ...(truncated)`;
9026
+ }
9027
+ return transcript;
9028
+ }
9029
+ function buildExtractionPrompt(transcript, existingMemory) {
9030
+ const categoryList = Object.entries(MEMORY_CATEGORIES).map(([key, meta]) => `- "${key}": ${meta.title} — ${meta.description}`).join(`
9031
+ `);
9032
+ return `You are a memory extraction assistant. Extract project-level reusable lessons from the following session transcript.
9033
+
9034
+ ## Valid Categories
9035
+
9036
+ ${categoryList}
9037
+
9038
+ ## Existing Memory (for deduplication)
9039
+
9040
+ Do NOT extract entries that duplicate or closely overlap with these existing entries:
9041
+
9042
+ <existing-memory>
9043
+ ${existingMemory || "(none)"}
9044
+ </existing-memory>
9045
+
9046
+ ## Quality Bar
9047
+
9048
+ Only extract entries that would help a new agent on a future task. Skip:
9049
+ - Session-specific details or in-progress work
9050
+ - One-time fixes or trivial observations
9051
+ - Speculative or unverified conclusions
9052
+ - Anything that duplicates existing memory entries above
9053
+
9054
+ ## Output Format
9055
+
9056
+ Respond with ONLY a JSON array. No markdown fencing, no explanation. Example:
9057
+ [{"category": "architecture", "text": "SDK types are shared via @locusai/shared package"}]
9058
+
9059
+ If there are no extractable lessons, respond with: []
9060
+
9061
+ ## Session Transcript
9062
+
9063
+ ${transcript}`;
9064
+ }
9065
+ function callExtractionAI(prompt, model) {
9066
+ return new Promise((resolve2) => {
9067
+ const args = [
9068
+ "--print",
9069
+ "--dangerously-skip-permissions",
9070
+ "--no-session-persistence"
9071
+ ];
9072
+ if (model) {
9073
+ args.push("--model", model);
9074
+ }
9075
+ const env = { ...process.env };
9076
+ delete env.CLAUDECODE;
9077
+ delete env.CLAUDE_CODE;
9078
+ const proc = spawn6("claude", args, {
9079
+ stdio: ["pipe", "pipe", "pipe"],
9080
+ env
9081
+ });
9082
+ let output = "";
9083
+ let errorOutput = "";
9084
+ proc.stdout?.on("data", (chunk) => {
9085
+ output += chunk.toString();
9086
+ });
9087
+ proc.stderr?.on("data", (chunk) => {
9088
+ errorOutput += chunk.toString();
9089
+ });
9090
+ proc.on("close", (code) => {
9091
+ if (code === 0) {
9092
+ resolve2({ success: true, output });
9093
+ } else {
9094
+ resolve2({
9095
+ success: false,
9096
+ output,
9097
+ error: errorOutput || `claude exited with code ${code}`
9098
+ });
9099
+ }
9100
+ });
9101
+ proc.on("error", (err) => {
9102
+ resolve2({
9103
+ success: false,
9104
+ output: "",
9105
+ error: `Failed to spawn claude: ${err.message}`
9106
+ });
9107
+ });
9108
+ proc.stdin?.write(prompt);
9109
+ proc.stdin?.end();
9110
+ });
9111
+ }
9112
+ function parseExtractionResponse(raw) {
9113
+ let text = raw.trim();
9114
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
9115
+ if (fenceMatch) {
9116
+ text = fenceMatch[1].trim();
9117
+ }
9118
+ const arrayMatch = text.match(/\[[\s\S]*\]/);
9119
+ if (!arrayMatch)
9120
+ return [];
9121
+ const parsed = JSON.parse(arrayMatch[0]);
9122
+ if (!Array.isArray(parsed))
9123
+ return [];
9124
+ return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.category === "string" && typeof item.text === "string");
9125
+ }
9126
+ async function captureMemoryFromSession(projectRoot, transcript, config) {
9127
+ const log = getLogger();
9128
+ try {
9129
+ if (!transcript.trim()) {
9130
+ return { captured: 0 };
9131
+ }
9132
+ const existingMemory = await readAllMemory(projectRoot);
9133
+ const prompt = buildExtractionPrompt(transcript, existingMemory);
9134
+ const result = await callExtractionAI(prompt, config.model);
9135
+ if (!result.success) {
9136
+ log.warn("Memory capture: AI call failed", { error: result.error });
9137
+ return { captured: 0 };
9138
+ }
9139
+ let entries;
9140
+ try {
9141
+ entries = parseExtractionResponse(result.output);
9142
+ } catch (e) {
9143
+ log.warn("Memory capture: failed to parse AI response", {
9144
+ error: e instanceof Error ? e.message : String(e)
9145
+ });
9146
+ return { captured: 0 };
9147
+ }
9148
+ if (entries.length === 0) {
9149
+ return { captured: 0 };
9150
+ }
9151
+ const validEntries = entries.filter((entry) => {
9152
+ if (!VALID_CATEGORIES.has(entry.category)) {
9153
+ log.debug("Memory capture: skipping invalid category", {
9154
+ category: entry.category
9155
+ });
9156
+ return false;
9157
+ }
9158
+ return true;
9159
+ });
9160
+ if (validEntries.length === 0) {
9161
+ return { captured: 0 };
9162
+ }
9163
+ await appendMemoryEntries(projectRoot, validEntries);
9164
+ log.debug("Memory capture: extracted entries", {
9165
+ count: validEntries.length
9166
+ });
9167
+ return { captured: validEntries.length };
9168
+ } catch (e) {
9169
+ log.warn("Memory capture: unexpected error", {
9170
+ error: e instanceof Error ? e.message : String(e)
9171
+ });
9172
+ return { captured: 0 };
9173
+ }
9174
+ }
9175
+ var TRANSCRIPT_MAX_CHARS = 32000, VALID_CATEGORIES;
9176
+ var init_memory_capture = __esm(() => {
9177
+ init_logger();
9178
+ init_memory();
9179
+ VALID_CATEGORIES = new Set(Object.keys(MEMORY_CATEGORIES));
9180
+ });
9181
+
8558
9182
  // src/display/diff-renderer.ts
8559
9183
  function renderDiff(diff, options = {}) {
8560
9184
  const { maxLines, lineNumbers = true } = options;
@@ -8936,7 +9560,7 @@ var init_commands = __esm(() => {
8936
9560
 
8937
9561
  // src/repl/completions.ts
8938
9562
  import { readdirSync as readdirSync5 } from "node:fs";
8939
- import { basename as basename2, dirname as dirname4, join as join17 } from "node:path";
9563
+ import { basename as basename2, dirname as dirname4, join as join18 } from "node:path";
8940
9564
 
8941
9565
  class SlashCommandCompletion {
8942
9566
  commands;
@@ -8991,7 +9615,7 @@ class FilePathCompletion {
8991
9615
  }
8992
9616
  findMatches(partial) {
8993
9617
  try {
8994
- const dir = partial.includes("/") ? join17(this.projectRoot, dirname4(partial)) : this.projectRoot;
9618
+ const dir = partial.includes("/") ? join18(this.projectRoot, dirname4(partial)) : this.projectRoot;
8995
9619
  const prefix = basename2(partial);
8996
9620
  const entries = readdirSync5(dir, { withFileTypes: true });
8997
9621
  return entries.filter((e) => {
@@ -9027,14 +9651,14 @@ class CombinedCompletion {
9027
9651
  var init_completions = () => {};
9028
9652
 
9029
9653
  // 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";
9654
+ import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
9655
+ import { dirname as dirname5, join as join19 } from "node:path";
9032
9656
 
9033
9657
  class InputHistory {
9034
9658
  entries = [];
9035
9659
  filePath;
9036
9660
  constructor(projectRoot) {
9037
- this.filePath = join18(projectRoot, ".locus", "sessions", ".input-history");
9661
+ this.filePath = join19(projectRoot, ".locus", "sessions", ".input-history");
9038
9662
  this.load();
9039
9663
  }
9040
9664
  add(text) {
@@ -9073,9 +9697,9 @@ class InputHistory {
9073
9697
  }
9074
9698
  load() {
9075
9699
  try {
9076
- if (!existsSync17(this.filePath))
9700
+ if (!existsSync18(this.filePath))
9077
9701
  return;
9078
- const content = readFileSync12(this.filePath, "utf-8");
9702
+ const content = readFileSync13(this.filePath, "utf-8");
9079
9703
  this.entries = content.split(`
9080
9704
  `).map((line) => this.unescape(line)).filter(Boolean);
9081
9705
  } catch {}
@@ -9083,7 +9707,7 @@ class InputHistory {
9083
9707
  save() {
9084
9708
  try {
9085
9709
  const dir = dirname5(this.filePath);
9086
- if (!existsSync17(dir)) {
9710
+ if (!existsSync18(dir)) {
9087
9711
  mkdirSync12(dir, { recursive: true });
9088
9712
  }
9089
9713
  const content = this.entries.map((e) => this.escape(e)).join(`
@@ -9114,20 +9738,20 @@ var init_model_config = __esm(() => {
9114
9738
 
9115
9739
  // src/repl/session-manager.ts
9116
9740
  import {
9117
- existsSync as existsSync18,
9741
+ existsSync as existsSync19,
9118
9742
  mkdirSync as mkdirSync13,
9119
9743
  readdirSync as readdirSync6,
9120
- readFileSync as readFileSync13,
9744
+ readFileSync as readFileSync14,
9121
9745
  unlinkSync as unlinkSync3,
9122
9746
  writeFileSync as writeFileSync10
9123
9747
  } from "node:fs";
9124
- import { basename as basename3, join as join19 } from "node:path";
9748
+ import { basename as basename3, join as join20 } from "node:path";
9125
9749
 
9126
9750
  class SessionManager {
9127
9751
  sessionsDir;
9128
9752
  constructor(projectRoot) {
9129
- this.sessionsDir = join19(projectRoot, ".locus", "sessions");
9130
- if (!existsSync18(this.sessionsDir)) {
9753
+ this.sessionsDir = join20(projectRoot, ".locus", "sessions");
9754
+ if (!existsSync19(this.sessionsDir)) {
9131
9755
  mkdirSync13(this.sessionsDir, { recursive: true });
9132
9756
  }
9133
9757
  }
@@ -9153,14 +9777,14 @@ class SessionManager {
9153
9777
  }
9154
9778
  isPersisted(sessionOrId) {
9155
9779
  const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
9156
- return existsSync18(this.getSessionPath(sessionId));
9780
+ return existsSync19(this.getSessionPath(sessionId));
9157
9781
  }
9158
9782
  load(idOrPrefix) {
9159
9783
  const files = this.listSessionFiles();
9160
9784
  const exactPath = this.getSessionPath(idOrPrefix);
9161
- if (existsSync18(exactPath)) {
9785
+ if (existsSync19(exactPath)) {
9162
9786
  try {
9163
- return JSON.parse(readFileSync13(exactPath, "utf-8"));
9787
+ return JSON.parse(readFileSync14(exactPath, "utf-8"));
9164
9788
  } catch {
9165
9789
  return null;
9166
9790
  }
@@ -9168,7 +9792,7 @@ class SessionManager {
9168
9792
  const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
9169
9793
  if (matches.length === 1) {
9170
9794
  try {
9171
- return JSON.parse(readFileSync13(matches[0], "utf-8"));
9795
+ return JSON.parse(readFileSync14(matches[0], "utf-8"));
9172
9796
  } catch {
9173
9797
  return null;
9174
9798
  }
@@ -9193,7 +9817,7 @@ class SessionManager {
9193
9817
  const sessions = [];
9194
9818
  for (const file of files) {
9195
9819
  try {
9196
- const session = JSON.parse(readFileSync13(file, "utf-8"));
9820
+ const session = JSON.parse(readFileSync14(file, "utf-8"));
9197
9821
  sessions.push({
9198
9822
  id: session.id,
9199
9823
  created: session.created,
@@ -9208,7 +9832,7 @@ class SessionManager {
9208
9832
  }
9209
9833
  delete(sessionId) {
9210
9834
  const path = this.getSessionPath(sessionId);
9211
- if (existsSync18(path)) {
9835
+ if (existsSync19(path)) {
9212
9836
  unlinkSync3(path);
9213
9837
  return true;
9214
9838
  }
@@ -9220,7 +9844,7 @@ class SessionManager {
9220
9844
  let pruned = 0;
9221
9845
  const withStats = files.map((f) => {
9222
9846
  try {
9223
- const session = JSON.parse(readFileSync13(f, "utf-8"));
9847
+ const session = JSON.parse(readFileSync14(f, "utf-8"));
9224
9848
  return { path: f, updated: new Date(session.updated).getTime() };
9225
9849
  } catch {
9226
9850
  return { path: f, updated: 0 };
@@ -9238,7 +9862,7 @@ class SessionManager {
9238
9862
  const remaining = withStats.length - pruned;
9239
9863
  if (remaining > MAX_SESSIONS) {
9240
9864
  const toRemove = remaining - MAX_SESSIONS;
9241
- const alive = withStats.filter((e) => existsSync18(e.path));
9865
+ const alive = withStats.filter((e) => existsSync19(e.path));
9242
9866
  for (let i = 0;i < toRemove && i < alive.length; i++) {
9243
9867
  try {
9244
9868
  unlinkSync3(alive[i].path);
@@ -9253,7 +9877,7 @@ class SessionManager {
9253
9877
  }
9254
9878
  listSessionFiles() {
9255
9879
  try {
9256
- return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join19(this.sessionsDir, f));
9880
+ return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join20(this.sessionsDir, f));
9257
9881
  } catch {
9258
9882
  return [];
9259
9883
  }
@@ -9262,7 +9886,7 @@ class SessionManager {
9262
9886
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
9263
9887
  }
9264
9888
  getSessionPath(sessionId) {
9265
- return join19(this.sessionsDir, `${sessionId}.json`);
9889
+ return join20(this.sessionsDir, `${sessionId}.json`);
9266
9890
  }
9267
9891
  }
9268
9892
  var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
@@ -9272,12 +9896,12 @@ var init_session_manager = __esm(() => {
9272
9896
  });
9273
9897
 
9274
9898
  // 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";
9899
+ import { execSync as execSync11, spawn as spawn7 } from "node:child_process";
9900
+ import { existsSync as existsSync20, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
9277
9901
  import { cpus, homedir as homedir4, platform, tmpdir as tmpdir5 } from "node:os";
9278
- import { join as join20 } from "node:path";
9902
+ import { join as join21 } from "node:path";
9279
9903
  function getWhisperModelPath() {
9280
- return join20(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
9904
+ return join21(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
9281
9905
  }
9282
9906
  function commandExists(cmd) {
9283
9907
  try {
@@ -9295,16 +9919,16 @@ function findWhisperBinary() {
9295
9919
  return name;
9296
9920
  }
9297
9921
  for (const name of candidates) {
9298
- const fullPath = join20(LOCUS_BIN_DIR, name);
9299
- if (existsSync19(fullPath))
9922
+ const fullPath = join21(LOCUS_BIN_DIR, name);
9923
+ if (existsSync20(fullPath))
9300
9924
  return fullPath;
9301
9925
  }
9302
9926
  if (platform() === "darwin") {
9303
9927
  const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
9304
9928
  for (const dir of brewDirs) {
9305
9929
  for (const name of candidates) {
9306
- const fullPath = join20(dir, name);
9307
- if (existsSync19(fullPath))
9930
+ const fullPath = join21(dir, name);
9931
+ if (existsSync20(fullPath))
9308
9932
  return fullPath;
9309
9933
  }
9310
9934
  }
@@ -9319,11 +9943,11 @@ function findSoxRecBinary() {
9319
9943
  if (platform() === "darwin") {
9320
9944
  const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
9321
9945
  for (const dir of brewDirs) {
9322
- const recPath = join20(dir, "rec");
9323
- if (existsSync19(recPath))
9946
+ const recPath = join21(dir, "rec");
9947
+ if (existsSync20(recPath))
9324
9948
  return recPath;
9325
- const soxPath = join20(dir, "sox");
9326
- if (existsSync19(soxPath))
9949
+ const soxPath = join21(dir, "sox");
9950
+ if (existsSync20(soxPath))
9327
9951
  return soxPath;
9328
9952
  }
9329
9953
  }
@@ -9332,7 +9956,7 @@ function findSoxRecBinary() {
9332
9956
  function checkDependencies() {
9333
9957
  const soxBinary = findSoxRecBinary();
9334
9958
  const whisperBinary = findWhisperBinary();
9335
- const modelDownloaded = existsSync19(getWhisperModelPath());
9959
+ const modelDownloaded = existsSync20(getWhisperModelPath());
9336
9960
  return {
9337
9961
  sox: soxBinary !== null,
9338
9962
  whisper: whisperBinary !== null,
@@ -9486,7 +10110,7 @@ function ensureBuildDeps(pm) {
9486
10110
  }
9487
10111
  function buildWhisperFromSource(pm) {
9488
10112
  const out = process.stderr;
9489
- const buildDir = join20(tmpdir5(), `locus-whisper-build-${process.pid}`);
10113
+ const buildDir = join21(tmpdir5(), `locus-whisper-build-${process.pid}`);
9490
10114
  if (!ensureBuildDeps(pm)) {
9491
10115
  out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
9492
10116
  `);
@@ -9497,8 +10121,8 @@ function buildWhisperFromSource(pm) {
9497
10121
  mkdirSync14(LOCUS_BIN_DIR, { recursive: true });
9498
10122
  out.write(` ${dim2("Cloning whisper.cpp...")}
9499
10123
  `);
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");
10124
+ execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join21(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
10125
+ const srcDir = join21(buildDir, "whisper.cpp");
9502
10126
  const numCpus = cpus().length || 2;
9503
10127
  out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
9504
10128
  `);
@@ -9512,13 +10136,13 @@ function buildWhisperFromSource(pm) {
9512
10136
  stdio: ["pipe", "pipe", "pipe"],
9513
10137
  timeout: 600000
9514
10138
  });
9515
- const destPath = join20(LOCUS_BIN_DIR, "whisper-cli");
10139
+ const destPath = join21(LOCUS_BIN_DIR, "whisper-cli");
9516
10140
  const binaryCandidates = [
9517
- join20(srcDir, "build", "bin", "whisper-cli"),
9518
- join20(srcDir, "build", "bin", "main")
10141
+ join21(srcDir, "build", "bin", "whisper-cli"),
10142
+ join21(srcDir, "build", "bin", "main")
9519
10143
  ];
9520
10144
  for (const candidate of binaryCandidates) {
9521
- if (existsSync19(candidate)) {
10145
+ if (existsSync20(candidate)) {
9522
10146
  execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
9523
10147
  stdio: "pipe"
9524
10148
  });
@@ -9597,7 +10221,7 @@ ${bold2("Installing voice dependencies...")}
9597
10221
  }
9598
10222
  function downloadModel() {
9599
10223
  const modelPath = getWhisperModelPath();
9600
- if (existsSync19(modelPath))
10224
+ if (existsSync20(modelPath))
9601
10225
  return true;
9602
10226
  mkdirSync14(WHISPER_MODELS_DIR, { recursive: true });
9603
10227
  const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
@@ -9644,7 +10268,7 @@ class VoiceController {
9644
10268
  onStateChange;
9645
10269
  constructor(options) {
9646
10270
  this.onStateChange = options.onStateChange;
9647
- this.tempFile = join20(tmpdir5(), `locus-voice-${process.pid}.wav`);
10271
+ this.tempFile = join21(tmpdir5(), `locus-voice-${process.pid}.wav`);
9648
10272
  this.deps = checkDependencies();
9649
10273
  }
9650
10274
  getState() {
@@ -9680,7 +10304,7 @@ class VoiceController {
9680
10304
  return false;
9681
10305
  }
9682
10306
  const spawnArgs = binary === "rec" ? args : ["-d", ...args];
9683
- this.recordProcess = spawn6(binary, spawnArgs, {
10307
+ this.recordProcess = spawn7(binary, spawnArgs, {
9684
10308
  stdio: ["pipe", "pipe", "pipe"]
9685
10309
  });
9686
10310
  this.recordProcess.on("error", (err) => {
@@ -9712,7 +10336,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
9712
10336
  this.recordProcess = null;
9713
10337
  this.setState("idle");
9714
10338
  await sleep2(200);
9715
- if (!existsSync19(this.tempFile)) {
10339
+ if (!existsSync20(this.tempFile)) {
9716
10340
  return null;
9717
10341
  }
9718
10342
  try {
@@ -9747,7 +10371,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
9747
10371
  "--language",
9748
10372
  WHISPER_MODEL.endsWith(".en") ? "en" : "auto"
9749
10373
  ];
9750
- const proc = spawn6(binary, args, {
10374
+ const proc = spawn7(binary, args, {
9751
10375
  stdio: ["pipe", "pipe", "pipe"]
9752
10376
  });
9753
10377
  let stdout = "";
@@ -9803,8 +10427,8 @@ function sleep2(ms) {
9803
10427
  var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
9804
10428
  var init_voice = __esm(() => {
9805
10429
  init_terminal();
9806
- WHISPER_MODELS_DIR = join20(homedir4(), ".locus", "whisper-models");
9807
- LOCUS_BIN_DIR = join20(homedir4(), ".locus", "bin");
10430
+ WHISPER_MODELS_DIR = join21(homedir4(), ".locus", "whisper-models");
10431
+ LOCUS_BIN_DIR = join21(homedir4(), ".locus", "bin");
9808
10432
  });
9809
10433
 
9810
10434
  // src/repl/repl.ts
@@ -9842,6 +10466,16 @@ async function startRepl(options) {
9842
10466
  }
9843
10467
  if (options.prompt) {
9844
10468
  await executeOneShotPrompt(options.prompt, session, sessionManager, options);
10469
+ if (session.messages.length >= 2) {
10470
+ const log = getLogger();
10471
+ const transcript = prepareTranscript(session.messages);
10472
+ captureMemoryFromSession(projectRoot, transcript, {
10473
+ model: config.ai?.model
10474
+ }).then((result) => {
10475
+ if (result.captured > 0)
10476
+ log.info(`Captured ${result.captured} memory entries`);
10477
+ }).catch(() => {});
10478
+ }
9845
10479
  return;
9846
10480
  }
9847
10481
  if (!process.stdin.isTTY) {
@@ -9906,6 +10540,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
9906
10540
  });
9907
10541
  printWelcome(session);
9908
10542
  let shouldExit = false;
10543
+ let wasInterrupted = false;
9909
10544
  let currentProvider = inferProviderFromModel(config.ai.model) || config.ai.provider;
9910
10545
  let currentModel = config.ai.model;
9911
10546
  let verbose = true;
@@ -10021,6 +10656,7 @@ ${red2("✗")} ${msg}
10021
10656
  break;
10022
10657
  }
10023
10658
  case "interrupt":
10659
+ wasInterrupted = true;
10024
10660
  shouldExit = true;
10025
10661
  break;
10026
10662
  case "exit":
@@ -10031,6 +10667,16 @@ ${red2("✗")} ${msg}
10031
10667
  }
10032
10668
  }
10033
10669
  voice.cancel();
10670
+ if (!wasInterrupted && session.messages.length >= 2) {
10671
+ const log = getLogger();
10672
+ const transcript = prepareTranscript(session.messages);
10673
+ captureMemoryFromSession(projectRoot, transcript, {
10674
+ model: config.ai?.model
10675
+ }).then((result) => {
10676
+ if (result.captured > 0)
10677
+ log.info(`Captured ${result.captured} memory entries`);
10678
+ }).catch(() => {});
10679
+ }
10034
10680
  const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
10035
10681
  if (shouldPersistOnExit) {
10036
10682
  sessionManager.save(session);
@@ -10107,6 +10753,8 @@ var init_repl = __esm(() => {
10107
10753
  init_run_ai();
10108
10754
  init_runner();
10109
10755
  init_ai_models();
10756
+ init_logger();
10757
+ init_memory_capture();
10110
10758
  init_prompt_builder();
10111
10759
  init_sandbox();
10112
10760
  init_terminal();
@@ -10316,8 +10964,8 @@ var init_exec = __esm(() => {
10316
10964
 
10317
10965
  // src/core/submodule.ts
10318
10966
  import { execSync as execSync13 } from "node:child_process";
10319
- import { existsSync as existsSync20 } from "node:fs";
10320
- import { join as join21 } from "node:path";
10967
+ import { existsSync as existsSync21 } from "node:fs";
10968
+ import { join as join22 } from "node:path";
10321
10969
  function git2(args, cwd) {
10322
10970
  return execSync13(`git ${args}`, {
10323
10971
  cwd,
@@ -10333,7 +10981,7 @@ function gitSafe(args, cwd) {
10333
10981
  }
10334
10982
  }
10335
10983
  function hasSubmodules(cwd) {
10336
- return existsSync20(join21(cwd, ".gitmodules"));
10984
+ return existsSync21(join22(cwd, ".gitmodules"));
10337
10985
  }
10338
10986
  function listSubmodules(cwd) {
10339
10987
  if (!hasSubmodules(cwd))
@@ -10353,7 +11001,7 @@ function listSubmodules(cwd) {
10353
11001
  continue;
10354
11002
  submodules.push({
10355
11003
  path,
10356
- absolutePath: join21(cwd, path),
11004
+ absolutePath: join22(cwd, path),
10357
11005
  dirty
10358
11006
  });
10359
11007
  }
@@ -10366,7 +11014,7 @@ function getDirtySubmodules(cwd) {
10366
11014
  const submodules = listSubmodules(cwd);
10367
11015
  const dirty = [];
10368
11016
  for (const sub of submodules) {
10369
- if (!existsSync20(sub.absolutePath))
11017
+ if (!existsSync21(sub.absolutePath))
10370
11018
  continue;
10371
11019
  const status = gitSafe("status --porcelain", sub.absolutePath);
10372
11020
  if (status && status.trim().length > 0) {
@@ -10453,7 +11101,7 @@ function pushSubmoduleBranches(cwd) {
10453
11101
  const log = getLogger();
10454
11102
  const submodules = listSubmodules(cwd);
10455
11103
  for (const sub of submodules) {
10456
- if (!existsSync20(sub.absolutePath))
11104
+ if (!existsSync21(sub.absolutePath))
10457
11105
  continue;
10458
11106
  const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
10459
11107
  if (!branch || branch === "HEAD")
@@ -10605,6 +11253,19 @@ ${summary}
10605
11253
  Duration: ${timer.formatted()}${prNumber ? `
10606
11254
  PR: #${prNumber}` : ""}`, { cwd: projectRoot });
10607
11255
  } catch {}
11256
+ const transcript = prepareTranscript([
11257
+ {
11258
+ role: "user",
11259
+ content: `Issue #${issueNumber}: ${issue.title}
11260
+
11261
+ ${issue.body}`
11262
+ },
11263
+ { role: "assistant", content: output }
11264
+ ]);
11265
+ captureMemoryFromSession(projectRoot, transcript, { model: config.ai?.model }).then((result) => {
11266
+ if (result.captured > 0)
11267
+ log.info(`Captured ${result.captured} memory entries`);
11268
+ }).catch(() => {});
10608
11269
  return {
10609
11270
  issueNumber,
10610
11271
  success: true,
@@ -10786,6 +11447,7 @@ var init_agent = __esm(() => {
10786
11447
  init_config();
10787
11448
  init_github();
10788
11449
  init_logger();
11450
+ init_memory_capture();
10789
11451
  init_prompt_builder();
10790
11452
  init_sandbox();
10791
11453
  init_submodule();
@@ -10923,15 +11585,15 @@ var init_conflict = __esm(() => {
10923
11585
 
10924
11586
  // src/core/run-state.ts
10925
11587
  import {
10926
- existsSync as existsSync21,
11588
+ existsSync as existsSync22,
10927
11589
  mkdirSync as mkdirSync15,
10928
- readFileSync as readFileSync14,
11590
+ readFileSync as readFileSync15,
10929
11591
  unlinkSync as unlinkSync5,
10930
11592
  writeFileSync as writeFileSync11
10931
11593
  } from "node:fs";
10932
- import { dirname as dirname6, join as join22 } from "node:path";
11594
+ import { dirname as dirname6, join as join23 } from "node:path";
10933
11595
  function getRunStateDir(projectRoot) {
10934
- return join22(projectRoot, ".locus", "run-state");
11596
+ return join23(projectRoot, ".locus", "run-state");
10935
11597
  }
10936
11598
  function sprintSlug(name) {
10937
11599
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -10939,16 +11601,16 @@ function sprintSlug(name) {
10939
11601
  function getRunStatePath(projectRoot, sprintName) {
10940
11602
  const dir = getRunStateDir(projectRoot);
10941
11603
  if (sprintName) {
10942
- return join22(dir, `${sprintSlug(sprintName)}.json`);
11604
+ return join23(dir, `${sprintSlug(sprintName)}.json`);
10943
11605
  }
10944
- return join22(dir, "_parallel.json");
11606
+ return join23(dir, "_parallel.json");
10945
11607
  }
10946
11608
  function loadRunState(projectRoot, sprintName) {
10947
11609
  const path = getRunStatePath(projectRoot, sprintName);
10948
- if (!existsSync21(path))
11610
+ if (!existsSync22(path))
10949
11611
  return null;
10950
11612
  try {
10951
- return JSON.parse(readFileSync14(path, "utf-8"));
11613
+ return JSON.parse(readFileSync15(path, "utf-8"));
10952
11614
  } catch {
10953
11615
  getLogger().warn("Corrupted run state file, ignoring");
10954
11616
  return null;
@@ -10957,7 +11619,7 @@ function loadRunState(projectRoot, sprintName) {
10957
11619
  function saveRunState(projectRoot, state) {
10958
11620
  const path = getRunStatePath(projectRoot, state.sprint);
10959
11621
  const dir = dirname6(path);
10960
- if (!existsSync21(dir)) {
11622
+ if (!existsSync22(dir)) {
10961
11623
  mkdirSync15(dir, { recursive: true });
10962
11624
  }
10963
11625
  writeFileSync11(path, `${JSON.stringify(state, null, 2)}
@@ -10965,7 +11627,7 @@ function saveRunState(projectRoot, state) {
10965
11627
  }
10966
11628
  function clearRunState(projectRoot, sprintName) {
10967
11629
  const path = getRunStatePath(projectRoot, sprintName);
10968
- if (existsSync21(path)) {
11630
+ if (existsSync22(path)) {
10969
11631
  unlinkSync5(path);
10970
11632
  }
10971
11633
  }
@@ -11123,23 +11785,23 @@ __export(exports_worktree, {
11123
11785
  import { execSync as execSync16 } from "node:child_process";
11124
11786
  import {
11125
11787
  cpSync as cpSync2,
11126
- existsSync as existsSync22,
11788
+ existsSync as existsSync23,
11127
11789
  mkdirSync as mkdirSync16,
11128
11790
  readdirSync as readdirSync7,
11129
11791
  realpathSync,
11130
11792
  statSync as statSync4
11131
11793
  } from "node:fs";
11132
- import { join as join23 } from "node:path";
11794
+ import { join as join24 } from "node:path";
11133
11795
  function copyLocusDir(projectRoot, worktreePath) {
11134
- const srcLocus = join23(projectRoot, ".locus");
11135
- if (!existsSync22(srcLocus))
11796
+ const srcLocus = join24(projectRoot, ".locus");
11797
+ if (!existsSync23(srcLocus))
11136
11798
  return;
11137
- const destLocus = join23(worktreePath, ".locus");
11799
+ const destLocus = join24(worktreePath, ".locus");
11138
11800
  mkdirSync16(destLocus, { recursive: true });
11139
11801
  for (const entry of readdirSync7(srcLocus)) {
11140
11802
  if (entry === "worktrees")
11141
11803
  continue;
11142
- cpSync2(join23(srcLocus, entry), join23(destLocus, entry), { recursive: true });
11804
+ cpSync2(join24(srcLocus, entry), join24(destLocus, entry), { recursive: true });
11143
11805
  }
11144
11806
  }
11145
11807
  function git4(args, cwd) {
@@ -11157,13 +11819,13 @@ function gitSafe3(args, cwd) {
11157
11819
  }
11158
11820
  }
11159
11821
  function getWorktreeDir(projectRoot) {
11160
- return join23(projectRoot, ".locus", "worktrees");
11822
+ return join24(projectRoot, ".locus", "worktrees");
11161
11823
  }
11162
11824
  function getWorktreePath(projectRoot, issueNumber) {
11163
- return join23(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
11825
+ return join24(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
11164
11826
  }
11165
11827
  function getSprintWorktreePath(projectRoot, sprintSlug2) {
11166
- return join23(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
11828
+ return join24(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
11167
11829
  }
11168
11830
  function generateBranchName(issueNumber) {
11169
11831
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -11176,7 +11838,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
11176
11838
  const log = getLogger();
11177
11839
  const slug = sprintSlug2(sprintName);
11178
11840
  const worktreePath = getSprintWorktreePath(projectRoot, slug);
11179
- if (existsSync22(worktreePath)) {
11841
+ if (existsSync23(worktreePath)) {
11180
11842
  log.verbose(`Sprint worktree already exists for "${sprintName}"`);
11181
11843
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
11182
11844
  return { path: worktreePath, branch: existingBranch };
@@ -11197,7 +11859,7 @@ function removeSprintWorktree(projectRoot, sprintName) {
11197
11859
  const log = getLogger();
11198
11860
  const slug = sprintSlug2(sprintName);
11199
11861
  const worktreePath = getSprintWorktreePath(projectRoot, slug);
11200
- if (!existsSync22(worktreePath)) {
11862
+ if (!existsSync23(worktreePath)) {
11201
11863
  log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
11202
11864
  return;
11203
11865
  }
@@ -11227,7 +11889,7 @@ function getWorktreeBranch(worktreePath) {
11227
11889
  function createWorktree(projectRoot, issueNumber, baseBranch) {
11228
11890
  const log = getLogger();
11229
11891
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11230
- if (existsSync22(worktreePath)) {
11892
+ if (existsSync23(worktreePath)) {
11231
11893
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
11232
11894
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
11233
11895
  return {
@@ -11256,7 +11918,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
11256
11918
  function removeWorktree(projectRoot, issueNumber) {
11257
11919
  const log = getLogger();
11258
11920
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11259
- if (!existsSync22(worktreePath)) {
11921
+ if (!existsSync23(worktreePath)) {
11260
11922
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
11261
11923
  return;
11262
11924
  }
@@ -11275,7 +11937,7 @@ function removeWorktree(projectRoot, issueNumber) {
11275
11937
  function listWorktrees(projectRoot) {
11276
11938
  const log = getLogger();
11277
11939
  const worktreeDir = getWorktreeDir(projectRoot);
11278
- if (!existsSync22(worktreeDir)) {
11940
+ if (!existsSync23(worktreeDir)) {
11279
11941
  return [];
11280
11942
  }
11281
11943
  const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
@@ -11295,7 +11957,7 @@ function listWorktrees(projectRoot) {
11295
11957
  if (!match)
11296
11958
  continue;
11297
11959
  const issueNumber = Number.parseInt(match[1], 10);
11298
- const path = join23(worktreeDir, entry);
11960
+ const path = join24(worktreeDir, entry);
11299
11961
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
11300
11962
  let resolvedPath;
11301
11963
  try {
@@ -11335,7 +11997,7 @@ function cleanupStaleWorktrees(projectRoot) {
11335
11997
  }
11336
11998
  function pushWorktreeBranch(projectRoot, issueNumber) {
11337
11999
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11338
- if (!existsSync22(worktreePath)) {
12000
+ if (!existsSync23(worktreePath)) {
11339
12001
  throw new Error(`Worktree for issue #${issueNumber} does not exist`);
11340
12002
  }
11341
12003
  const branch = getWorktreeBranch(worktreePath);
@@ -11347,18 +12009,18 @@ function pushWorktreeBranch(projectRoot, issueNumber) {
11347
12009
  }
11348
12010
  function hasWorktreeChanges(projectRoot, issueNumber) {
11349
12011
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11350
- if (!existsSync22(worktreePath))
12012
+ if (!existsSync23(worktreePath))
11351
12013
  return false;
11352
12014
  const status = gitSafe3("status --porcelain", worktreePath);
11353
12015
  return status !== null && status.trim().length > 0;
11354
12016
  }
11355
12017
  function getWorktreeAge(projectRoot, issueNumber) {
11356
12018
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
11357
- if (!existsSync22(worktreePath))
12019
+ if (!existsSync23(worktreePath))
11358
12020
  return 0;
11359
12021
  try {
11360
- const stat = statSync4(worktreePath);
11361
- return Date.now() - stat.ctimeMs;
12022
+ const stat2 = statSync4(worktreePath);
12023
+ return Date.now() - stat2.ctimeMs;
11362
12024
  } catch {
11363
12025
  return 0;
11364
12026
  }
@@ -11374,8 +12036,8 @@ __export(exports_run, {
11374
12036
  runCommand: () => runCommand
11375
12037
  });
11376
12038
  import { execSync as execSync17 } from "node:child_process";
11377
- import { existsSync as existsSync23 } from "node:fs";
11378
- import { join as join24 } from "node:path";
12039
+ import { existsSync as existsSync24 } from "node:fs";
12040
+ import { join as join25 } from "node:path";
11379
12041
  function resolveExecutionContext(config, modelOverride) {
11380
12042
  const model = modelOverride ?? config.ai.model;
11381
12043
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -11933,8 +12595,8 @@ async function handleResume(projectRoot, config, sandboxed, flags) {
11933
12595
  const sprintsToResume = [];
11934
12596
  try {
11935
12597
  const { readdirSync: readdirSync8 } = await import("node:fs");
11936
- const runStateDir = join24(projectRoot, ".locus", "run-state");
11937
- if (existsSync23(runStateDir)) {
12598
+ const runStateDir = join25(projectRoot, ".locus", "run-state");
12599
+ if (existsSync24(runStateDir)) {
11938
12600
  const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
11939
12601
  for (const file of files) {
11940
12602
  const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
@@ -11978,9 +12640,9 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` ($
11978
12640
  let workDir = projectRoot;
11979
12641
  if (state.type === "sprint" && state.sprint) {
11980
12642
  const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
11981
- const { existsSync: existsSync24 } = await import("node:fs");
12643
+ const { existsSync: existsSync25 } = await import("node:fs");
11982
12644
  const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
11983
- if (existsSync24(wtPath)) {
12645
+ if (existsSync25(wtPath)) {
11984
12646
  workDir = wtPath;
11985
12647
  } else if (state.branch) {
11986
12648
  try {
@@ -12210,8 +12872,8 @@ __export(exports_status, {
12210
12872
  statusCommand: () => statusCommand
12211
12873
  });
12212
12874
  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";
12875
+ import { existsSync as existsSync25 } from "node:fs";
12876
+ import { dirname as dirname7, join as join26 } from "node:path";
12215
12877
  async function statusCommand(projectRoot) {
12216
12878
  const config = loadConfig(projectRoot);
12217
12879
  const spinner = new Spinner;
@@ -12343,13 +13005,13 @@ ${drawBox(lines, { title: "Locus Status" })}
12343
13005
  `);
12344
13006
  }
12345
13007
  function getPm2Bin() {
12346
- const pkgsBin = join25(getPackagesDir(), "node_modules", ".bin", "pm2");
12347
- if (existsSync24(pkgsBin))
13008
+ const pkgsBin = join26(getPackagesDir(), "node_modules", ".bin", "pm2");
13009
+ if (existsSync25(pkgsBin))
12348
13010
  return pkgsBin;
12349
13011
  let dir = process.cwd();
12350
13012
  while (dir !== dirname7(dir)) {
12351
- const candidate = join25(dir, "node_modules", ".bin", "pm2");
12352
- if (existsSync24(candidate))
13013
+ const candidate = join26(dir, "node_modules", ".bin", "pm2");
13014
+ if (existsSync25(candidate))
12353
13015
  return candidate;
12354
13016
  dir = dirname7(dir);
12355
13017
  }
@@ -12545,13 +13207,13 @@ __export(exports_plan, {
12545
13207
  parsePlanArgs: () => parsePlanArgs
12546
13208
  });
12547
13209
  import {
12548
- existsSync as existsSync25,
13210
+ existsSync as existsSync26,
12549
13211
  mkdirSync as mkdirSync17,
12550
13212
  readdirSync as readdirSync8,
12551
- readFileSync as readFileSync15,
13213
+ readFileSync as readFileSync16,
12552
13214
  writeFileSync as writeFileSync12
12553
13215
  } from "node:fs";
12554
- import { join as join26 } from "node:path";
13216
+ import { join as join27 } from "node:path";
12555
13217
  function printHelp2() {
12556
13218
  process.stderr.write(`
12557
13219
  ${bold2("locus plan")} — AI-powered sprint planning
@@ -12583,11 +13245,11 @@ function normalizeSprintName(name) {
12583
13245
  return name.trim().toLowerCase();
12584
13246
  }
12585
13247
  function getPlansDir(projectRoot) {
12586
- return join26(projectRoot, ".locus", "plans");
13248
+ return join27(projectRoot, ".locus", "plans");
12587
13249
  }
12588
13250
  function ensurePlansDir(projectRoot) {
12589
13251
  const dir = getPlansDir(projectRoot);
12590
- if (!existsSync25(dir)) {
13252
+ if (!existsSync26(dir)) {
12591
13253
  mkdirSync17(dir, { recursive: true });
12592
13254
  }
12593
13255
  return dir;
@@ -12597,14 +13259,14 @@ function generateId() {
12597
13259
  }
12598
13260
  function loadPlanFile(projectRoot, id) {
12599
13261
  const dir = getPlansDir(projectRoot);
12600
- if (!existsSync25(dir))
13262
+ if (!existsSync26(dir))
12601
13263
  return null;
12602
13264
  const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
12603
13265
  const match = files.find((f) => f.startsWith(id));
12604
13266
  if (!match)
12605
13267
  return null;
12606
13268
  try {
12607
- const content = readFileSync15(join26(dir, match), "utf-8");
13269
+ const content = readFileSync16(join27(dir, match), "utf-8");
12608
13270
  return JSON.parse(content);
12609
13271
  } catch {
12610
13272
  return null;
@@ -12671,7 +13333,7 @@ async function planCommand(projectRoot, args, flags = {}) {
12671
13333
  }
12672
13334
  function handleListPlans(projectRoot) {
12673
13335
  const dir = getPlansDir(projectRoot);
12674
- if (!existsSync25(dir)) {
13336
+ if (!existsSync26(dir)) {
12675
13337
  process.stderr.write(`${dim2("No saved plans yet.")}
12676
13338
  `);
12677
13339
  return;
@@ -12689,7 +13351,7 @@ ${bold2("Saved Plans:")}
12689
13351
  for (const file of files) {
12690
13352
  const id = file.replace(".json", "");
12691
13353
  try {
12692
- const content = readFileSync15(join26(dir, file), "utf-8");
13354
+ const content = readFileSync16(join27(dir, file), "utf-8");
12693
13355
  const plan = JSON.parse(content);
12694
13356
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
12695
13357
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -12757,7 +13419,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
12757
13419
  return;
12758
13420
  }
12759
13421
  const config = loadConfig(projectRoot);
12760
- const planPath = join26(getPlansDir(projectRoot), `${plan.id}.json`);
13422
+ const planPath = join27(getPlansDir(projectRoot), `${plan.id}.json`);
12761
13423
  const planPathRelative = `.locus/plans/${plan.id}.json`;
12762
13424
  process.stderr.write(`
12763
13425
  ${bold2("Refining plan:")} ${cyan2(plan.directive)}
@@ -12796,7 +13458,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
12796
13458
  `);
12797
13459
  return;
12798
13460
  }
12799
- if (!existsSync25(planPath)) {
13461
+ if (!existsSync26(planPath)) {
12800
13462
  process.stderr.write(`
12801
13463
  ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
12802
13464
  `);
@@ -12804,7 +13466,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
12804
13466
  }
12805
13467
  let updatedPlan;
12806
13468
  try {
12807
- const content = readFileSync15(planPath, "utf-8");
13469
+ const content = readFileSync16(planPath, "utf-8");
12808
13470
  updatedPlan = JSON.parse(content);
12809
13471
  } catch {
12810
13472
  process.stderr.write(`
@@ -12894,7 +13556,7 @@ ${bold2("Approving plan:")}
12894
13556
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
12895
13557
  const id = generateId();
12896
13558
  const plansDir = ensurePlansDir(projectRoot);
12897
- const planPath = join26(plansDir, `${id}.json`);
13559
+ const planPath = join27(plansDir, `${id}.json`);
12898
13560
  const planPathRelative = `.locus/plans/${id}.json`;
12899
13561
  const displayDirective = directive;
12900
13562
  process.stderr.write(`
@@ -12937,7 +13599,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
12937
13599
  `);
12938
13600
  return;
12939
13601
  }
12940
- if (!existsSync25(planPath)) {
13602
+ if (!existsSync26(planPath)) {
12941
13603
  process.stderr.write(`
12942
13604
  ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
12943
13605
  `);
@@ -12947,7 +13609,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
12947
13609
  }
12948
13610
  let plan;
12949
13611
  try {
12950
- const content = readFileSync15(planPath, "utf-8");
13612
+ const content = readFileSync16(planPath, "utf-8");
12951
13613
  plan = JSON.parse(content);
12952
13614
  } catch {
12953
13615
  process.stderr.write(`
@@ -13109,6 +13771,18 @@ ${bold2("Suggested Order:")}
13109
13771
  `);
13110
13772
  }
13111
13773
  }
13774
+ function loadPastMemory(projectRoot) {
13775
+ const memoryDir = getMemoryDir(projectRoot);
13776
+ if (existsSync26(memoryDir)) {
13777
+ const content = readAllMemorySync(projectRoot);
13778
+ if (content.trim()) {
13779
+ return content.length > MEMORY_MAX_CHARS2 ? `${content.slice(0, MEMORY_MAX_CHARS2)}
13780
+
13781
+ ...(truncated)` : content;
13782
+ }
13783
+ }
13784
+ return "";
13785
+ }
13112
13786
  function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
13113
13787
  const parts = [];
13114
13788
  parts.push(`<role>
@@ -13119,18 +13793,17 @@ ${directive}${sprintName ? `
13119
13793
 
13120
13794
  **Sprint:** ${sprintName}` : ""}
13121
13795
  </directive>`);
13122
- const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
13123
- if (existsSync25(locusPath)) {
13124
- const content = readFileSync15(locusPath, "utf-8");
13796
+ const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13797
+ if (existsSync26(locusPath)) {
13798
+ const content = readFileSync16(locusPath, "utf-8");
13125
13799
  parts.push(`<project-context>
13126
13800
  ${content.slice(0, 3000)}
13127
13801
  </project-context>`);
13128
13802
  }
13129
- const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
13130
- if (existsSync25(learningsPath)) {
13131
- const content = readFileSync15(learningsPath, "utf-8");
13803
+ const memoryContent = loadPastMemory(projectRoot);
13804
+ if (memoryContent) {
13132
13805
  parts.push(`<past-learnings>
13133
- ${content.slice(0, 2000)}
13806
+ ${memoryContent}
13134
13807
  </past-learnings>`);
13135
13808
  }
13136
13809
  parts.push(`<task>
@@ -13180,9 +13853,9 @@ ${JSON.stringify(plan, null, 2)}
13180
13853
  parts.push(`<feedback>
13181
13854
  ${feedback}
13182
13855
  </feedback>`);
13183
- const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
13184
- if (existsSync25(locusPath)) {
13185
- const content = readFileSync15(locusPath, "utf-8");
13856
+ const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13857
+ if (existsSync26(locusPath)) {
13858
+ const content = readFileSync16(locusPath, "utf-8");
13186
13859
  parts.push(`<project-context>
13187
13860
  ${content.slice(0, 3000)}
13188
13861
  </project-context>`);
@@ -13350,10 +14023,12 @@ ${green("✓")} Created ${planned.length} issues.${milestoneTitle ? ` Sprint: ${
13350
14023
 
13351
14024
  `);
13352
14025
  }
14026
+ var MEMORY_MAX_CHARS2 = 2000;
13353
14027
  var init_plan = __esm(() => {
13354
14028
  init_run_ai();
13355
14029
  init_config();
13356
14030
  init_github();
14031
+ init_memory();
13357
14032
  init_sandbox();
13358
14033
  init_terminal();
13359
14034
  });
@@ -13364,8 +14039,8 @@ __export(exports_review, {
13364
14039
  reviewCommand: () => reviewCommand
13365
14040
  });
13366
14041
  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";
14042
+ import { existsSync as existsSync27, readFileSync as readFileSync17 } from "node:fs";
14043
+ import { join as join28 } from "node:path";
13369
14044
  function printHelp3() {
13370
14045
  process.stderr.write(`
13371
14046
  ${bold2("locus review")} — AI-powered code review
@@ -13534,9 +14209,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
13534
14209
  parts.push(`<role>
13535
14210
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
13536
14211
  </role>`);
13537
- const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
13538
- if (existsSync26(locusPath)) {
13539
- const content = readFileSync16(locusPath, "utf-8");
14212
+ const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
14213
+ if (existsSync27(locusPath)) {
14214
+ const content = readFileSync17(locusPath, "utf-8");
13540
14215
  parts.push(`<project-context>
13541
14216
  ${content.slice(0, 2000)}
13542
14217
  </project-context>`);
@@ -13851,14 +14526,14 @@ __export(exports_discuss, {
13851
14526
  discussCommand: () => discussCommand
13852
14527
  });
13853
14528
  import {
13854
- existsSync as existsSync27,
14529
+ existsSync as existsSync28,
13855
14530
  mkdirSync as mkdirSync18,
13856
14531
  readdirSync as readdirSync9,
13857
- readFileSync as readFileSync17,
14532
+ readFileSync as readFileSync18,
13858
14533
  unlinkSync as unlinkSync6,
13859
14534
  writeFileSync as writeFileSync13
13860
14535
  } from "node:fs";
13861
- import { join as join28 } from "node:path";
14536
+ import { join as join29 } from "node:path";
13862
14537
  function printHelp5() {
13863
14538
  process.stderr.write(`
13864
14539
  ${bold2("locus discuss")} — AI-powered architectural discussions
@@ -13880,11 +14555,11 @@ ${bold2("Examples:")}
13880
14555
  `);
13881
14556
  }
13882
14557
  function getDiscussionsDir(projectRoot) {
13883
- return join28(projectRoot, ".locus", "discussions");
14558
+ return join29(projectRoot, ".locus", "discussions");
13884
14559
  }
13885
14560
  function ensureDiscussionsDir(projectRoot) {
13886
14561
  const dir = getDiscussionsDir(projectRoot);
13887
- if (!existsSync27(dir)) {
14562
+ if (!existsSync28(dir)) {
13888
14563
  mkdirSync18(dir, { recursive: true });
13889
14564
  }
13890
14565
  return dir;
@@ -13919,7 +14594,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
13919
14594
  }
13920
14595
  function listDiscussions(projectRoot) {
13921
14596
  const dir = getDiscussionsDir(projectRoot);
13922
- if (!existsSync27(dir)) {
14597
+ if (!existsSync28(dir)) {
13923
14598
  process.stderr.write(`${dim2("No discussions yet.")}
13924
14599
  `);
13925
14600
  return;
@@ -13936,7 +14611,7 @@ ${bold2("Discussions:")}
13936
14611
  `);
13937
14612
  for (const file of files) {
13938
14613
  const id = file.replace(".md", "");
13939
- const content = readFileSync17(join28(dir, file), "utf-8");
14614
+ const content = readFileSync18(join29(dir, file), "utf-8");
13940
14615
  const titleMatch = content.match(/^#\s+(.+)/m);
13941
14616
  const title = titleMatch ? titleMatch[1] : id;
13942
14617
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -13954,7 +14629,7 @@ function showDiscussion(projectRoot, id) {
13954
14629
  return;
13955
14630
  }
13956
14631
  const dir = getDiscussionsDir(projectRoot);
13957
- if (!existsSync27(dir)) {
14632
+ if (!existsSync28(dir)) {
13958
14633
  process.stderr.write(`${red2("✗")} No discussions found.
13959
14634
  `);
13960
14635
  return;
@@ -13966,7 +14641,7 @@ function showDiscussion(projectRoot, id) {
13966
14641
  `);
13967
14642
  return;
13968
14643
  }
13969
- const content = readFileSync17(join28(dir, match), "utf-8");
14644
+ const content = readFileSync18(join29(dir, match), "utf-8");
13970
14645
  process.stdout.write(`${content}
13971
14646
  `);
13972
14647
  }
@@ -13977,7 +14652,7 @@ function deleteDiscussion(projectRoot, id) {
13977
14652
  return;
13978
14653
  }
13979
14654
  const dir = getDiscussionsDir(projectRoot);
13980
- if (!existsSync27(dir)) {
14655
+ if (!existsSync28(dir)) {
13981
14656
  process.stderr.write(`${red2("✗")} No discussions found.
13982
14657
  `);
13983
14658
  return;
@@ -13989,7 +14664,7 @@ function deleteDiscussion(projectRoot, id) {
13989
14664
  `);
13990
14665
  return;
13991
14666
  }
13992
- unlinkSync6(join28(dir, match));
14667
+ unlinkSync6(join29(dir, match));
13993
14668
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
13994
14669
  `);
13995
14670
  }
@@ -14002,7 +14677,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
14002
14677
  return;
14003
14678
  }
14004
14679
  const dir = getDiscussionsDir(projectRoot);
14005
- if (!existsSync27(dir)) {
14680
+ if (!existsSync28(dir)) {
14006
14681
  process.stderr.write(`${red2("✗")} No discussions found.
14007
14682
  `);
14008
14683
  return;
@@ -14014,7 +14689,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
14014
14689
  `);
14015
14690
  return;
14016
14691
  }
14017
- const content = readFileSync17(join28(dir, match), "utf-8");
14692
+ const content = readFileSync18(join29(dir, match), "utf-8");
14018
14693
  const titleMatch = content.match(/^#\s+(.+)/m);
14019
14694
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
14020
14695
  await planCommand(projectRoot, [
@@ -14140,7 +14815,7 @@ ${turn.content}`;
14140
14815
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
14141
14816
  ].join(`
14142
14817
  `);
14143
- writeFileSync13(join28(dir, `${id}.md`), markdown, "utf-8");
14818
+ writeFileSync13(join29(dir, `${id}.md`), markdown, "utf-8");
14144
14819
  process.stderr.write(`
14145
14820
  ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
14146
14821
  `);
@@ -14150,23 +14825,34 @@ ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
14150
14825
 
14151
14826
  `);
14152
14827
  }
14828
+ function loadPastMemory2(projectRoot) {
14829
+ const memoryDir = getMemoryDir(projectRoot);
14830
+ if (existsSync28(memoryDir)) {
14831
+ const content = readAllMemorySync(projectRoot);
14832
+ if (content.trim()) {
14833
+ return content.length > MEMORY_MAX_CHARS3 ? `${content.slice(0, MEMORY_MAX_CHARS3)}
14834
+
14835
+ ...(truncated)` : content;
14836
+ }
14837
+ }
14838
+ return "";
14839
+ }
14153
14840
  function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
14154
14841
  const parts = [];
14155
14842
  parts.push(`<role>
14156
14843
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
14157
14844
  </role>`);
14158
- const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
14159
- if (existsSync27(locusPath)) {
14160
- const content = readFileSync17(locusPath, "utf-8");
14845
+ const locusPath = join29(projectRoot, ".locus", "LOCUS.md");
14846
+ if (existsSync28(locusPath)) {
14847
+ const content = readFileSync18(locusPath, "utf-8");
14161
14848
  parts.push(`<project-context>
14162
14849
  ${content.slice(0, 3000)}
14163
14850
  </project-context>`);
14164
14851
  }
14165
- const learningsPath = join28(projectRoot, ".locus", "LEARNINGS.md");
14166
- if (existsSync27(learningsPath)) {
14167
- const content = readFileSync17(learningsPath, "utf-8");
14852
+ const memoryContent = loadPastMemory2(projectRoot);
14853
+ if (memoryContent) {
14168
14854
  parts.push(`<past-learnings>
14169
- ${content.slice(0, 2000)}
14855
+ ${memoryContent}
14170
14856
  </past-learnings>`);
14171
14857
  }
14172
14858
  parts.push(`<discussion-topic>
@@ -14215,10 +14901,11 @@ If you still need key information to give a good answer:
14215
14901
 
14216
14902
  `);
14217
14903
  }
14218
- var MAX_DISCUSSION_ROUNDS = 5;
14904
+ var MAX_DISCUSSION_ROUNDS = 5, MEMORY_MAX_CHARS3 = 2000;
14219
14905
  var init_discuss = __esm(() => {
14220
14906
  init_run_ai();
14221
14907
  init_config();
14908
+ init_memory();
14222
14909
  init_sandbox();
14223
14910
  init_progress();
14224
14911
  init_terminal();
@@ -14235,8 +14922,8 @@ __export(exports_artifacts, {
14235
14922
  formatDate: () => formatDate2,
14236
14923
  artifactsCommand: () => artifactsCommand
14237
14924
  });
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";
14925
+ import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync19, statSync as statSync5 } from "node:fs";
14926
+ import { join as join30 } from "node:path";
14240
14927
  function printHelp6() {
14241
14928
  process.stderr.write(`
14242
14929
  ${bold2("locus artifacts")} — View and manage AI-generated artifacts
@@ -14256,37 +14943,37 @@ ${dim2("Artifact names support partial matching.")}
14256
14943
  `);
14257
14944
  }
14258
14945
  function getArtifactsDir(projectRoot) {
14259
- return join29(projectRoot, ".locus", "artifacts");
14946
+ return join30(projectRoot, ".locus", "artifacts");
14260
14947
  }
14261
14948
  function listArtifacts(projectRoot) {
14262
14949
  const dir = getArtifactsDir(projectRoot);
14263
- if (!existsSync28(dir))
14950
+ if (!existsSync29(dir))
14264
14951
  return [];
14265
14952
  return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
14266
- const filePath = join29(dir, fileName);
14267
- const stat = statSync5(filePath);
14953
+ const filePath = join30(dir, fileName);
14954
+ const stat2 = statSync5(filePath);
14268
14955
  return {
14269
14956
  name: fileName.replace(/\.md$/, ""),
14270
14957
  fileName,
14271
- createdAt: stat.birthtime,
14272
- size: stat.size
14958
+ createdAt: stat2.birthtime,
14959
+ size: stat2.size
14273
14960
  };
14274
14961
  }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
14275
14962
  }
14276
14963
  function readArtifact(projectRoot, name) {
14277
14964
  const dir = getArtifactsDir(projectRoot);
14278
14965
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
14279
- const filePath = join29(dir, fileName);
14280
- if (!existsSync28(filePath))
14966
+ const filePath = join30(dir, fileName);
14967
+ if (!existsSync29(filePath))
14281
14968
  return null;
14282
- const stat = statSync5(filePath);
14969
+ const stat2 = statSync5(filePath);
14283
14970
  return {
14284
- content: readFileSync18(filePath, "utf-8"),
14971
+ content: readFileSync19(filePath, "utf-8"),
14285
14972
  info: {
14286
14973
  name: fileName.replace(/\.md$/, ""),
14287
14974
  fileName,
14288
- createdAt: stat.birthtime,
14289
- size: stat.size
14975
+ createdAt: stat2.birthtime,
14976
+ size: stat2.size
14290
14977
  }
14291
14978
  };
14292
14979
  }
@@ -14581,7 +15268,7 @@ Co-Authored-By: LocusAgent <agent@locusai.team>`;
14581
15268
  `);
14582
15269
  }
14583
15270
  }
14584
- function buildCommitPrompt(diff, stat, recentCommits) {
15271
+ function buildCommitPrompt(diff, stat2, recentCommits) {
14585
15272
  const maxDiffLength = 15000;
14586
15273
  const truncatedDiff = diff.length > maxDiffLength ? `${diff.slice(0, maxDiffLength)}
14587
15274
 
@@ -14604,7 +15291,7 @@ ${recentCommits}
14604
15291
  `;
14605
15292
  }
14606
15293
  prompt += `File summary:
14607
- ${stat}
15294
+ ${stat2}
14608
15295
 
14609
15296
  Diff:
14610
15297
  ${truncatedDiff}`;
@@ -14638,10 +15325,10 @@ __export(exports_sandbox2, {
14638
15325
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
14639
15326
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
14640
15327
  });
14641
- import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
15328
+ import { execSync as execSync22, spawn as spawn8 } from "node:child_process";
14642
15329
  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";
15330
+ import { existsSync as existsSync30, readFileSync as readFileSync20 } from "node:fs";
15331
+ import { basename as basename4, join as join31 } from "node:path";
14645
15332
  import { createInterface as createInterface3 } from "node:readline";
14646
15333
  function printSandboxHelp() {
14647
15334
  process.stderr.write(`
@@ -14830,7 +15517,7 @@ async function handleAgentLogin(projectRoot, agent) {
14830
15517
  process.stderr.write(`${dim2("Login and then exit when ready.")}
14831
15518
 
14832
15519
  `);
14833
- const child = spawn7("docker", [
15520
+ const child = spawn8("docker", [
14834
15521
  "sandbox",
14835
15522
  "exec",
14836
15523
  "--privileged",
@@ -15171,7 +15858,7 @@ async function handleLogs(projectRoot, args) {
15171
15858
  }
15172
15859
  function detectPackageManager2(projectRoot) {
15173
15860
  try {
15174
- const raw = readFileSync19(join30(projectRoot, "package.json"), "utf-8");
15861
+ const raw = readFileSync20(join31(projectRoot, "package.json"), "utf-8");
15175
15862
  const pkgJson = JSON.parse(raw);
15176
15863
  if (typeof pkgJson.packageManager === "string") {
15177
15864
  const name = pkgJson.packageManager.split("@")[0];
@@ -15180,13 +15867,13 @@ function detectPackageManager2(projectRoot) {
15180
15867
  }
15181
15868
  }
15182
15869
  } catch {}
15183
- if (existsSync29(join30(projectRoot, "bun.lock")) || existsSync29(join30(projectRoot, "bun.lockb"))) {
15870
+ if (existsSync30(join31(projectRoot, "bun.lock")) || existsSync30(join31(projectRoot, "bun.lockb"))) {
15184
15871
  return "bun";
15185
15872
  }
15186
- if (existsSync29(join30(projectRoot, "yarn.lock"))) {
15873
+ if (existsSync30(join31(projectRoot, "yarn.lock"))) {
15187
15874
  return "yarn";
15188
15875
  }
15189
- if (existsSync29(join30(projectRoot, "pnpm-lock.yaml"))) {
15876
+ if (existsSync30(join31(projectRoot, "pnpm-lock.yaml"))) {
15190
15877
  return "pnpm";
15191
15878
  }
15192
15879
  return "npm";
@@ -15289,9 +15976,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
15289
15976
  ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
15290
15977
  `);
15291
15978
  }
15292
- const setupScript = join30(projectRoot, ".locus", "sandbox-setup.sh");
15293
- const containerSetupScript = containerWorkdir ? join30(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
15294
- if (existsSync29(setupScript)) {
15979
+ const setupScript = join31(projectRoot, ".locus", "sandbox-setup.sh");
15980
+ const containerSetupScript = containerWorkdir ? join31(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
15981
+ if (existsSync30(setupScript)) {
15295
15982
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
15296
15983
  `);
15297
15984
  const hookOk = await runInteractiveCommand("docker", [
@@ -15373,7 +16060,7 @@ function getActiveProviderSandbox(projectRoot, provider) {
15373
16060
  }
15374
16061
  function runInteractiveCommand(command, args) {
15375
16062
  return new Promise((resolve2) => {
15376
- const child = spawn7(command, args, { stdio: "inherit" });
16063
+ const child = spawn8(command, args, { stdio: "inherit" });
15377
16064
  child.on("close", (code) => resolve2(code === 0));
15378
16065
  child.on("error", () => resolve2(false));
15379
16066
  });
@@ -15458,23 +16145,295 @@ var init_sandbox2 = __esm(() => {
15458
16145
  PROVIDERS = ["claude", "codex"];
15459
16146
  });
15460
16147
 
16148
+ // src/commands/memory.ts
16149
+ var exports_memory = {};
16150
+ __export(exports_memory, {
16151
+ memoryCommand: () => memoryCommand
16152
+ });
16153
+ import { existsSync as existsSync31 } from "node:fs";
16154
+ import { writeFile as writeFile2 } from "node:fs/promises";
16155
+ import { join as join32 } from "node:path";
16156
+ import { createInterface as createInterface4 } from "node:readline";
16157
+ function printHelp7() {
16158
+ process.stderr.write(`
16159
+ ${bold2("locus memory")} — Inspect, search, and manage structured memory
16160
+
16161
+ ${bold2("Usage:")}
16162
+ locus memory list [--category <name>] List all memory entries
16163
+ locus memory search <query> Search entries by keyword
16164
+ locus memory stats Show per-category statistics
16165
+ locus memory reset [--confirm] Clear all entries (preserve headers)
16166
+ locus memory migrate Migrate legacy LEARNINGS.md → memory/
16167
+
16168
+ ${bold2("Categories:")}
16169
+ architecture, conventions, decisions, preferences, debugging
16170
+
16171
+ ${bold2("Examples:")}
16172
+ locus memory list ${dim2("# Show all entries")}
16173
+ locus memory list --category debugging ${dim2("# Show only debugging entries")}
16174
+ locus memory search "sandbox" ${dim2("# Search for 'sandbox'")}
16175
+ locus memory stats ${dim2("# Show entry counts and sizes")}
16176
+ locus memory reset --confirm ${dim2("# Clear all entries")}
16177
+ locus memory migrate ${dim2("# Migrate legacy LEARNINGS.md")}
16178
+ `);
16179
+ }
16180
+ async function memoryCommand(projectRoot, args) {
16181
+ const subcommand = args[0];
16182
+ if (!subcommand || subcommand === "help") {
16183
+ printHelp7();
16184
+ return;
16185
+ }
16186
+ const memoryDir = getMemoryDir(projectRoot);
16187
+ if (!existsSync31(memoryDir) && subcommand !== "migrate") {
16188
+ process.stderr.write(`${red2("✗")} Memory directory not found at ${dim2(".locus/memory/")}
16189
+ `);
16190
+ process.stderr.write(` Run ${bold2("locus init")} to create the memory directory.
16191
+ `);
16192
+ process.exit(1);
16193
+ }
16194
+ switch (subcommand) {
16195
+ case "list":
16196
+ await handleList(projectRoot, args.slice(1));
16197
+ break;
16198
+ case "search":
16199
+ await handleSearch(projectRoot, args.slice(1));
16200
+ break;
16201
+ case "stats":
16202
+ await handleStats(projectRoot);
16203
+ break;
16204
+ case "reset":
16205
+ await handleReset(projectRoot, args.slice(1));
16206
+ break;
16207
+ case "migrate":
16208
+ await handleMigrate(projectRoot);
16209
+ break;
16210
+ default:
16211
+ process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
16212
+ `);
16213
+ process.stderr.write(` Run ${bold2("locus memory help")} for available subcommands.
16214
+ `);
16215
+ process.exit(1);
16216
+ }
16217
+ }
16218
+ async function handleList(projectRoot, args) {
16219
+ let categoryFilter;
16220
+ const catIdx = args.indexOf("--category");
16221
+ if (catIdx !== -1) {
16222
+ categoryFilter = args[catIdx + 1];
16223
+ if (!categoryFilter || !MEMORY_CATEGORIES[categoryFilter]) {
16224
+ const valid = Object.keys(MEMORY_CATEGORIES).join(", ");
16225
+ process.stderr.write(`${red2("✗")} Invalid category. Valid categories: ${valid}
16226
+ `);
16227
+ process.exit(1);
16228
+ }
16229
+ }
16230
+ const categories = categoryFilter ? [categoryFilter] : Object.keys(MEMORY_CATEGORIES);
16231
+ let totalEntries = 0;
16232
+ for (const category of categories) {
16233
+ const content = await readMemoryFile(projectRoot, category);
16234
+ const entries = parseEntries(content);
16235
+ if (entries.length === 0)
16236
+ continue;
16237
+ const meta = MEMORY_CATEGORIES[category];
16238
+ process.stderr.write(`
16239
+ ${bold2(cyan2(meta.title))} ${dim2(`(${meta.description})`)}
16240
+ `);
16241
+ for (const entry of entries) {
16242
+ process.stderr.write(` ${entry}
16243
+ `);
16244
+ }
16245
+ totalEntries += entries.length;
16246
+ }
16247
+ if (totalEntries === 0) {
16248
+ process.stderr.write(`
16249
+ ${dim2("No memory entries found.")}
16250
+ `);
16251
+ } else {
16252
+ process.stderr.write(`
16253
+ ${dim2(`${totalEntries} entries total`)}
16254
+ `);
16255
+ }
16256
+ }
16257
+ async function handleSearch(projectRoot, args) {
16258
+ const query = args.join(" ").trim();
16259
+ if (!query) {
16260
+ process.stderr.write(`${red2("✗")} Usage: locus memory search <query>
16261
+ `);
16262
+ process.exit(1);
16263
+ }
16264
+ const queryLower = query.toLowerCase();
16265
+ let matchCount = 0;
16266
+ for (const [category, meta] of Object.entries(MEMORY_CATEGORIES)) {
16267
+ const content = await readMemoryFile(projectRoot, category);
16268
+ const entries = parseEntries(content);
16269
+ const matches = entries.filter((entry) => entry.toLowerCase().includes(queryLower));
16270
+ if (matches.length === 0)
16271
+ continue;
16272
+ process.stderr.write(`
16273
+ ${bold2(cyan2(meta.title))}
16274
+ `);
16275
+ for (const entry of matches) {
16276
+ const highlighted = highlightMatch(entry, query);
16277
+ process.stderr.write(` ${highlighted}
16278
+ `);
16279
+ }
16280
+ matchCount += matches.length;
16281
+ }
16282
+ if (matchCount === 0) {
16283
+ process.stderr.write(`
16284
+ ${dim2("No matches found for")} "${query}"
16285
+ `);
16286
+ } else {
16287
+ process.stderr.write(`
16288
+ ${green(`${matchCount} match${matchCount === 1 ? "" : "es"}`)} found for "${query}"
16289
+ `);
16290
+ }
16291
+ }
16292
+ async function handleStats(projectRoot) {
16293
+ const stats = await getMemoryStats(projectRoot);
16294
+ process.stderr.write(`
16295
+ ${bold2("Memory Statistics")}
16296
+
16297
+ `);
16298
+ let totalEntries = 0;
16299
+ let totalSize = 0;
16300
+ for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
16301
+ const s = stats[key];
16302
+ const countStr = String(s.count).padStart(3);
16303
+ const sizeStr = formatSize2(s.size).padStart(8);
16304
+ const dateStr = s.size > 0 ? dim2(s.lastModified.toLocaleDateString("en-US", {
16305
+ month: "short",
16306
+ day: "numeric",
16307
+ year: "numeric"
16308
+ })) : dim2("—");
16309
+ const icon = s.count > 0 ? green("●") : dim2("○");
16310
+ process.stderr.write(` ${icon} ${cyan2(meta.title.padEnd(14))} ${countStr} entries ${sizeStr} ${dateStr}
16311
+ `);
16312
+ totalEntries += s.count;
16313
+ totalSize += s.size;
16314
+ }
16315
+ process.stderr.write(`
16316
+ ${bold2("Total:")} ${totalEntries} entries, ${formatSize2(totalSize)}
16317
+ `);
16318
+ process.stderr.write(`
16319
+ `);
16320
+ }
16321
+ async function handleReset(projectRoot, args) {
16322
+ const confirmed = args.includes("--confirm");
16323
+ if (!confirmed) {
16324
+ const rl = createInterface4({
16325
+ input: process.stdin,
16326
+ output: process.stderr
16327
+ });
16328
+ const answer = await new Promise((resolve2) => {
16329
+ rl.question(`${yellow2("⚠")} This will clear all memory entries. Continue? (y/N) `, (ans) => {
16330
+ rl.close();
16331
+ resolve2(ans.trim().toLowerCase());
16332
+ });
16333
+ });
16334
+ if (answer !== "y" && answer !== "yes") {
16335
+ process.stderr.write(`${dim2("Cancelled.")}
16336
+ `);
16337
+ return;
16338
+ }
16339
+ }
16340
+ const stats = await getMemoryStats(projectRoot);
16341
+ const memoryDir = getMemoryDir(projectRoot);
16342
+ let totalCleared = 0;
16343
+ for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
16344
+ const count = stats[key].count;
16345
+ const filePath = join32(memoryDir, meta.file);
16346
+ const header = `# ${meta.title}
16347
+
16348
+ ${meta.description}
16349
+
16350
+ `;
16351
+ await writeFile2(filePath, header, "utf-8");
16352
+ if (count > 0) {
16353
+ process.stderr.write(` ${dim2("○")} ${meta.title}: cleared ${count} entries
16354
+ `);
16355
+ totalCleared += count;
16356
+ }
16357
+ }
16358
+ if (totalCleared > 0) {
16359
+ process.stderr.write(`
16360
+ ${green("✓")} Cleared ${totalCleared} entries (file headers preserved)
16361
+ `);
16362
+ } else {
16363
+ process.stderr.write(`${dim2("No entries to clear.")}
16364
+ `);
16365
+ }
16366
+ }
16367
+ async function handleMigrate(projectRoot) {
16368
+ const learningsPath = join32(projectRoot, ".locus", "LEARNINGS.md");
16369
+ if (!existsSync31(learningsPath)) {
16370
+ process.stderr.write(`${dim2("○")} No LEARNINGS.md found — nothing to migrate.
16371
+ `);
16372
+ return;
16373
+ }
16374
+ process.stderr.write(`Migrating entries from LEARNINGS.md...
16375
+ `);
16376
+ const result = await migrateFromLearnings(projectRoot);
16377
+ if (result.migrated > 0) {
16378
+ process.stderr.write(`${green("✓")} Migrated ${result.migrated} entries to .locus/memory/
16379
+ `);
16380
+ }
16381
+ if (result.skipped > 0) {
16382
+ process.stderr.write(`${dim2("○")} Skipped ${result.skipped} duplicate entries
16383
+ `);
16384
+ }
16385
+ if (result.migrated > 0 || result.skipped > 0) {
16386
+ process.stderr.write(`${green("✓")} Removed legacy LEARNINGS.md
16387
+ `);
16388
+ }
16389
+ if (result.migrated === 0 && result.skipped === 0) {
16390
+ process.stderr.write(`${dim2("○")} No entries found in LEARNINGS.md to migrate.
16391
+ `);
16392
+ }
16393
+ }
16394
+ function parseEntries(content) {
16395
+ if (!content)
16396
+ return [];
16397
+ return content.split(`
16398
+ `).filter((line) => line.startsWith("- "));
16399
+ }
16400
+ function highlightMatch(text, query) {
16401
+ const regex = new RegExp(`(${escapeRegex(query)})`, "gi");
16402
+ return text.replace(regex, (match) => bold2(match));
16403
+ }
16404
+ function escapeRegex(str) {
16405
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16406
+ }
16407
+ function formatSize2(bytes) {
16408
+ if (bytes === 0)
16409
+ return "0 B";
16410
+ if (bytes < 1024)
16411
+ return `${bytes} B`;
16412
+ const kb = bytes / 1024;
16413
+ return `${kb.toFixed(1)} KB`;
16414
+ }
16415
+ var init_memory2 = __esm(() => {
16416
+ init_memory();
16417
+ init_terminal();
16418
+ });
16419
+
15461
16420
  // src/cli.ts
15462
16421
  init_config();
15463
16422
  init_context();
15464
16423
  init_logger();
15465
16424
  init_rate_limiter();
15466
16425
  init_terminal();
15467
- import { existsSync as existsSync30, readFileSync as readFileSync20 } from "node:fs";
15468
- import { join as join31 } from "node:path";
16426
+ import { existsSync as existsSync32, readFileSync as readFileSync21 } from "node:fs";
16427
+ import { join as join33 } from "node:path";
15469
16428
  import { fileURLToPath } from "node:url";
15470
16429
  function getCliVersion() {
15471
16430
  const fallbackVersion = "0.0.0";
15472
- const packageJsonPath = join31(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
15473
- if (!existsSync30(packageJsonPath)) {
16431
+ const packageJsonPath = join33(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
16432
+ if (!existsSync32(packageJsonPath)) {
15474
16433
  return fallbackVersion;
15475
16434
  }
15476
16435
  try {
15477
- const parsed = JSON.parse(readFileSync20(packageJsonPath, "utf-8"));
16436
+ const parsed = JSON.parse(readFileSync21(packageJsonPath, "utf-8"));
15478
16437
  return parsed.version ?? fallbackVersion;
15479
16438
  } catch {
15480
16439
  return fallbackVersion;
@@ -15636,7 +16595,7 @@ function printLogo() {
15636
16595
  `);
15637
16596
  }
15638
16597
  }
15639
- function printHelp7() {
16598
+ function printHelp8() {
15640
16599
  printLogo();
15641
16600
  process.stderr.write(`
15642
16601
 
@@ -15664,6 +16623,7 @@ ${bold2("Commands:")}
15664
16623
  ${cyan2("packages")} Manage installed packages (list, outdated)
15665
16624
  ${cyan2("pkg")} ${dim2("<name> [cmd]")} Run a command from an installed package
15666
16625
  ${cyan2("skills")} Discover and manage agent skills
16626
+ ${cyan2("memory")} Inspect, search, and manage memory
15667
16627
  ${cyan2("sandbox")} Manage Docker sandbox lifecycle
15668
16628
  ${cyan2("upgrade")} Check for and install updates
15669
16629
 
@@ -15746,7 +16706,7 @@ async function main() {
15746
16706
  process.exit(0);
15747
16707
  }
15748
16708
  if (parsed.flags.help && !parsed.command) {
15749
- printHelp7();
16709
+ printHelp8();
15750
16710
  process.exit(0);
15751
16711
  }
15752
16712
  const command = resolveAlias(parsed.command);
@@ -15757,7 +16717,7 @@ async function main() {
15757
16717
  try {
15758
16718
  const root = getGitRoot(cwd);
15759
16719
  if (isInitialized(root)) {
15760
- logDir = join31(root, ".locus", "logs");
16720
+ logDir = join33(root, ".locus", "logs");
15761
16721
  getRateLimiter(root);
15762
16722
  }
15763
16723
  } catch {}
@@ -15778,7 +16738,7 @@ async function main() {
15778
16738
  printVersionNotice = startVersionCheck2(VERSION);
15779
16739
  }
15780
16740
  if (!command) {
15781
- printHelp7();
16741
+ printHelp8();
15782
16742
  process.exit(0);
15783
16743
  }
15784
16744
  if (command === "init") {
@@ -15974,6 +16934,12 @@ async function main() {
15974
16934
  await sandboxCommand2(projectRoot, sandboxArgs);
15975
16935
  break;
15976
16936
  }
16937
+ case "memory": {
16938
+ const { memoryCommand: memoryCommand2 } = await Promise.resolve().then(() => (init_memory2(), exports_memory));
16939
+ const memoryArgs = parsed.flags.help ? ["help"] : parsed.args;
16940
+ await memoryCommand2(projectRoot, memoryArgs);
16941
+ break;
16942
+ }
15977
16943
  case "upgrade": {
15978
16944
  const { upgradeCommand: upgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade(), exports_upgrade));
15979
16945
  await upgradeCommand2(projectRoot, parsed.args, {