devsmind-mcp 4.0.1 → 4.1.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.
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.DEVSMIND_INSTRUCTIONS = exports.DEVSMIND_PORT = void 0;
39
+ exports.DEVSMIND_INSTRUCTIONS = exports.DEVSMIND_PROMPT_NAME = exports.DEVSMIND_PORT = void 0;
40
40
  exports.bindDevmindPath = bindDevmindPath;
41
41
  exports.getBoundDevmindPath = getBoundDevmindPath;
42
42
  exports.cleanup = cleanup;
@@ -86,6 +86,8 @@ exports.DEVSMIND_PORT = 4513;
86
86
  * `description` (resent every call), this is easy for a long session to lose
87
87
  * track of too, so it should carry only what's genuinely cross-cutting.
88
88
  */
89
+ /** MCP prompts/list name for the workflow-contract prompt below — see createMcpServer's prompts handlers. */
90
+ exports.DEVSMIND_PROMPT_NAME = 'devsmind-workflow';
89
91
  exports.DEVSMIND_INSTRUCTIONS = `DevsMind is this team's persistent shared code memory — not a personal tool you reach for only when asked to search something. Every teammate's AI agent, in every session, reads from the SAME graph you are about to write to. There is no "your copy."
90
92
 
91
93
  If you skip recording a change, you are not skipping a formality. You are leaving the whole team's graph stale for every other AI agent that queries this code later — tomorrow, on a different task, in a different session. And the reasoning behind your change (why it was made, what ticket drove it, what was broken before, what you tried and rejected) exists ONLY in this conversation, right now. It is not in the diff. It is not in the commit message. If it isn't captured at commit_changes this turn, it is gone forever — no reindex, no log, no git blame can recover it later.
@@ -96,12 +98,14 @@ Non-negotiable workflow:
96
98
  3. To read one function/class: call get_node_code instead of opening the file, and do not follow it with a file read for context — this is the ONE node-read call, not a lean summary. It already includes the file's imports, the node's own name/type/signature/description, up to 20 named callers AND callees per direction (with exact uses/used_by counts even when the lists are capped), and up to 40 other declarations from the same file (file_outline — this is how you tell "was this renamed?" or "what else lives here" without opening the file). Reach further in the SAME call instead of a second tool: graph_depth + graph_direction walks the transitive graph past the direct neighbors already included (add graph_code:true for a whole call flow's source in one round trip — if some nodes' code doesn't fit the budget they are named in graph.code_omitted_node_ids, so fetch exactly those rather than re-running blind), and history:"full" returns every revision with diffable edits, pageable with history_limit/history_offset. Every capped section is honest about it (*_truncated, *_hint) — a hint means more is reachable in this same call, not a dead end.
97
99
  4. Before touching any function's signature: get_node_code already includes its direct callers by name (used_by_nodes) — for the FULL transitive blast radius, pass graph_direction:"in" with a graph_depth of 2-3 in that same call. Git shows you what changed; it never shows you what depends on it. Find out before you break something, not after.
98
100
  5. Before refactoring: get_node_code already includes the last 3 changes' reasoning by default — for the full revision trail with diffable before/after edits, pass history:"full" in that same call. Git blame tells you who and when; it never tells you why. The actual decision context only exists here.
99
- 6. Write EVERY file with edit_node — .ts, .vue, .css, .json, .xml, .md, anything — and never your editor's own edit/write tools. There is no second write tool: edit_node is the only one. It takes file_path + old_string + new_string exactly like an ordinary edit tool and never refuses a file type; to create a file that doesn't exist yet, pass old_string: "" and the whole file as new_string. Because it knows where your text landed, it works out which function/class you changed automatically: no node_id to look up, no code_snapshot to send back. It answers with every caller of what you changed. The graph only ever holds source code (functions/classes/logic) though — writes landing outside any function (markup, config, an import) get no graph node, normal and expected, not a failure — but the whole-file change is still staged for the local activity log, so commit_changes makes it revertable there like any other edit.
100
- 7. commit_changes REQUIRES a message AND a reasoning the call fails without either. message is the user's request, verbatim, that led to this commit; it builds a local, private, never-pushed activity log (devsmind view Activity) grouping your work by request and letting the user revert a request as a whole. Pass the exact same text again on a later commit that's still answering the same request that merges them into one entry instead of splitting it. reasoning (what_changed/why/goal) is ONE object covering everything staged since the last commit a commit is one logical change, so it gets one why, recorded against every node it touches, not one per edit_node call.
101
- 8. commit_changes also REFUSES any batch containing a brand-NEW node with no description a description is what makes that node findable later by a natural-language search_nodes query instead of only by its exact identifier. If edit_node just created a node, call add_description with it (1-3 sentences of what it does and the domain concepts involved never a restatement of the name) before commit_changes will accept it. Nothing staged is lost by the refusal; retry the same commit_changes call once it's described. Existing nodes are never gated by this only ones new this commit.
102
- 9. A workflow is a named log of how ONE piece of functionality grew, across many sessions read it to learn how the code got this way. start_session tells you if there is a recent one worth continuing; otherwise, when starting work that might belong to an existing multi-session feature, call workflow_list. If a description matches, ask the user before continuing it, then workflow_bind to attach THIS session. Binding is local to you: it never moves, pauses, or steals anyone else's workflow, and two sessions can work different ones at the same time.
103
- 10. Once bound, commit_changes logs a step for you automatically you do NOT need workflow_add_step for ordinary code work. Call it for the thing a commit cannot express: a DECISION OR RESEARCH FINDING THAT CHANGED NO CODE ("evaluated X, rejected it because Y"), attaching the docs behind it via doc_paths. That is the one kind of knowledge nothing else keeps git has the diff, history has the per-node reasoning, but neither records what was considered and rejected. If you did work while unbound, or on the wrong workflow, workflow_sync attaches it afterwards from your local activity log: it previews first and only writes when you pass confirm:true, so nothing has to be got right in the moment.
104
- 11. commit_changes also REQUIRES a feedback object (5 fields) this is the only channel that improves DevsMind over time, so answer it for real, not as a formality. Before writing "none" on any field, actually check: did anything in THIS task take an extra tool call, a guess, a re-read, or a wrong turn? There almost always is something, even on an easy task — a specific one-line answer with evidence (file:line) is far more useful than a reflexive "none". "none" is correct only when you genuinely paid attention and nothing applies. Noticed something worth reporting but aren't committing right now (or don't want to wait until you are)? Call add_feedback directly — same 5 categories, but any one or more, nothing required, no commit needed. Passing evidence (file + snippet) on a graph_problem/edge_problem gets it verified fresh at call time and marked confirmed instead of suspected.`;
101
+ 6. Write EVERY file with edit_node — .ts, .vue, .css, .json, .xml, .md, anything — and never your editor's own edit/write tools. It takes file_path + old_string + new_string exactly like an ordinary edit tool and never refuses a file type; to create a file that doesn't exist yet, pass old_string: "" and the whole file as new_string. Because it knows where your text landed, it works out which function/class you changed automatically: no node_id to look up, no code_snapshot to send back. It answers with every caller of what you changed. The graph only ever holds source code (functions/classes/logic) though — writes landing outside any function (markup, config, an import) get no graph node, normal and expected, not a failure — but the whole-file change is still staged for the local activity log, so commit_changes makes it revertable there like any other edit.
102
+ 7. If a file already got edited WITHOUT going through edit_node your own editor's edit/write tool got used by mistake, or you're catching up on work from before this session call stage_change to recover it: same file_path/old_string/new_string/replace_all/description shape as edit_node, but it traces and stages the change without touching the file, since new_string is already sitting on disk. It fails clearly (new_string not found) if the edit never actually happened, so it can't silently record the wrong thing. Always prefer edit_node going forward; stage_change exists only to catch up after the fact, not as a second way to make an edit.
103
+ 8. commit_changes REQUIRES a message AND a reasoning the call fails without either. message is the user's request, verbatim, that led to this commit; it builds a local, private, never-pushed activity log (devsmind view Activity) grouping your work by request and letting the user revert a request as a whole. Pass the exact same text again on a later commit that's still answering the same requestthat merges them into one entry instead of splitting it. reasoning (what_changed/why/goal) is ONE object covering everything staged since the last commit a commit is one logical change, so it gets one why, recorded against every node it touches, not one per edit_node/stage_change call.
104
+ 9. commit_changes also REFUSES any batch containing a brand-NEW node with no description a description is what makes that node findable later by a natural-language search_nodes query instead of only by its exact identifier. If edit_node or stage_change just created a node, call add_description with it (1-3 sentences of what it does and the domain concepts involved never a restatement of the name) before commit_changes will accept it. Nothing staged is lost by the refusal; retry the same commit_changes call once it's described. Existing nodes are never gated by this — only ones new this commit.
105
+ 10. commit_changes is NOT git and never touches it. It writes only into DevsMind's own local graph/database (.devmind/) and local activity log — it never runs git add, git commit, git push, or any other git command, and its "commit" is a completely different thing from a git commit despite the shared word. Calling it successfully is not a signal to now go run git yourself: never run git add/git commit/git push on your own initiative just because commit_changes succeeded, or because you finished a task. Your real git commit staging and committing the actual code changes is the developer's decision, made separately, only when they explicitly ask for it, exactly like any other git action.
106
+ 11. A workflow is a named log of how ONE piece of functionality grew, across many sessions read it to learn how the code got this way. start_session tells you if there is a recent one worth continuing; otherwise, when starting work that might belong to an existing multi-session feature, call workflow_list. If a description matches, ask the user before continuing it, then workflow_bind to attach THIS session. Binding is local to you: it never moves, pauses, or steals anyone else's workflow, and two sessions can work different ones at the same time.
107
+ 12. Once bound, commit_changes logs a step for you automatically — you do NOT need workflow_add_step for ordinary code work. Call it for the thing a commit cannot express: a DECISION OR RESEARCH FINDING THAT CHANGED NO CODE ("evaluated X, rejected it because Y"), attaching the docs behind it via doc_paths. That is the one kind of knowledge nothing else keeps — git has the diff, history has the per-node reasoning, but neither records what was considered and rejected. If you did work while unbound, or on the wrong workflow, workflow_sync attaches it afterwards from your local activity log: it previews first and only writes when you pass confirm:true, so nothing has to be got right in the moment.
108
+ 13. commit_changes also REQUIRES a feedback object (5 fields) — this is the only channel that improves DevsMind over time, so answer it for real, not as a formality. Before writing "none" on any field, actually check: did anything in THIS task take an extra tool call, a guess, a re-read, or a wrong turn? There almost always is something, even on an easy task — a specific one-line answer with evidence (file:line) is far more useful than a reflexive "none". "none" is correct only when you genuinely paid attention and nothing applies. Noticed something worth reporting but aren't committing right now (or don't want to wait until you are)? Call add_feedback directly — same 5 categories, but any one or more, nothing required, no commit needed. Passing evidence (file + snippet) on a graph_problem/edge_problem gets it verified fresh at call time and marked confirmed instead of suspected.`;
105
109
  // Shared node-type taxonomy description, reused by update_history and add_description.
106
110
  const NODE_TYPE_DESCRIPTION = 'The type of node. Be highly specific and framework-aware. Choose from the taxonomy below (or use a custom value if nothing fits).\n\n' +
107
111
  'UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator\n\n' +
@@ -132,7 +136,7 @@ const NODE_TYPE_DESCRIPTION = 'The type of node. Be highly specific and framewor
132
136
  'SCRIPTS: build_script | migration_script | seed_script\n' +
133
137
  'TESTS: test_suite | test_case | test_helper | mock | fixture\n' +
134
138
  'UTILITY: util_function | helper | transformer | validator | formatter';
135
- // Shared `description` field schema, reused by edit_node and add_description. This is what
139
+ // Shared `description` field schema, reused by edit_node, stage_change, and add_description. This is what
136
140
  // makes search_nodes findable by natural language — an identifier alone is a handful of words;
137
141
  // this is where the domain vocabulary (login/auth/sign-in, cart/basket, ...) actually lives.
138
142
  const DESCRIPTION_FIELD_SCHEMA = {
@@ -381,7 +385,7 @@ const SESSION_EXEMPT_READ_TOOLS = new Set([
381
385
  */
382
386
  function createMcpServer() {
383
387
  const server = new index_js_1.Server({ name: 'devsmind-server', version: version_1.DEVSMIND_VERSION }, {
384
- capabilities: { tools: {} },
388
+ capabilities: { tools: {}, prompts: {} },
385
389
  instructions: exports.DEVSMIND_INSTRUCTIONS
386
390
  });
387
391
  // ── Tool Definitions ────────────────────────────────────────────────────────
@@ -529,6 +533,27 @@ function createMcpServer() {
529
533
  required: ['devmind_path', 'file_path', 'old_string', 'new_string']
530
534
  }
531
535
  },
536
+ {
537
+ name: 'stage_change',
538
+ description: "Catch up on a file that got edited WITHOUT going through edit_node — your editor's own edit/write tool got used by mistake, a shell command touched it, or you're recording work from earlier that never got staged. Same shape as edit_node — `file_path` + `old_string` + `new_string` (+ optional `replace_all`/`description`) — but it NEVER writes to the file: `new_string` is expected to already be sitting on disk, and it just traces and stages the change into the graph exactly like edit_node would have, had it been called at the time.\n\n" +
539
+ "`old_string` here is what the code looked like BEFORE the edit (used to reconstruct history and diffs), and `new_string` is what it looks like NOW, on disk — the exact opposite of what edit_node searches for. If `new_string` isn't found, nothing is staged and you're told plainly: either the edit hasn't actually happened yet (call edit_node instead) or the text doesn't match exactly.\n\n" +
540
+ "Prefer edit_node for every edit going forward — this exists only to recover a change edit_node didn't make, not as a second way to make one. Same rules apply once staged: nothing reaches the graph until commit_changes, and a brand-new node still needs a description before commit_changes will accept it (pass it here, or use add_description after).",
541
+ inputSchema: {
542
+ type: 'object',
543
+ properties: {
544
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
545
+ file_path: { type: 'string', description: 'The file that was already edited. Must exist.' },
546
+ old_string: { type: 'string', description: 'The exact text the file had BEFORE the edit (for history/diffing) — the opposite of edit_node, this is NOT searched for on disk. Pass "" if the file itself did not exist before this edit (i.e. it was newly created).' },
547
+ new_string: { type: 'string', description: 'The exact text the file has NOW. Must be found on disk exactly once unless replace_all is true — this is what gets located and traced.' },
548
+ replace_all: { type: 'boolean', description: 'Trace every occurrence of new_string instead of requiring a unique match (default false).' },
549
+ description: {
550
+ ...DESCRIPTION_FIELD_SCHEMA,
551
+ description: DESCRIPTION_FIELD_SCHEMA.description + ' Only applies when this edit touches exactly one function/class — ignored otherwise, since it would be ambiguous which touched symbol it describes.'
552
+ }
553
+ },
554
+ required: ['devmind_path', 'file_path', 'old_string', 'new_string']
555
+ }
556
+ },
532
557
  {
533
558
  name: 'add_description',
534
559
  description: 'Give a natural-language description to one or more nodes that don\'t have one yet. This is what `search_nodes` matches against, so it is the ONLY way a teammate\'s natural-language question ("where do we handle X") finds this code later — an identifier alone rarely does. Three situations call for this: (1) `commit_changes` refused because a NEW node from this turn has no description — call this with exactly those node_ids, then call commit_changes again; (2) you noticed an EXISTING committed node has no description (or a poor one) and want to add/fix it directly, which writes immediately with no commit needed; (3) `index_start`/`index_continue` handed you a batch of freshly-extracted nodes — this is how you describe them, no separate indexing tool exists. Write 1-3 sentences describing PURPOSE — what it does and the domain concepts involved — using the words a developer would actually search by (e.g. mention "login"/"sign-in"/"authentication" together, not just whichever one the identifier happens to use). Never just restate the identifier: "verifyCredentials verifies credentials" is rejected — it adds no findable vocabulary.',
@@ -609,7 +634,8 @@ function createMcpServer() {
609
634
  },
610
635
  {
611
636
  name: 'commit_changes',
612
- description: 'Flush THIS SESSION\'s buffered edit_node entries in one atomic pass: creates/updates every staged node, writes every history snapshot with the ONE `reasoning` you give here, resolves all connections via local AST (auto-creating any referenced-but-missing nodes), then clears only this session\'s share of the buffer. Every entity staged since your last commit gets the SAME reasoning — a commit is one logical change, so it needs one why, not one per node. The staging buffer is shared by every session pointed at this .devmind directory, but a commit only ever touches entries YOUR session staged — another session\'s still-pending work (possibly in an unrelated file or repo) is never included and is never cleared out from under it; `other_sessions_pending` in the response tells you if any exist. If a workflow is currently active, this ALSO auto-records a step on its timeline from that reasoning — you do not need a separate workflow_add_step call for the normal case. Call commit_changes at natural checkpoints — after a batch of related nodes, or when switching context — not only once at the very end of a long task; a checkpoint commit can\'t be forgotten the way a single end-of-task one can. Always call it again before ending the turn if anything is still staged: an uncommitted turn leaves your own work out of the graph.',
637
+ description: 'Flush THIS SESSION\'s buffered edit_node/stage_change entries in one atomic pass: creates/updates every staged node, writes every history snapshot with the ONE `reasoning` you give here, resolves all connections via local AST (auto-creating any referenced-but-missing nodes), then clears only this session\'s share of the buffer. Every entity staged since your last commit gets the SAME reasoning — a commit is one logical change, so it needs one why, not one per node. The staging buffer is shared by every session pointed at this .devmind directory, but a commit only ever touches entries YOUR session staged — another session\'s still-pending work (possibly in an unrelated file or repo) is never included and is never cleared out from under it; `other_sessions_pending` in the response tells you if any exist. If a workflow is currently active, this ALSO auto-records a step on its timeline from that reasoning — you do not need a separate workflow_add_step call for the normal case. Call commit_changes at natural checkpoints — after a batch of related nodes, or when switching context — not only once at the very end of a long task; a checkpoint commit can\'t be forgotten the way a single end-of-task one can. Always call it again before ending the turn if anything is still staged: an uncommitted turn leaves your own work out of the graph.\n\n' +
638
+ 'NOT git. This never runs `git add`/`git commit`/`git push` or any other git command, and never touches your actual git history — it writes only into DevsMind\'s own local graph/database, despite sharing the word "commit". A successful call here is not a cue to now run git yourself: do not `git add`/`git commit`/`git push` on your own initiative just because this succeeded or a task feels finished. That stays the developer\'s call, made separately, only when explicitly asked for.',
613
639
  inputSchema: {
614
640
  type: 'object',
615
641
  properties: {
@@ -1770,6 +1796,183 @@ function createMcpServer() {
1770
1796
  });
1771
1797
  return { content };
1772
1798
  }
1799
+ case 'stage_change': {
1800
+ const devmindPath = resolveDevmindPath(args.devmind_path);
1801
+ const stageDb = getDatabase(devmindPath);
1802
+ const workspaceRoot = path.dirname(devmindPath);
1803
+ if (!args.file_path || args.old_string === undefined || args.new_string === undefined) {
1804
+ return {
1805
+ isError: true,
1806
+ content: [{
1807
+ type: 'text',
1808
+ text: JSON.stringify({
1809
+ staged: false,
1810
+ error: 'stage_change needs file_path, old_string (the code BEFORE the edit) and new_string (the code as it is NOW, already on disk).'
1811
+ })
1812
+ }]
1813
+ };
1814
+ }
1815
+ const rawPath = String(args.file_path);
1816
+ const filePath = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(workspaceRoot, rawPath);
1817
+ if (!stageDb.isPathAllowed(filePath)) {
1818
+ return {
1819
+ isError: true,
1820
+ content: [{
1821
+ type: 'text',
1822
+ text: JSON.stringify({
1823
+ staged: false,
1824
+ error: "file_path resolves outside the project's configured repos — nothing was staged.",
1825
+ resolved_path: filePath
1826
+ })
1827
+ }]
1828
+ };
1829
+ }
1830
+ const oldString = String(args.old_string);
1831
+ if (!fs.existsSync(filePath)) {
1832
+ return {
1833
+ isError: true,
1834
+ content: [{
1835
+ type: 'text',
1836
+ text: JSON.stringify({
1837
+ staged: false,
1838
+ file_path: filePath,
1839
+ error: `${path.basename(filePath)} does not exist. stage_change records an edit that has ALREADY landed on disk — if this file hasn't been created yet, call edit_node instead.`
1840
+ })
1841
+ }]
1842
+ };
1843
+ }
1844
+ const result = (0, edit_1.locateAppliedEdit)(filePath, oldString, String(args.new_string), args.replace_all === true);
1845
+ if (!result.ok) {
1846
+ return {
1847
+ isError: true,
1848
+ content: [{ type: 'text', text: JSON.stringify({ staged: false, file_path: filePath, error: result.error }) }]
1849
+ };
1850
+ }
1851
+ (0, ast_1.invalidateParsedFile)(filePath);
1852
+ // Same tracing as edit_node from here on — the file already has new_string on disk,
1853
+ // so this just works out which function/class that already-applied edit landed in.
1854
+ const knownHere = stageDb.getNodesByFilePath(filePath).map(n => {
1855
+ const parsed = (0, ast_1.parseNodeId)(n.id);
1856
+ return { id: n.id, symbolName: parsed ? parsed.symbolName : (n.id.split('#').pop() || n.name) };
1857
+ });
1858
+ const touched = (0, ast_1.findTouchedSymbols)(filePath, result.ranges || [], knownHere, result.before);
1859
+ let singleSymbolDescription;
1860
+ let descriptionNote;
1861
+ if (args.description !== undefined) {
1862
+ if (touched.length !== 1) {
1863
+ descriptionNote = touched.length === 0
1864
+ ? 'description was ignored: this edit touched no function/class, so there was nothing to describe.'
1865
+ : `description was ignored: this edit touched ${touched.length} symbols, which one it describes is ambiguous — use add_description for each after this call.`;
1866
+ }
1867
+ else {
1868
+ const check = (0, tokenize_1.validateDescription)(String(args.description), touched[0].name);
1869
+ if (check.ok) {
1870
+ singleSymbolDescription = String(args.description);
1871
+ }
1872
+ else {
1873
+ descriptionNote = `description was rejected and ignored: ${check.error}`;
1874
+ }
1875
+ }
1876
+ }
1877
+ const staged = [];
1878
+ const diffBlocks = [];
1879
+ for (const t of touched) {
1880
+ const nodeId = t.node_id || `${stageDb.toRepoRelativePath(filePath)}#${t.symbolName}`;
1881
+ (0, staging_1.stageEntry)(devmindPath, {
1882
+ node_id: nodeId,
1883
+ file_path: filePath,
1884
+ code_snapshot: t.codeSnapshot,
1885
+ code_before: t.codeBefore,
1886
+ name: t.name,
1887
+ type: t.type,
1888
+ signature: t.signature || undefined,
1889
+ description: singleSymbolDescription,
1890
+ session_id: sessionId
1891
+ });
1892
+ const conns = t.node_id ? stageDb.getConnections(t.node_id) : { uses: [], usedBy: [] };
1893
+ const priorHistory = (t.node_id ? stageDb.getFullHistory(t.node_id) : [])
1894
+ .flatMap(h => (0, database_1.parseReasoningBlocks)(h.reasoning).map(r => ({ updated_at: h.updated_at, r })))
1895
+ .slice(0, 2)
1896
+ .map(({ updated_at, r }) => ({ updated_at, developer: r.developer, what_changed: r.what_changed, why: r.why }));
1897
+ staged.push({
1898
+ node_id: nodeId,
1899
+ name: t.name,
1900
+ type: t.type,
1901
+ lines: `${t.startLine}-${t.endLine}`,
1902
+ is_new_to_graph: t.isNew,
1903
+ described: singleSymbolDescription !== undefined,
1904
+ callers: conns.usedBy.slice(0, 10).map(n => ({ id: n.id, name: n.name, file_path: n.file_path })),
1905
+ callers_total: conns.usedBy.length,
1906
+ calls_out: conns.uses.slice(0, 10).map(n => ({ id: n.id, name: n.name })),
1907
+ prior_history: priorHistory
1908
+ });
1909
+ diffBlocks.push(`### ${t.name} \`${path.basename(filePath)}:${t.startLine}-${t.endLine}\`\n\`\`\`diff\n${(0, diff_1.renderUnifiedDiff)(t.codeBefore ?? '', t.codeSnapshot)}\n\`\`\``);
1910
+ }
1911
+ // Same as edit_node: nothing traced into the graph doesn't mean nothing worth keeping —
1912
+ // a non-code file, or an edit landing outside any function, still gets its whole-file
1913
+ // before/after staged for the LOCAL activity log so it's revertable in devsmind view too.
1914
+ let fileEditStaged = false;
1915
+ if (touched.length === 0) {
1916
+ const afterContent = fs.readFileSync(filePath, 'utf-8');
1917
+ (0, staging_1.stageFileEdit)(devmindPath, {
1918
+ file_path: filePath,
1919
+ before: result.before ?? '',
1920
+ after: afterContent,
1921
+ session_id: sessionId
1922
+ });
1923
+ fileEditStaged = true;
1924
+ }
1925
+ const { entries: myPendingEntries, fileEdits: myPendingFileEdits } = (0, staging_1.partitionStagedForSession)(devmindPath, sessionId);
1926
+ const pending = myPendingEntries.length + myPendingFileEdits.length;
1927
+ const ext = path.extname(filePath).toLowerCase();
1928
+ const callerCount = staged.reduce((sum, s) => sum + s.callers_total, 0);
1929
+ const newUndescribedCount = staged.filter(s => s.is_new_to_graph && !s.described).length;
1930
+ const newDescribedCount = staged.filter(s => s.is_new_to_graph && s.described).length;
1931
+ let reminder;
1932
+ if (staged.length) {
1933
+ reminder = callerCount
1934
+ ? `Recorded ${staged.length} node(s). ${callerCount} node(s) call what you changed — if you altered a signature or contract, check them before moving on. Nothing reaches the graph until commit_changes.`
1935
+ : `Recorded ${staged.length} node(s). Nothing reaches the graph until commit_changes.`;
1936
+ if (newUndescribedCount > 0) {
1937
+ reminder += ` ${newUndescribedCount} of these are NEW and undescribed — commit_changes will refuse the batch without a description for each (call add_description before you commit, while this code is still fresh in context, or pass description directly to stage_change next time when it's a single new symbol).`;
1938
+ }
1939
+ if (newDescribedCount > 0) {
1940
+ reminder += ` ${newDescribedCount} new node(s) already described via this call — no add_description round trip needed for those.`;
1941
+ }
1942
+ if (descriptionNote) {
1943
+ reminder += ` ${descriptionNote}`;
1944
+ }
1945
+ }
1946
+ else if (!scanner_1.INDEXABLE_EXTENSIONS.has(ext)) {
1947
+ reminder = `${ext || 'This file type'} is intentionally out of scope for the code graph — no node was recorded. The whole-file change is staged for the local activity log though, so commit_changes will still make it revertable there.`;
1948
+ }
1949
+ else if ((0, ast_1.isAstParseable)(filePath)) {
1950
+ reminder = oldString === ''
1951
+ ? 'The file exists but declares no function or class, so no graph node was recorded. The whole-file change is staged for the local activity log, so commit_changes will still make it revertable there.'
1952
+ : 'This edit did not land inside any function or class (an import, a top-level constant, or similar), so no graph node was recorded. The whole-file change is staged for the local activity log, so commit_changes will still make it revertable there.';
1953
+ }
1954
+ else {
1955
+ reminder = `${ext} has no parser support in DevsMind yet (TS/JS only, for now) — this could not be traced into the graph. The whole-file change is staged for the local activity log though, so commit_changes will still make it revertable there.`;
1956
+ }
1957
+ const content = [];
1958
+ if (diffBlocks.length) {
1959
+ content.push({ type: 'text', text: `Recorded ${path.basename(filePath)}\n\n${diffBlocks.join('\n\n')}` });
1960
+ }
1961
+ content.push({
1962
+ type: 'text',
1963
+ text: JSON.stringify({
1964
+ staged: true,
1965
+ file_path: filePath,
1966
+ matched: result.replacements,
1967
+ recorded: staged.length,
1968
+ pending_count: pending,
1969
+ touched: staged,
1970
+ file_edit_staged: fileEditStaged,
1971
+ reminder
1972
+ }, null, 2)
1973
+ });
1974
+ return { content };
1975
+ }
1773
1976
  case 'add_description': {
1774
1977
  const devmindPath = resolveDevmindPath(args.devmind_path);
1775
1978
  const db = getDatabase(devmindPath);
@@ -2860,6 +3063,27 @@ function createMcpServer() {
2860
3063
  ]
2861
3064
  };
2862
3065
  });
3066
+ // Prompts (MCP's own explicit-invocation capability) — separate from the tools above:
3067
+ // prompts are spec-mandated user-controlled, so a client surfaces them for a human to
3068
+ // explicitly pick (often as a slash command), never invokes one autonomously. This is the
3069
+ // same DEVSMIND_INSTRUCTIONS text already sent once at connect (`instructions:` above) and
3070
+ // reused by `devsmind memory`'s printed prompt — exposing it here too costs nothing extra
3071
+ // and gives clients with real prompts support (Claude Code, Cursor, Windsurf, Kiro, Qwen) a
3072
+ // way to re-assert it mid-conversation without the user re-pasting anything.
3073
+ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
3074
+ prompts: [{
3075
+ name: exports.DEVSMIND_PROMPT_NAME,
3076
+ description: 'The DevsMind workflow contract — start_session, search_nodes over grep, edit_node for every write, commit_changes at checkpoints. Same text sent automatically at connect; invoke this any time to re-assert it mid-conversation.'
3077
+ }]
3078
+ }));
3079
+ server.setRequestHandler(types_js_1.GetPromptRequestSchema, async (request) => {
3080
+ if (request.params.name !== exports.DEVSMIND_PROMPT_NAME) {
3081
+ throw new Error(`Prompt not found: ${request.params.name}`);
3082
+ }
3083
+ return {
3084
+ messages: [{ role: 'user', content: { type: 'text', text: exports.DEVSMIND_INSTRUCTIONS } }]
3085
+ };
3086
+ });
2863
3087
  return server;
2864
3088
  }
2865
3089
  // ── Graceful shutdown helpers ─────────────────────────────────────────────────