@virsanghavi/axis-server 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.axis/instructions/conventions.md +8 -2
- package/dist/mcp-server.mjs +84 -17
- package/package.json +1 -1
|
@@ -51,7 +51,13 @@ Skip jobs ONLY for: single-line fixes, typos, config tweaks.
|
|
|
51
51
|
- Release locks IMMEDIATELY by completing jobs. Never hold a lock while doing unrelated work.
|
|
52
52
|
- `force_unlock` is a **last resort** — only for locks >25 min old from a crashed agent. Always give a reason.
|
|
53
53
|
|
|
54
|
+
### Releasing Locks (CRITICAL — do not skip)
|
|
55
|
+
**Every file you lock MUST be unlocked before your session ends.** Dangling locks block every other agent in the project.
|
|
56
|
+
- **Primary unlock method**: `complete_job` — releases all locks for that job.
|
|
57
|
+
- **Session end**: `finalize_session` — clears ALL remaining locks. Call this before you stop responding.
|
|
58
|
+
- **Self-check**: Before finishing, verify: "Have I completed all jobs and called `finalize_session`?" If not, do it now.
|
|
59
|
+
|
|
54
60
|
### Session Cleanup (MANDATORY)
|
|
55
|
-
- `complete_job` after EVERY finished task — do not accumulate incomplete jobs.
|
|
61
|
+
- `complete_job` after EVERY finished task — do not accumulate incomplete jobs. **This is how locks get released.**
|
|
56
62
|
- `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.
|
|
63
|
+
- `finalize_session` when the user's request is fully complete — this is required, not optional. **This clears all remaining locks.**
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -284,7 +284,7 @@ var CircuitOpenError = class extends Error {
|
|
|
284
284
|
this.name = "CircuitOpenError";
|
|
285
285
|
}
|
|
286
286
|
};
|
|
287
|
-
var NerveCenter = class {
|
|
287
|
+
var NerveCenter = class _NerveCenter {
|
|
288
288
|
mutex;
|
|
289
289
|
state;
|
|
290
290
|
contextManager;
|
|
@@ -876,6 +876,38 @@ ${notepad}`;
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
// --- Decision & Orchestration ---
|
|
879
|
+
/**
|
|
880
|
+
* Check if two file paths conflict hierarchically.
|
|
881
|
+
* A lock on a directory blocks locks on any file within it, and vice versa.
|
|
882
|
+
* Examples:
|
|
883
|
+
* pathsConflict("/foo/bar", "/foo/bar/baz.ts") => true (parent blocks child)
|
|
884
|
+
* pathsConflict("/foo/bar/baz.ts", "/foo/bar") => true (child blocks parent)
|
|
885
|
+
* pathsConflict("/foo/bar", "/foo/bar") => true (exact match)
|
|
886
|
+
* pathsConflict("/foo/bar", "/foo/baz") => false (siblings)
|
|
887
|
+
*/
|
|
888
|
+
static pathsConflict(pathA, pathB) {
|
|
889
|
+
const a = pathA.replace(/\/+$/, "");
|
|
890
|
+
const b = pathB.replace(/\/+$/, "");
|
|
891
|
+
if (a === b) return true;
|
|
892
|
+
if (b.startsWith(a + "/")) return true;
|
|
893
|
+
if (a.startsWith(b + "/")) return true;
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Find any existing lock that conflicts hierarchically with the requested path.
|
|
898
|
+
* Skips locks owned by the same agent and stale locks.
|
|
899
|
+
*/
|
|
900
|
+
findHierarchicalConflict(requestedPath, requestingAgent, locks) {
|
|
901
|
+
for (const lock of locks) {
|
|
902
|
+
if (lock.agentId === requestingAgent) continue;
|
|
903
|
+
const isStale = Date.now() - lock.timestamp > this.lockTimeout;
|
|
904
|
+
if (isStale) continue;
|
|
905
|
+
if (_NerveCenter.pathsConflict(requestedPath, lock.filePath)) {
|
|
906
|
+
return lock;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
879
911
|
async proposeFileAccess(agentId, filePath, intent, userPrompt) {
|
|
880
912
|
return await this.mutex.runExclusive(async () => {
|
|
881
913
|
logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
|
|
@@ -906,6 +938,16 @@ ${notepad}`;
|
|
|
906
938
|
} catch (e) {
|
|
907
939
|
if (e.message && e.message.includes("409")) {
|
|
908
940
|
logger.info(`[proposeFileAccess] Lock conflict (409)`);
|
|
941
|
+
let blockingAgent;
|
|
942
|
+
try {
|
|
943
|
+
const jsonMatch = e.message.match(/\{.*\}/s);
|
|
944
|
+
if (jsonMatch) {
|
|
945
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
946
|
+
blockingAgent = parsed.current_lock?.agent_id;
|
|
947
|
+
}
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
this.logLockEvent("BLOCKED", filePath, agentId, blockingAgent, intent);
|
|
909
951
|
return {
|
|
910
952
|
status: "REQUIRES_ORCHESTRATION",
|
|
911
953
|
message: `File '${filePath}' is locked by another agent`
|
|
@@ -917,6 +959,26 @@ ${notepad}`;
|
|
|
917
959
|
}
|
|
918
960
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
919
961
|
try {
|
|
962
|
+
const { data: existingLocks } = await this.supabase.from("locks").select("agent_id, file_path, intent, updated_at").eq("project_id", this._projectId);
|
|
963
|
+
if (existingLocks && existingLocks.length > 0) {
|
|
964
|
+
const asFileLocks = existingLocks.map((row2) => ({
|
|
965
|
+
agentId: row2.agent_id,
|
|
966
|
+
filePath: row2.file_path,
|
|
967
|
+
intent: row2.intent,
|
|
968
|
+
userPrompt: "",
|
|
969
|
+
timestamp: row2.updated_at ? Date.parse(row2.updated_at) : Date.now()
|
|
970
|
+
}));
|
|
971
|
+
const conflict2 = this.findHierarchicalConflict(filePath, agentId, asFileLocks);
|
|
972
|
+
if (conflict2) {
|
|
973
|
+
logger.info(`[proposeFileAccess] Hierarchical conflict: '${filePath}' overlaps with locked '${conflict2.filePath}' (owner: ${conflict2.agentId})`);
|
|
974
|
+
this.logLockEvent("BLOCKED", filePath, agentId, conflict2.agentId, intent);
|
|
975
|
+
return {
|
|
976
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
977
|
+
message: `Conflict: '${filePath}' overlaps with '${conflict2.filePath}' locked by '${conflict2.agentId}'`,
|
|
978
|
+
currentLock: conflict2
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
920
982
|
const { data, error } = await this.supabase.rpc("try_acquire_lock", {
|
|
921
983
|
p_project_id: this._projectId,
|
|
922
984
|
p_file_path: filePath,
|
|
@@ -949,17 +1011,16 @@ ${notepad}`;
|
|
|
949
1011
|
logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
|
|
950
1012
|
}
|
|
951
1013
|
}
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
1014
|
+
const allLocks = Object.values(this.state.locks);
|
|
1015
|
+
const conflict = this.findHierarchicalConflict(filePath, agentId, allLocks);
|
|
1016
|
+
if (conflict) {
|
|
1017
|
+
logger.info(`[proposeFileAccess] Hierarchical conflict (local): '${filePath}' overlaps with locked '${conflict.filePath}' (owner: ${conflict.agentId})`);
|
|
1018
|
+
this.logLockEvent("BLOCKED", filePath, agentId, conflict.agentId, intent);
|
|
1019
|
+
return {
|
|
1020
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
1021
|
+
message: `Conflict: '${filePath}' overlaps with '${conflict.filePath}' locked by '${conflict.agentId}'`,
|
|
1022
|
+
currentLock: conflict
|
|
1023
|
+
};
|
|
963
1024
|
}
|
|
964
1025
|
this.state.locks[filePath] = { agentId, filePath, intent, userPrompt, timestamp: Date.now() };
|
|
965
1026
|
await this.saveState();
|
|
@@ -1785,10 +1846,16 @@ Agents MUST execute the full Axis workflow without being told:
|
|
|
1785
1846
|
Every non-trivial task MUST follow: post_job \u2192 claim_next_job \u2192 propose_file_access \u2192 (edit) \u2192 complete_job.
|
|
1786
1847
|
Skip ONLY for single-line typo fixes.
|
|
1787
1848
|
|
|
1849
|
+
### Releasing Locks (CRITICAL)
|
|
1850
|
+
Every file you lock MUST be unlocked before your session ends. Dangling locks block all other agents.
|
|
1851
|
+
- complete_job releases locks for that job. Call it IMMEDIATELY after each task.
|
|
1852
|
+
- finalize_session clears ALL remaining locks. Call it before you stop responding.
|
|
1853
|
+
- NEVER end a session while holding locks. Self-check: "Did I call finalize_session?"
|
|
1854
|
+
|
|
1788
1855
|
### Session Cleanup (MANDATORY)
|
|
1789
|
-
- complete_job IMMEDIATELY after finishing each task.
|
|
1856
|
+
- complete_job IMMEDIATELY after finishing each task \u2014 this is how locks get released.
|
|
1790
1857
|
- update_shared_context after meaningful steps.
|
|
1791
|
-
- finalize_session when the user's request is fully complete \u2014 do not wait to be told.
|
|
1858
|
+
- finalize_session when the user's request is fully complete \u2014 do not wait to be told. This clears all remaining locks.
|
|
1792
1859
|
|
|
1793
1860
|
### Force-Unlock Policy
|
|
1794
1861
|
force_unlock is a LAST RESORT \u2014 only for locks >25 min old from a crashed agent. Always give a reason.
|
|
@@ -1945,7 +2012,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1945
2012
|
// --- Decision & Orchestration ---
|
|
1946
2013
|
{
|
|
1947
2014
|
name: "propose_file_access",
|
|
1948
|
-
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.",
|
|
2015
|
+
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- **Hierarchical matching**: Locking a directory also blocks locks on files within it, and vice versa. E.g. locking `src/api/` blocks `src/api/auth/login.ts`.\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.\n- **IMPORTANT**: Every lock you acquire MUST be released. Call `complete_job` when done with each task, and `finalize_session` before ending your session. Dangling locks block all other agents.",
|
|
1949
2016
|
inputSchema: {
|
|
1950
2017
|
type: "object",
|
|
1951
2018
|
properties: {
|
|
@@ -1972,7 +2039,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1972
2039
|
// --- Permanent Memory ---
|
|
1973
2040
|
{
|
|
1974
2041
|
name: "finalize_session",
|
|
1975
|
-
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
|
|
2042
|
+
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 file locks** and completed jobs. This is your safety net to ensure no dangling locks.\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.\n- **CRITICAL**: You MUST call this before ending ANY session. Failing to do so leaves file locks that block all other agents.",
|
|
1976
2043
|
inputSchema: { type: "object", properties: {}, required: [] }
|
|
1977
2044
|
},
|
|
1978
2045
|
{
|
|
@@ -2032,7 +2099,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2032
2099
|
},
|
|
2033
2100
|
{
|
|
2034
2101
|
name: "complete_job",
|
|
2035
|
-
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.",
|
|
2102
|
+
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- **This is the primary way to release file locks.** Leaving jobs open holds locks and blocks other agents.\n- REMINDER: After completing all jobs, you MUST also call `finalize_session` to clear any remaining locks.",
|
|
2036
2103
|
inputSchema: {
|
|
2037
2104
|
type: "object",
|
|
2038
2105
|
properties: {
|