@virsanghavi/axis-server 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,40 +23,35 @@
23
23
 
24
24
  ---
25
25
 
26
- ## Agent Behavioral Norms
26
+ ## Agent Behavioral Norms (MANDATORY)
27
27
 
28
- ### Plan Before WriteThe Core Invariant
28
+ These norms apply to all AI coding agents. They are not optional or suggestive execute them autonomously.
29
29
 
30
- **No agent writes code unless it either owns a file lock OR has explicitly declined the job board for a scoped reason.**
30
+ ### Autonomous Execution
31
31
 
32
- On non-trivial tasks (2+ files, new features, refactors):
33
- 1. Break work into jobs`post_job`
34
- 2. Claim before editing `claim_next_job`
35
- 3. Lock before writing → `propose_file_access` with a **descriptive intent**
36
- 4. Complete when done → `complete_job` with outcome
32
+ - **NEVER** wait for the user to say "use Axis" or "post a job" or "lock the file." Just do it.
33
+ - **NEVER** pause after analysis to ask permission to proceed. Identify issues start fixing immediately.
34
+ - **NEVER** leave locks or jobs dangling. Complete jobs and finalize sessions without being told.
35
+ - **IMMEDIATELY** call `get_project_soul` as the first action in any session.
37
36
 
38
- Direct edits without a job are allowed only for:
39
- - Single-line fixes, typos, config tweaks
40
- - Clearly scoped changes the user asked for directly
37
+ ### Plan Before Write
41
38
 
42
- ### Force Unlock Policy
39
+ **No agent writes code unless it either owns a file lock OR has explicitly declined the job board for a scoped reason.**
43
40
 
44
- `force_unlock` is a **last resort, not a convenience tool.**
41
+ On non-trivial tasks (2+ files, new features, refactors):
42
+ 1. `post_job` — break into trackable jobs (do this IMMEDIATELY, not after being asked)
43
+ 2. `claim_next_job` — claim before starting
44
+ 3. `propose_file_access` — lock with a **descriptive intent**
45
+ 4. `complete_job` — report outcome when done (this releases the lock)
45
46
 
46
- Rules:
47
- 1. **Never** call `force_unlock` on a file you didn't lock unless:
48
- - The lock has been held for >25 minutes (close to TTL expiry), AND
49
- - The locking agent is clearly not responding or has crashed
50
- 2. **Always** provide a specific reason (e.g. "Agent claude-code crashed 20 minutes ago, lock on auth.ts is blocking progress")
51
- 3. **Never** force-unlock to skip coordination. If another agent holds a lock, work on something else.
52
- 4. Prefer waiting for TTL expiry (30 min) over force-unlocking.
47
+ Skip jobs ONLY for: single-line fixes, typos, config tweaks.
53
48
 
54
49
  ### Lock Hygiene
55
- - Always provide descriptive `intent` when locking (e.g. "Refactor auth middleware to use JWT validation" — not "editing file")
56
- - Release locks early by completing jobs when done
57
- - Call `finalize_session` at end of session to clean up all locks
58
-
59
- ### Shared Memory
60
- - Call `update_shared_context` after completing meaningful steps
61
- - Log decisions, not just actions (e.g. "Chose JWT over session tokens because...")
62
- - Other agents read the notepad in real-timewrite for them
50
+ - Descriptive `intent` when locking (not "editing file").
51
+ - Release locks IMMEDIATELY by completing jobs. Never hold a lock while doing unrelated work.
52
+ - `force_unlock` is a **last resort** only for locks >25 min old from a crashed agent. Always give a reason.
53
+
54
+ ### Session Cleanup (MANDATORY)
55
+ - `complete_job` after EVERY finished task — do not accumulate incomplete jobs.
56
+ - `update_shared_context` after meaningful steps — log decisions, not just actions.
57
+ - `finalize_session` when the user's request is fully complete this is required, not optional.
@@ -1161,23 +1161,338 @@ var RagEngine = class {
1161
1161
  };
1162
1162
 
1163
1163
  // ../../src/local/mcp-server.ts
1164
+ import path4 from "path";
1165
+ import fs4 from "fs";
1166
+
1167
+ // ../../src/local/local-search.ts
1168
+ import fs3 from "fs/promises";
1164
1169
  import path3 from "path";
1165
- import fs3 from "fs";
1170
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1171
+ "node_modules",
1172
+ ".git",
1173
+ ".next",
1174
+ ".nuxt",
1175
+ ".svelte-kit",
1176
+ "dist",
1177
+ "build",
1178
+ "out",
1179
+ ".output",
1180
+ "coverage",
1181
+ "__pycache__",
1182
+ ".pytest_cache",
1183
+ ".mypy_cache",
1184
+ ".venv",
1185
+ "venv",
1186
+ "env",
1187
+ ".turbo",
1188
+ ".cache",
1189
+ ".parcel-cache",
1190
+ ".axis",
1191
+ "history",
1192
+ ".DS_Store"
1193
+ ]);
1194
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
1195
+ // Binary / media
1196
+ ".png",
1197
+ ".jpg",
1198
+ ".jpeg",
1199
+ ".gif",
1200
+ ".webp",
1201
+ ".ico",
1202
+ ".svg",
1203
+ ".mp3",
1204
+ ".mp4",
1205
+ ".wav",
1206
+ ".webm",
1207
+ ".ogg",
1208
+ ".woff",
1209
+ ".woff2",
1210
+ ".ttf",
1211
+ ".eot",
1212
+ ".pdf",
1213
+ ".zip",
1214
+ ".tar",
1215
+ ".gz",
1216
+ ".br",
1217
+ // Compiled / generated
1218
+ ".pyc",
1219
+ ".pyo",
1220
+ ".so",
1221
+ ".dylib",
1222
+ ".dll",
1223
+ ".exe",
1224
+ ".class",
1225
+ ".jar",
1226
+ ".war",
1227
+ ".wasm",
1228
+ // Lock files (huge, not useful for search)
1229
+ ".lock"
1230
+ ]);
1231
+ var SKIP_FILENAMES = /* @__PURE__ */ new Set([
1232
+ "package-lock.json",
1233
+ "yarn.lock",
1234
+ "pnpm-lock.yaml",
1235
+ "Cargo.lock",
1236
+ "Gemfile.lock",
1237
+ "poetry.lock",
1238
+ ".DS_Store",
1239
+ "Thumbs.db"
1240
+ ]);
1241
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1242
+ "a",
1243
+ "an",
1244
+ "the",
1245
+ "is",
1246
+ "are",
1247
+ "was",
1248
+ "were",
1249
+ "be",
1250
+ "been",
1251
+ "being",
1252
+ "have",
1253
+ "has",
1254
+ "had",
1255
+ "do",
1256
+ "does",
1257
+ "did",
1258
+ "will",
1259
+ "would",
1260
+ "could",
1261
+ "should",
1262
+ "may",
1263
+ "might",
1264
+ "shall",
1265
+ "can",
1266
+ "i",
1267
+ "me",
1268
+ "my",
1269
+ "we",
1270
+ "our",
1271
+ "you",
1272
+ "your",
1273
+ "he",
1274
+ "she",
1275
+ "it",
1276
+ "they",
1277
+ "them",
1278
+ "their",
1279
+ "this",
1280
+ "that",
1281
+ "these",
1282
+ "those",
1283
+ "what",
1284
+ "which",
1285
+ "who",
1286
+ "whom",
1287
+ "where",
1288
+ "when",
1289
+ "how",
1290
+ "why",
1291
+ "in",
1292
+ "on",
1293
+ "at",
1294
+ "to",
1295
+ "for",
1296
+ "of",
1297
+ "with",
1298
+ "by",
1299
+ "from",
1300
+ "up",
1301
+ "about",
1302
+ "into",
1303
+ "through",
1304
+ "during",
1305
+ "before",
1306
+ "after",
1307
+ "and",
1308
+ "but",
1309
+ "or",
1310
+ "nor",
1311
+ "not",
1312
+ "so",
1313
+ "if",
1314
+ "then",
1315
+ "all",
1316
+ "each",
1317
+ "every",
1318
+ "both",
1319
+ "few",
1320
+ "more",
1321
+ "most",
1322
+ "some",
1323
+ "any",
1324
+ "find",
1325
+ "show",
1326
+ "get",
1327
+ "look",
1328
+ "search",
1329
+ "locate",
1330
+ "check",
1331
+ "file",
1332
+ "files",
1333
+ "code",
1334
+ "function",
1335
+ "class",
1336
+ "method",
1337
+ "there",
1338
+ "here",
1339
+ "just",
1340
+ "also",
1341
+ "very",
1342
+ "really",
1343
+ "quite"
1344
+ ]);
1345
+ var MAX_FILE_SIZE = 256 * 1024;
1346
+ var MAX_RESULTS = 20;
1347
+ var CONTEXT_LINES = 2;
1348
+ var MAX_MATCHES_PER_FILE = 6;
1349
+ function extractKeywords(query) {
1350
+ const raw = query.toLowerCase().replace(/[^\w\s\-_.]/g, " ").split(/\s+/).filter((w) => w.length >= 2 && !STOP_WORDS.has(w));
1351
+ return [...new Set(raw)];
1352
+ }
1353
+ async function walkDir(dir, maxDepth = 12) {
1354
+ const results = [];
1355
+ async function recurse(current, depth) {
1356
+ if (depth > maxDepth) return;
1357
+ let entries;
1358
+ try {
1359
+ entries = await fs3.readdir(current, { withFileTypes: true });
1360
+ } catch {
1361
+ return;
1362
+ }
1363
+ for (const entry of entries) {
1364
+ if (entry.name.startsWith(".") && entry.name !== ".env.example") {
1365
+ if (SKIP_DIRS.has(entry.name) || entry.isDirectory()) continue;
1366
+ }
1367
+ const fullPath = path3.join(current, entry.name);
1368
+ if (entry.isDirectory()) {
1369
+ if (SKIP_DIRS.has(entry.name)) continue;
1370
+ await recurse(fullPath, depth + 1);
1371
+ } else if (entry.isFile()) {
1372
+ if (SKIP_FILENAMES.has(entry.name)) continue;
1373
+ const ext = path3.extname(entry.name).toLowerCase();
1374
+ if (SKIP_EXTENSIONS.has(ext)) continue;
1375
+ try {
1376
+ const stat = await fs3.stat(fullPath);
1377
+ if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
1378
+ } catch {
1379
+ continue;
1380
+ }
1381
+ results.push(fullPath);
1382
+ }
1383
+ }
1384
+ }
1385
+ await recurse(dir, 0);
1386
+ return results;
1387
+ }
1388
+ async function searchFile(filePath, rootDir, keywords) {
1389
+ let content;
1390
+ try {
1391
+ content = await fs3.readFile(filePath, "utf-8");
1392
+ } catch {
1393
+ return null;
1394
+ }
1395
+ const contentLower = content.toLowerCase();
1396
+ const relativePath = path3.relative(rootDir, filePath);
1397
+ const matchedKeywords = keywords.filter((kw) => contentLower.includes(kw));
1398
+ if (matchedKeywords.length === 0) return null;
1399
+ const lines = content.split("\n");
1400
+ let score = matchedKeywords.length;
1401
+ const relLower = relativePath.toLowerCase();
1402
+ for (const kw of keywords) {
1403
+ if (relLower.includes(kw)) score += 2;
1404
+ }
1405
+ const matchingLineIndices = [];
1406
+ for (let i = 0; i < lines.length; i++) {
1407
+ const lineLower = lines[i].toLowerCase();
1408
+ if (matchedKeywords.some((kw) => lineLower.includes(kw))) {
1409
+ matchingLineIndices.push(i);
1410
+ }
1411
+ }
1412
+ score += Math.min(matchingLineIndices.length, 20) * 0.1;
1413
+ const regions = [];
1414
+ let lastEnd = -1;
1415
+ for (const idx of matchingLineIndices) {
1416
+ if (regions.length >= MAX_MATCHES_PER_FILE) break;
1417
+ const start = Math.max(0, idx - CONTEXT_LINES);
1418
+ const end = Math.min(lines.length - 1, idx + CONTEXT_LINES);
1419
+ if (start <= lastEnd) continue;
1420
+ const regionLines = lines.slice(start, end + 1).map((line, i) => {
1421
+ const lineNum = start + i + 1;
1422
+ const marker = start + i === idx ? ">" : " ";
1423
+ return `${marker} ${lineNum.toString().padStart(4)}| ${line}`;
1424
+ }).join("\n");
1425
+ regions.push({ lineNumber: idx + 1, lines: regionLines });
1426
+ lastEnd = end;
1427
+ }
1428
+ return { filePath, relativePath, score, matchedKeywords, regions };
1429
+ }
1430
+ async function localSearch(query, rootDir) {
1431
+ const cwd = rootDir || process.cwd();
1432
+ const keywords = extractKeywords(query);
1433
+ if (keywords.length === 0) {
1434
+ return "Could not extract meaningful search terms from the query. Try being more specific (e.g. 'authentication middleware' instead of 'how does it work').";
1435
+ }
1436
+ logger.info(`[localSearch] Query: "${query}" \u2192 Keywords: [${keywords.join(", ")}] in ${cwd}`);
1437
+ const files = await walkDir(cwd);
1438
+ logger.info(`[localSearch] Scanning ${files.length} files`);
1439
+ const BATCH_SIZE = 50;
1440
+ const allMatches = [];
1441
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
1442
+ const batch = files.slice(i, i + BATCH_SIZE);
1443
+ const results = await Promise.all(
1444
+ batch.map((f) => searchFile(f, cwd, keywords))
1445
+ );
1446
+ for (const r of results) {
1447
+ if (r) allMatches.push(r);
1448
+ }
1449
+ }
1450
+ allMatches.sort((a, b) => b.score - a.score);
1451
+ const topMatches = allMatches.slice(0, MAX_RESULTS);
1452
+ if (topMatches.length === 0) {
1453
+ return `No matches found for: "${query}" (searched ${files.length} files for keywords: ${keywords.join(", ")}).
1454
+ Try different terms or check if the code exists in this project.`;
1455
+ }
1456
+ let output = `Found ${allMatches.length} matching file${allMatches.length === 1 ? "" : "s"} (showing top ${topMatches.length}, searched ${files.length} files)
1457
+ `;
1458
+ output += `Keywords: ${keywords.join(", ")}
1459
+ `;
1460
+ output += "\u2550".repeat(60) + "\n\n";
1461
+ for (const match of topMatches) {
1462
+ output += `\u{1F4C4} ${match.relativePath}
1463
+ `;
1464
+ output += ` Keywords matched: ${match.matchedKeywords.join(", ")} | Score: ${match.score.toFixed(1)}
1465
+ `;
1466
+ if (match.regions.length > 0) {
1467
+ output += " \u2500\u2500\u2500\u2500\u2500\n";
1468
+ for (const region of match.regions) {
1469
+ output += region.lines.split("\n").map((l) => ` ${l}`).join("\n") + "\n";
1470
+ if (region !== match.regions[match.regions.length - 1]) {
1471
+ output += " ...\n";
1472
+ }
1473
+ }
1474
+ }
1475
+ output += "\n";
1476
+ }
1477
+ return output;
1478
+ }
1479
+
1480
+ // ../../src/local/mcp-server.ts
1166
1481
  if (process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_KEY) {
1167
1482
  logger.info("Using configuration from MCP client (mcp.json)");
1168
1483
  } else {
1169
1484
  const cwd = process.cwd();
1170
1485
  const possiblePaths = [
1171
- path3.join(cwd, ".env.local"),
1172
- path3.join(cwd, "..", ".env.local"),
1173
- path3.join(cwd, "..", "..", ".env.local"),
1174
- path3.join(cwd, "shared-context", ".env.local"),
1175
- path3.join(cwd, "..", "shared-context", ".env.local")
1486
+ path4.join(cwd, ".env.local"),
1487
+ path4.join(cwd, "..", ".env.local"),
1488
+ path4.join(cwd, "..", "..", ".env.local"),
1489
+ path4.join(cwd, "shared-context", ".env.local"),
1490
+ path4.join(cwd, "..", "shared-context", ".env.local")
1176
1491
  ];
1177
1492
  let envLoaded = false;
1178
1493
  for (const envPath of possiblePaths) {
1179
1494
  try {
1180
- if (fs3.existsSync(envPath)) {
1495
+ if (fs4.existsSync(envPath)) {
1181
1496
  logger.info(`[Fallback] Loading .env.local from: ${envPath}`);
1182
1497
  dotenv2.config({ path: envPath });
1183
1498
  envLoaded = true;
@@ -1241,21 +1556,21 @@ if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUP
1241
1556
  }
1242
1557
  async function ensureFileSystem() {
1243
1558
  try {
1244
- const fs4 = await import("fs/promises");
1245
- const path4 = await import("path");
1559
+ const fs5 = await import("fs/promises");
1560
+ const path5 = await import("path");
1246
1561
  const fsSync2 = await import("fs");
1247
1562
  const cwd = process.cwd();
1248
1563
  logger.info(`Server CWD: ${cwd}`);
1249
- const historyDir = path4.join(cwd, "history");
1250
- await fs4.mkdir(historyDir, { recursive: true }).catch(() => {
1564
+ const historyDir = path5.join(cwd, "history");
1565
+ await fs5.mkdir(historyDir, { recursive: true }).catch(() => {
1251
1566
  });
1252
- const axisDir = path4.join(cwd, ".axis");
1253
- const axisInstructions = path4.join(axisDir, "instructions");
1254
- const legacyInstructions = path4.join(cwd, "agent-instructions");
1567
+ const axisDir = path5.join(cwd, ".axis");
1568
+ const axisInstructions = path5.join(axisDir, "instructions");
1569
+ const legacyInstructions = path5.join(cwd, "agent-instructions");
1255
1570
  if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
1256
1571
  logger.info("Using legacy agent-instructions directory");
1257
1572
  } else {
1258
- await fs4.mkdir(axisInstructions, { recursive: true }).catch(() => {
1573
+ await fs5.mkdir(axisInstructions, { recursive: true }).catch(() => {
1259
1574
  });
1260
1575
  const defaults = [
1261
1576
  ["context.md", `# Project Context
@@ -1286,32 +1601,35 @@ can work on the same codebase without conflicts.
1286
1601
  - Tailwind CSS for styling; no raw CSS unless unavoidable.
1287
1602
  - Functional React components; prefer server components in Next.js App Router.
1288
1603
 
1289
- ## Agent Behavioral Norms
1604
+ ## Agent Behavioral Norms (MANDATORY)
1605
+
1606
+ ### Autonomous Execution
1607
+ Agents MUST execute the full Axis workflow without being told:
1608
+ - NEVER wait for the user to say "use Axis" or "post a job." Just do it.
1609
+ - NEVER pause after analysis to ask permission. Identify issues \u2192 start fixing immediately.
1610
+ - NEVER leave locks or jobs dangling. Complete jobs and finalize sessions automatically.
1611
+ - IMMEDIATELY call get_project_soul as the first action in every session.
1290
1612
 
1291
1613
  ### Plan Before Write
1292
- Every non-trivial task must follow: post_job -> claim_next_job -> propose_file_access -> (edit) -> complete_job.
1293
- Skip only for single-line typo fixes.
1614
+ Every non-trivial task MUST follow: post_job \u2192 claim_next_job \u2192 propose_file_access \u2192 (edit) \u2192 complete_job.
1615
+ Skip ONLY for single-line typo fixes.
1294
1616
 
1295
- ### Force-Unlock Policy
1296
- force_unlock is a LAST RESORT. Before using it:
1297
- 1. Verify the lock is > 25 minutes old.
1298
- 2. Confirm the locking agent is unresponsive.
1299
- 3. Provide a specific reason string.
1300
- Never casually unlock files \u2014 always try propose_file_access first.
1617
+ ### Session Cleanup (MANDATORY)
1618
+ - complete_job IMMEDIATELY after finishing each task.
1619
+ - update_shared_context after meaningful steps.
1620
+ - finalize_session when the user's request is fully complete \u2014 do not wait to be told.
1301
1621
 
1302
- ### Proactive Tool Usage
1303
- Agents must use Axis MCP tools by default \u2014 do not wait for the user to say "use Axis".
1304
- On session start, call get_project_soul or read_context to load project state.
1305
- After significant progress, call update_shared_context.
1622
+ ### Force-Unlock Policy
1623
+ force_unlock is a LAST RESORT \u2014 only for locks >25 min old from a crashed agent. Always give a reason.
1306
1624
  `],
1307
1625
  ["activity.md", "# Activity Log\n\n"]
1308
1626
  ];
1309
1627
  for (const [file, content] of defaults) {
1310
- const p = path4.join(axisInstructions, file);
1628
+ const p = path5.join(axisInstructions, file);
1311
1629
  try {
1312
- await fs4.access(p);
1630
+ await fs5.access(p);
1313
1631
  } catch {
1314
- await fs4.writeFile(p, content);
1632
+ await fs5.writeFile(p, content);
1315
1633
  logger.info(`Created default context file: ${file}`);
1316
1634
  }
1317
1635
  }
@@ -1412,7 +1730,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1412
1730
  },
1413
1731
  {
1414
1732
  name: SEARCH_CONTEXT_TOOL,
1415
- description: "**SEMANTIC SEARCH** for the codebase.\n- Uses vector similarity to find relevant code snippets or documentation.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the class that manages locks'.\n- Note: This searches *indexed* content only. For exact string matches, use `grep` (if available) or `warpgrep`.",
1733
+ description: "**CODEBASE SEARCH** \u2014 search the entire project by natural language or keywords.\n- Scans all source files on disk. Always returns results if matching code exists \u2014 no setup required.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the database connection code'.\n- Also checks the RAG vector index if available, but the local filesystem search always works.\n- Use this INSTEAD of grep/ripgrep to stay within the Axis workflow. This tool searches file contents directly.",
1416
1734
  inputSchema: {
1417
1735
  type: "object",
1418
1736
  properties: {
@@ -1458,7 +1776,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1458
1776
  // --- Decision & Orchestration ---
1459
1777
  {
1460
1778
  name: "propose_file_access",
1461
- description: "**CRITICAL: REQUEST FILE LOCK**.\n- **MUST** be called *before* editing any file to prevent conflicts with other agents.\n- Checks if another agent currently holds a lock.\n- Returns `GRANTED` if safe to proceed, or `REQUIRES_ORCHESTRATION` if someone else is editing.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute), and `intent` (what you are doing).\n- Note: Locks expire after 30 minutes. Use `force_unlock` only if you are certain a lock is stale and blocking progress.",
1779
+ description: "**CRITICAL: REQUEST FILE LOCK** \u2014 call this before EVERY file edit, no exceptions.\n- Checks if another agent currently holds a lock.\n- Returns `GRANTED` if safe to proceed, or `REQUIRES_ORCHESTRATION` if someone else is editing.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute), and `intent` (descriptive \u2014 e.g. 'Refactor auth to use JWT', NOT 'editing file').\n- Locks expire after 30 minutes. Use `force_unlock` only as a last resort for crashed agents.",
1462
1780
  inputSchema: {
1463
1781
  type: "object",
1464
1782
  properties: {
@@ -1485,18 +1803,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1485
1803
  // --- Permanent Memory ---
1486
1804
  {
1487
1805
  name: "finalize_session",
1488
- description: "**END OF SESSION HOUSEKEEPING**.\n- Archives the current Live Notepad to a permanent session log.\n- Clears all active locks and completed jobs.\n- Resets the Live Notepad for the next session.\n- Call this when the user says 'we are done' or 'start fresh'.",
1806
+ description: "**MANDATORY SESSION CLEANUP** \u2014 call this automatically when the user's request is fully complete.\n- Archives the current Live Notepad to a permanent session log.\n- Clears all active locks and completed jobs.\n- Resets the Live Notepad for the next session.\n- Do NOT wait for the user to say 'we are done.' When all tasks are finished, call this yourself.",
1489
1807
  inputSchema: { type: "object", properties: {}, required: [] }
1490
1808
  },
1491
1809
  {
1492
1810
  name: "get_project_soul",
1493
- description: "**HIGH-LEVEL INTENT**: Returns the 'Soul' of the project.\n- Combines `context.md`, `conventions.md`, and other core directives into a single prompt.\n- Use this at the *start* of a conversation to ground yourself in the project's reality.",
1811
+ description: "**MANDATORY FIRST CALL**: Returns the project's goals, architecture, conventions, and active state.\n- Combines `context.md`, `conventions.md`, and other core directives into a single prompt.\n- You MUST call this as your FIRST action in every new session or task \u2014 before reading files, before responding to the user, before anything else.\n- Skipping this call means you are working without context and will make wrong decisions.",
1494
1812
  inputSchema: { type: "object", properties: {}, required: [] }
1495
1813
  },
1496
1814
  // --- Job Board (Task Orchestration) ---
1497
1815
  {
1498
1816
  name: "post_job",
1499
- description: "**CREATE TICKET**: Post a new task to the Job Board.\n- Use this when you identify work that needs to be done but *cannot* be done right now (e.g., refactoring, new feature).\n- Supports `dependencies` (list of other Job IDs that must be done first).\n- Priority: low, medium, high, critical.",
1817
+ description: "**CREATE TICKET**: Post a new task to the Job Board.\n- Call this IMMEDIATELY when you receive a non-trivial task (2+ files, new features, refactors). Do not wait to be asked.\n- Break work into trackable jobs BEFORE you start coding.\n- Supports `dependencies` (list of other Job IDs that must be done first).\n- Priority: low, medium, high, critical.",
1500
1818
  inputSchema: {
1501
1819
  type: "object",
1502
1820
  properties: {
@@ -1534,7 +1852,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1534
1852
  },
1535
1853
  {
1536
1854
  name: "claim_next_job",
1537
- description: "**AUTO-ASSIGNMENT**: Ask the Job Board for the next most important task.\n- Respects priority (Critical > High > ...) and dependencies (won't assign a job if its deps aren't done).\n- Returns the Job object if successful, or 'NO_JOBS_AVAILABLE'.\n- Use this when you are idle and looking for work.",
1855
+ description: "**CLAIM WORK**: Claim the next job from the Job Board before starting it.\n- You MUST claim a job before editing files for that job.\n- Respects priority (Critical > High > ...) and dependencies (won't assign a job if its deps aren't done).\n- Returns the Job object if successful, or 'NO_JOBS_AVAILABLE'.\n- Call this immediately after posting jobs, and again after completing each job to pick up the next one.",
1538
1856
  inputSchema: {
1539
1857
  type: "object",
1540
1858
  properties: {
@@ -1545,7 +1863,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1545
1863
  },
1546
1864
  {
1547
1865
  name: "complete_job",
1548
- description: "**CLOSE TICKET**: Mark a job as done.\n- Requires `outcome` (what was done).\n- If you are not the assigned agent, you must provide the `completionKey`.",
1866
+ description: "**CLOSE TICKET**: Mark a job as done and release file locks.\n- Call this IMMEDIATELY after finishing each job \u2014 do not accumulate completed-but-unclosed jobs.\n- Requires `outcome` (what was done).\n- If you are not the assigned agent, you must provide the `completionKey`.\n- Leaving jobs open holds locks and blocks other agents.",
1549
1867
  inputSchema: {
1550
1868
  type: "object",
1551
1869
  properties: {
@@ -1622,16 +1940,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1622
1940
  }
1623
1941
  if (name === SEARCH_CONTEXT_TOOL) {
1624
1942
  const query = String(args?.query);
1943
+ let ragResults = null;
1625
1944
  try {
1626
- const results = await manager.searchContext(query, nerveCenter.currentProjectName);
1627
- return { content: [{ type: "text", text: results }] };
1628
- } catch (e) {
1629
- if (ragEngine) {
1630
- const results = await ragEngine.search(query);
1631
- return { content: [{ type: "text", text: results.join("\n---\n") }] };
1945
+ const remote = await manager.searchContext(query, nerveCenter.currentProjectName);
1946
+ if (remote && !remote.includes("No results found") && remote.trim().length > 20) {
1947
+ ragResults = remote;
1632
1948
  }
1633
- return { content: [{ type: "text", text: `Search failed: ${e}` }], isError: true };
1949
+ } catch {
1950
+ }
1951
+ if (!ragResults && ragEngine) {
1952
+ try {
1953
+ const localRag = await ragEngine.search(query);
1954
+ if (localRag.length > 0) {
1955
+ ragResults = localRag.join("\n---\n");
1956
+ }
1957
+ } catch {
1958
+ }
1959
+ }
1960
+ let localResults = null;
1961
+ try {
1962
+ localResults = await localSearch(query);
1963
+ } catch (e) {
1964
+ logger.warn(`[search_codebase] Local search error: ${e}`);
1965
+ }
1966
+ const parts = [];
1967
+ if (ragResults) {
1968
+ parts.push("## Indexed Results (RAG)\n\n" + ragResults);
1969
+ }
1970
+ if (localResults && !localResults.startsWith("No matches found") && !localResults.startsWith("Could not extract")) {
1971
+ parts.push("## Local Codebase Search\n\n" + localResults);
1972
+ } else if (!ragResults) {
1973
+ return { content: [{ type: "text", text: localResults || "No results found." }] };
1974
+ }
1975
+ if (parts.length === 0) {
1976
+ return { content: [{ type: "text", text: "No results found for this query." }] };
1634
1977
  }
1978
+ return { content: [{ type: "text", text: parts.join("\n\n---\n\n") }] };
1635
1979
  }
1636
1980
  if (name === "get_subscription_status") {
1637
1981
  const email = String(args?.email);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virsanghavi/axis-server",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Axis MCP Server CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {