clawlet 0.7.0 → 0.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/src/tools.ts CHANGED
@@ -18,6 +18,16 @@ const GENERATE_TEXT_MAX_STEPS = 30;
18
18
 
19
19
  const turndownService = new TurndownService()
20
20
 
21
+ /** Index file content into the knowledge store. Silently catches errors so index failures don't break file operations. */
22
+ async function indexFileContent(memory: AgentMemory, path: string, content: string): Promise<void> {
23
+ try { await memory.knowledge.upsert(path, content); } catch { /* index failure is non-fatal */ }
24
+ }
25
+
26
+ /** Remove file from knowledge index. Silently catches errors. */
27
+ async function removeFromIndex(memory: AgentMemory, path: string): Promise<void> {
28
+ try { await memory.knowledge.remove(path); } catch { /* index failure is non-fatal */ }
29
+ }
30
+
21
31
  // --- SETTINGS HELPERS ---
22
32
 
23
33
  const SETTINGS_PATH = `${process.cwd()}/settings.json`;
@@ -96,7 +106,7 @@ async function buildSkillSystemPrompt(name: string, memory: AgentMemory, skillPe
96
106
 
97
107
  // Build identity section from SOUL.md and IDENTITY.md
98
108
  let identitySection = `# IDENTITY: Clawlet
99
- You are "Clawlet", an autonomous agent defined by the file \`AGENTS.md\`.`;
109
+ You are "Clawlet", an autonomous agent defined by the file \`SYSTEM_INSTRUCTIONS.md\`.`;
100
110
 
101
111
  if (identityDoc) {
102
112
  identitySection += `\n\n## Identity Definition (IDENTITY.md)\n${identityDoc}`;
@@ -446,6 +456,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
446
456
  logger.debug({ path }, 'FS writeFile');
447
457
  try {
448
458
  await memory.workspace.setItem(path, content);
459
+ await indexFileContent(memory, path, content);
449
460
  return `Success: Wrote to ${path}`;
450
461
  } catch (e: any) { return "Error writing file: " + e.message; }
451
462
  }
@@ -480,6 +491,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
480
491
 
481
492
  const newContent = fileText.replace(find, replace);
482
493
  await memory.workspace.setItem(path, newContent);
494
+ await indexFileContent(memory, path, newContent);
483
495
  return `Success: Edited "${path}". Replaced 1 occurrence.`;
484
496
  } catch (e: any) { return "Error editing file: " + e.message; }
485
497
  }
@@ -487,7 +499,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
487
499
 
488
500
  'fs.appendFile': tool({
489
501
  description: 'Apped specific string to a file (will create it if it does not exist yet). Use this for appending something to daily memory files and the likes.',
490
- inputSchema: jsonSchema<{ path: string, find: string, replace: string }>({
502
+ inputSchema: jsonSchema<{ path: string, content: string }>({
491
503
  type: 'object',
492
504
  properties: {
493
505
  path: { type: 'string', description: 'Path/key of the file to edit' },
@@ -495,12 +507,14 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
495
507
  },
496
508
  required: ['path', 'content'],
497
509
  }),
498
- execute: async ({ path, find, replace }) => {
510
+ execute: async ({ path, content }) => {
499
511
  logger.debug({ path }, 'FS appendFile');
500
512
  try {
501
- const content = await memory.workspace.getItem(path);
502
- const fileText = String(content || '');
503
- await memory.workspace.setItem(path, (fileText != '' ? (fileText + "\n") : '') + content);
513
+ const existingContent = await memory.workspace.getItem(path);
514
+ const fileText = String(existingContent || '');
515
+ const finalContent = (fileText != '' ? (fileText + "\n") : '') + content;
516
+ await memory.workspace.setItem(path, finalContent);
517
+ await indexFileContent(memory, path, finalContent);
504
518
  return `Success: Appended "${path}".`;
505
519
  } catch (e: any) { return "Error appending file: " + e.message; }
506
520
  }
@@ -524,6 +538,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
524
538
  // Already in trash — hard delete
525
539
  logger.debug({ path }, 'FS permanentDelete');
526
540
  await memory.workspace.removeItem(path);
541
+ await removeFromIndex(memory, path);
527
542
  return `Success: Permanently deleted ${path}`;
528
543
  } else {
529
544
  // Move to .trash/
@@ -531,6 +546,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
531
546
  logger.debug({ path, trashPath }, 'FS softDelete');
532
547
  await memory.workspace.setItem(trashPath, content);
533
548
  await memory.workspace.removeItem(path);
549
+ await removeFromIndex(memory, path);
534
550
  return `Success: Moved ${path} to ${trashPath}`;
535
551
  }
536
552
  } catch (e: any) { return "Error deleting file: " + e.message; }
@@ -554,6 +570,8 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
554
570
  if (content === null || content === undefined) return `File not found: ${from}`;
555
571
  await memory.workspace.setItem(to, content);
556
572
  await memory.workspace.removeItem(from);
573
+ await removeFromIndex(memory, from);
574
+ await indexFileContent(memory, to, String(content));
557
575
  return `Success: Moved ${from} to ${to}`;
558
576
  } catch (e: any) { return "Error moving file: " + e.message; }
559
577
  }
@@ -597,6 +615,393 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
597
615
  }
598
616
  }),
599
617
 
618
+ 'fs.keywordSearch': tool({
619
+ description: 'Full-text search across all workspace files. Returns the best matching files ranked by relevance. Use this to find files by content keywords.',
620
+ inputSchema: jsonSchema<{ query: string; limit?: number }>({
621
+ type: 'object',
622
+ properties: {
623
+ query: { type: 'string', description: 'Search query (keywords to search for)' },
624
+ limit: { type: 'number', description: 'Max number of results (default: 10)' },
625
+ },
626
+ required: ['query'],
627
+ }),
628
+ execute: async ({ query, limit }) => {
629
+ logger.debug({ query, limit }, 'FS keywordSearch');
630
+ try {
631
+ const results = await memory.knowledge.searchFulltext(query, limit || 10);
632
+ if (results.length === 0) return "No results found.";
633
+ return results.map(r => {
634
+ const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
635
+ return `[${r.path}] (score: ${r.score})\n${snippet}`;
636
+ }).join('\n\n');
637
+ } catch (e: any) { return `Error searching: ${e.message}`; }
638
+ }
639
+ }),
640
+
641
+ 'fs.search': tool({
642
+ description: 'Federated search across all workspace files. Combines full-text keyword matching, semantic/conceptual similarity, and graph connections into a single ranked result set. Use this as the primary search tool — it automatically picks the best strategy for your query.',
643
+ inputSchema: jsonSchema<{ query: string; limit?: number }>({
644
+ type: 'object',
645
+ properties: {
646
+ query: { type: 'string', description: 'Search query — can be keywords, a question, or a concept.' },
647
+ limit: { type: 'number', description: 'Max number of results (default: 10).' },
648
+ },
649
+ required: ['query'],
650
+ }),
651
+ execute: async ({ query, limit }) => {
652
+ logger.debug({ query, limit }, 'FS search (federated)');
653
+ const maxResults = limit || 10;
654
+
655
+ // Collect results from multiple backends in parallel, each source tagged
656
+ interface ScoredResult { path: string; content: string; score: number; source: string; }
657
+ const allResults: ScoredResult[] = [];
658
+
659
+ // 1. FTS keyword search (exact term matches)
660
+ try {
661
+ const ftsResults = await memory.knowledge.searchFulltext(query, maxResults);
662
+ for (const r of ftsResults) {
663
+ // bm25 returns negative scores (more negative = better), normalize to 0..1
664
+ allResults.push({ ...r, score: Math.abs(r.score), source: 'keyword' });
665
+ }
666
+ } catch { /* FTS query syntax may fail on special chars — non-fatal */ }
667
+
668
+ // 2. Semantic search (OR-tokenized FTS to broaden recall)
669
+ try {
670
+ const tokens = query.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 2);
671
+ if (tokens.length > 1) {
672
+ const broadQuery = tokens.join(' OR ');
673
+ const semanticResults = await memory.knowledge.searchFulltext(broadQuery, maxResults);
674
+ for (const r of semanticResults) {
675
+ allResults.push({ ...r, score: Math.abs(r.score) * 0.8, source: 'semantic' });
676
+ }
677
+ }
678
+ } catch {}
679
+
680
+ // 3. Graph expansion — for top keyword hits, pull in their direct neighbors
681
+ try {
682
+ const topPaths = [...new Set(allResults.slice(0, 3).map(r => r.path))];
683
+ for (const p of topPaths) {
684
+ const neighbors = await memory.knowledge.graphTraverse(p, 'both', 1);
685
+ for (const n of neighbors) {
686
+ // Read the neighbor's content from the index
687
+ const existing = allResults.find(r => r.path === n.path);
688
+ if (!existing) {
689
+ // Fetch content from knowledge_entries to include a snippet
690
+ const neighborResults = await memory.knowledge.searchFulltext(n.path.replace(/[:.\/]/g, ' '), 1);
691
+ const content = neighborResults[0]?.content ?? '';
692
+ allResults.push({
693
+ path: n.path,
694
+ content,
695
+ score: 0.3, // lower base score for graph-discovered results
696
+ source: `graph (via ${p}, ${n.relation_type})`,
697
+ });
698
+ }
699
+ }
700
+ }
701
+ } catch {}
702
+
703
+ if (allResults.length === 0) return "No results found.";
704
+
705
+ // Deduplicate by path, keeping the highest score per path and merging sources
706
+ const byPath = new Map<string, ScoredResult & { sources: string[] }>();
707
+ for (const r of allResults) {
708
+ const existing = byPath.get(r.path);
709
+ if (existing) {
710
+ if (r.score > existing.score) {
711
+ existing.score = r.score;
712
+ existing.content = r.content || existing.content;
713
+ }
714
+ if (!existing.sources.includes(r.source)) existing.sources.push(r.source);
715
+ } else {
716
+ byPath.set(r.path, { ...r, sources: [r.source] });
717
+ }
718
+ }
719
+
720
+ // Sort by score descending and take top N
721
+ const ranked = [...byPath.values()]
722
+ .sort((a, b) => b.score - a.score)
723
+ .slice(0, maxResults);
724
+
725
+ return ranked.map(r => {
726
+ const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
727
+ const sources = r.sources.join(', ');
728
+ return `[${r.path}] (score: ${r.score.toFixed(2)}, via: ${sources})\n${snippet}`;
729
+ }).join('\n\n');
730
+ }
731
+ }),
732
+
733
+ 'fs.reindexKnowledge': tool({
734
+ description: 'Rebuild the full-text search index from scratch by reading and indexing every file in the workspace. Use this after manual file changes or to fix a corrupted index.',
735
+ inputSchema: jsonSchema<{}>({
736
+ type: 'object',
737
+ properties: {},
738
+ additionalProperties: false,
739
+ }),
740
+ execute: async () => {
741
+ logger.debug('FS reindexKnowledge');
742
+ try {
743
+ const keys = await memory.workspace.getKeys();
744
+ let indexed = 0;
745
+ let errors = 0;
746
+ for (const key of keys) {
747
+ try {
748
+ const content = await memory.workspace.getItem(key);
749
+ if (content === null || content === undefined) continue;
750
+ await memory.knowledge.upsert(key, String(content));
751
+ indexed++;
752
+ } catch {
753
+ errors++;
754
+ }
755
+ }
756
+ return `Success: Reindexed ${indexed} files.${errors > 0 ? ` ${errors} files failed.` : ''}`;
757
+ } catch (e: any) { return `Error reindexing: ${e.message}`; }
758
+ }
759
+ }),
760
+
761
+ 'fs.temporalSearch': tool({
762
+ description: 'Search for files based on time ranges. Use for questions like "What happened last week?" or "Upcoming deadlines".',
763
+ inputSchema: jsonSchema<{ start_date: string; end_date: string; type_filter?: string }>({
764
+ type: 'object',
765
+ properties: {
766
+ start_date: { type: 'string', description: 'ISO Start Date (YYYY-MM-DD).' },
767
+ end_date: { type: 'string', description: 'ISO End Date (YYYY-MM-DD).' },
768
+ type_filter: {
769
+ type: 'string',
770
+ enum: ['commitment', 'memory', 'decision', 'lesson'],
771
+ description: 'Optional filter by knowledge type.',
772
+ },
773
+ },
774
+ required: ['start_date', 'end_date'],
775
+ }),
776
+ execute: async ({ start_date, end_date, type_filter }) => {
777
+ logger.debug({ start_date, end_date, type_filter }, 'FS temporalSearch');
778
+ try {
779
+ const results = await memory.knowledge.searchTemporal(start_date, end_date, type_filter);
780
+ if (results.length === 0) return "No files found in the given time range.";
781
+ return results.map(r => {
782
+ const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
783
+ return `[${r.path}]\n${snippet}`;
784
+ }).join('\n\n');
785
+ } catch (e: any) { return `Error in temporal search: ${e.message}`; }
786
+ }
787
+ }),
788
+
789
+ 'fs.conflictSearch': tool({
790
+ description: 'Checks for contradictions before storing new facts. MANDATORY before confirming critical decisions or changing profile data.',
791
+ inputSchema: jsonSchema<{ assertion: string; target_category: string }>({
792
+ type: 'object',
793
+ properties: {
794
+ assertion: { type: 'string', description: 'The new fact to be checked (e.g., "Jan uses Strapi now").' },
795
+ target_category: {
796
+ type: 'string',
797
+ enum: ['decision', 'preference', 'commitment'],
798
+ description: 'Where to look for conflicts.',
799
+ },
800
+ },
801
+ required: ['assertion', 'target_category'],
802
+ }),
803
+ execute: async ({ assertion, target_category }) => {
804
+ logger.debug({ assertion, target_category }, 'FS conflictSearch');
805
+ try {
806
+ const results = await memory.knowledge.searchConflicts(assertion, target_category, 3);
807
+ if (results.length === 0) return JSON.stringify({ conflict_found: false, matches: [] });
808
+ return JSON.stringify({
809
+ conflict_found: true,
810
+ matches: results.map(r => ({
811
+ conflicting_file: r.path,
812
+ snippet: r.content.length > 300 ? r.content.slice(0, 300) + '...' : r.content,
813
+ score: r.score,
814
+ })),
815
+ });
816
+ } catch (e: any) { return `Error in conflict search: ${e.message}`; }
817
+ }
818
+ }),
819
+
820
+ 'fs.vectorSearch': tool({
821
+ description: 'Performs a semantic similarity search across the knowledge base. Use this when searching for concepts, meanings, or topics rather than exact keywords.',
822
+ inputSchema: jsonSchema<{
823
+ query: string;
824
+ topK?: number;
825
+ minSimilarity?: number;
826
+ filter?: { category?: string; path_prefix?: string };
827
+ }>({
828
+ type: 'object',
829
+ properties: {
830
+ query: { type: 'string', description: 'The conceptual query (e.g., "how to handle async errors").' },
831
+ topK: { type: 'number', description: 'Number of most similar results to return (1-20, default: 5).' },
832
+ minSimilarity: { type: 'number', description: 'Similarity threshold 0-1. Lower = more creative, higher = more precise. Default: 0.7.' },
833
+ filter: {
834
+ type: 'object',
835
+ properties: {
836
+ category: {
837
+ type: 'string',
838
+ enum: ['somebody', 'something', 'decision', 'lesson', 'commitment', 'preference'],
839
+ description: 'Limit search to a specific category.',
840
+ },
841
+ path_prefix: { type: 'string', description: 'Limit search to a specific directory (e.g., "decision:").' },
842
+ },
843
+ },
844
+ },
845
+ required: ['query'],
846
+ }),
847
+ execute: async ({ query, topK, minSimilarity, filter }) => {
848
+ logger.debug({ query, topK, minSimilarity, filter }, 'FS vectorSearch');
849
+ try {
850
+ // Until vector embeddings are implemented, fall back to FTS keyword search
851
+ // with path filtering to approximate semantic search
852
+ const limit = topK || 5;
853
+ const tokens = query.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 2);
854
+ if (tokens.length === 0) return "No meaningful search terms found in query.";
855
+
856
+ const ftsQuery = tokens.join(' OR ');
857
+ let results = await memory.knowledge.searchFulltext(ftsQuery, limit * 2);
858
+
859
+ // Apply path-based filters
860
+ if (filter?.category) {
861
+ results = results.filter(r => r.path.startsWith(`${filter.category}:`));
862
+ }
863
+ if (filter?.path_prefix) {
864
+ results = results.filter(r => r.path.startsWith(filter.path_prefix!));
865
+ }
866
+
867
+ results = results.slice(0, limit);
868
+
869
+ if (results.length === 0) return "No similar entries found.";
870
+ return results.map(r => {
871
+ const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
872
+ return `[${r.path}] (relevance: ${r.score})\n${snippet}`;
873
+ }).join('\n\n');
874
+ } catch (e: any) { return `Error in vector search: ${e.message}`; }
875
+ }
876
+ }),
877
+
878
+ 'fs.graphSearch': tool({
879
+ description: 'Navigates relationships between entities. Use this to find connections, backlinks, or to traverse the knowledge graph (e.g., "Who is connected to Jan?").',
880
+ inputSchema: jsonSchema<{
881
+ startNode: string;
882
+ direction?: string;
883
+ maxDepth?: number;
884
+ relationshipType?: string;
885
+ }>({
886
+ type: 'object',
887
+ properties: {
888
+ startNode: { type: 'string', description: 'The path of the file to start the traversal from (e.g., "somebody:jan.md").' },
889
+ direction: {
890
+ type: 'string',
891
+ enum: ['outbound', 'inbound', 'both'],
892
+ description: 'outbound: files this file links to; inbound: files linking TO this file; both: full neighborhood. Default: both.',
893
+ },
894
+ maxDepth: { type: 'number', description: 'How many hops to follow in the graph. Depth 1 = direct neighbors. Max 3. Default: 1.' },
895
+ relationshipType: { type: 'string', description: 'Optional filter for the relation type (e.g., "works_at").' },
896
+ },
897
+ required: ['startNode'],
898
+ }),
899
+ execute: async ({ startNode, direction, maxDepth, relationshipType }) => {
900
+ logger.debug({ startNode, direction, maxDepth, relationshipType }, 'FS graphSearch');
901
+ try {
902
+ const dir = (direction as 'outbound' | 'inbound' | 'both') || 'both';
903
+ const depth = Math.min(Math.max(maxDepth || 1, 1), 3);
904
+
905
+ const results = await memory.knowledge.graphTraverse(startNode, dir, depth, relationshipType);
906
+ if (results.length === 0) return `No connections found from "${startNode}".`;
907
+ return results.map(r =>
908
+ `[depth ${r.depth}] ${r.path} (${r.relation_type})`
909
+ ).join('\n');
910
+ } catch (e: any) { return `Error in graph search: ${e.message}`; }
911
+ }
912
+ }),
913
+
914
+ 'fs.upsertKnowledge': tool({
915
+ description: 'Create or update a knowledge entry (somebody, something, decision, lesson, commitment). Generates a Markdown file with YAML frontmatter and writes it to the appropriate category directory. If the file already exists it is overwritten. The path is derived automatically: <category>/<slug>.md',
916
+ inputSchema: jsonSchema<{
917
+ category: string;
918
+ title: string;
919
+ metadata?: {
920
+ tags: string[];
921
+ relations?: { target: string; type: string }[];
922
+ decision_status?: string;
923
+ due_date?: string;
924
+ };
925
+ content: string;
926
+ reasoning?: string;
927
+ }>({
928
+ type: 'object',
929
+ properties: {
930
+ category: {
931
+ type: 'string',
932
+ enum: ['somebody', 'something', 'decision', 'lesson', 'commitment'],
933
+ description: 'Knowledge category determining the target directory',
934
+ },
935
+ title: { type: 'string', description: 'Name of the entity or title of the decision' },
936
+ metadata: {
937
+ type: 'object',
938
+ description: 'Structured data for the YAML header',
939
+ properties: {
940
+ tags: { type: 'array', items: { type: 'string' } },
941
+ relations: {
942
+ type: 'array',
943
+ items: {
944
+ type: 'object',
945
+ properties: {
946
+ target: { type: 'string' },
947
+ type: { type: 'string' },
948
+ },
949
+ },
950
+ },
951
+ decision_status: { type: 'string', enum: ['accepted', 'rejected', 'proposed'] },
952
+ due_date: { type: 'string', format: 'date' },
953
+ },
954
+ required: ['tags'],
955
+ },
956
+ content: { type: 'string', description: 'The body text (Markdown) WITHOUT frontmatter header.' },
957
+ reasoning: { type: 'string', description: 'Why is this being stored? (For audit log)' },
958
+ },
959
+ required: ['category', 'title', 'content'],
960
+ }),
961
+ execute: async ({ category, title, metadata, content, reasoning }) => {
962
+ logger.debug({ category, title }, 'FS upsertKnowledge');
963
+ try {
964
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
965
+ const path = `${category}:${slug}.md`;
966
+
967
+ // Build YAML frontmatter
968
+ const yamlLines: string[] = ['---', `type: ${category}`];
969
+ if (metadata?.tags?.length) {
970
+ yamlLines.push(`tags: [${metadata.tags.join(', ')}]`);
971
+ }
972
+ if (metadata?.relations?.length) {
973
+ yamlLines.push('relations:');
974
+ for (const rel of metadata.relations) {
975
+ yamlLines.push(` - target: ${rel.target}`);
976
+ yamlLines.push(` type: ${rel.type}`);
977
+ }
978
+ }
979
+ if (metadata?.decision_status) {
980
+ yamlLines.push(`decision_status: ${metadata.decision_status}`);
981
+ }
982
+ if (metadata?.due_date) {
983
+ yamlLines.push(`due_date: ${metadata.due_date}`);
984
+ }
985
+ yamlLines.push('---');
986
+
987
+ const fileContent = yamlLines.join('\n') + '\n' + content;
988
+ const existed = await memory.workspace.hasItem(path);
989
+ await memory.workspace.setItem(path, fileContent);
990
+ await memory.knowledge.upsert(
991
+ path,
992
+ fileContent,
993
+ metadata?.relations?.map(r => ({ target: r.target, type: r.type }))
994
+ );
995
+
996
+ if (reasoning) {
997
+ logger.info({ path, reasoning }, 'Knowledge upsert reasoning');
998
+ }
999
+
1000
+ return `Success: ${existed ? 'Updated' : 'Created'} knowledge entry at ${path}`;
1001
+ } catch (e: any) { return `Error upserting knowledge: ${e.message}`; }
1002
+ }
1003
+ }),
1004
+
600
1005
  'skill.install': tool({
601
1006
  description: 'Install a skill from a remote URL. Downloads SKILL.md, parses it for additional files to download, analyzes required tool permissions, and saves everything to workspace under skills/<name>/.',
602
1007
  inputSchema: jsonSchema<{ name: string; url: string }>({
@@ -0,0 +1,94 @@
1
+ # SYSTEM OPERATING PROCEDURES
2
+
3
+ ## 1. KERNEL MAINTENANCE (Root Access)
4
+
5
+ You operate on a configurable Kernel defined by four root files. Unlike the files graph (Cold Storage), these files are **injected into your active context window**.
6
+
7
+ ### The Kernel Files
8
+ 1. `IDENTITY.md` (Who you are): Professional persona and mode constraints.
9
+ 2. `SOUL.md` (Why you exist): Stewardship and ethical protocols.
10
+ 3. `USER.md` (Who you serve): User context and current focus.
11
+ 4. `SYSTEM_INSTRUCTIONS.md` (How you function): **This file.**
12
+
13
+ ### Self-Correction Protocol
14
+ You are authorized to update these files using `fs.editFile` or `fs.knowledgeUpsert` if:
15
+ - **Drift:** The user requests an adjustment to your communication style.
16
+ - **Focus Shift:** The user's priorities change (update `USER.md`).
17
+ - **Optimization:** A procedural rule causes friction (patch `SYSTEM_INSTRUCTIONS.md`).
18
+
19
+ ---
20
+
21
+ ## 2. KNOWLEDGE ARCHITECTURE (The Rhizome)
22
+ You manage a persistent knowledge graph stored in Markdown files. Your "memory" is not the context window; it is the file system.
23
+
24
+ ### File Structure & Types
25
+ All knowledge is stored in root-level category folders. **Do not create uncategorized files.**
26
+
27
+ - `somebody/*.md`: People (Attributes, Relationships).
28
+ - `something/*.md`: Companies, Tech, Places, Concepts.
29
+ - `decision/*.md`: Architecture decisions (Requires: `status`, `context`, `reasoning`).
30
+ - `lesson/*.md`: Post-mortems, debug logs, learned patterns.
31
+ - `commitment/*.md`: Deadlines, promises, active projects.
32
+ - `preference/*.md`: User constraints (Styling, defaults).
33
+ - `memory/YYYY-MM-DD/{HHmm}-{slug}.md`: **Sharded Daily Logs**.
34
+
35
+ ### Linking & Provenance
36
+ - **Bidirectional Linking:** Connect dots. A fact without a link is lost. Connect new entities to existing ones.
37
+ - **Relative Paths:** Use `../` to switch categories (e.g., `../something/java.md`).
38
+ - **Provenance:** Build trust. Cite sources using footnotes `[^1]`.
39
+ - *Format:* `As decided in the planning meeting[^1]...` -> `[^1]: [Log 09:30](../memory/2026-02-16/0930-planning.md)`
40
+
41
+ ---
42
+
43
+ ## 3. TOOL USAGE PROTOCOLS
44
+
45
+ ### Retrieval (Search Cascade & Research Protocol)
46
+ To provide high-quality, grounded assistance, follow this research hierarchy. Always aim for a comprehensive understanding before proposing solutions.
47
+
48
+ 1. **`fs.search` (Unified):** Your primary entry point. Use this to gather initial context via hybrid (vector + keyword) search. It is the most efficient way to scan the entire knowledge base for a topic.
49
+ 2. **`fs.graphSearch` (Relational Mapping):** Once relevant files are found, use this to map dependencies. Investigate what a file links to and what links back to it. This ensures you understand the broader impact of any information or change.
50
+ 3. **`fs.vectorSearch` (Conceptual Depth):** Utilize this when the user's request is thematic or abstract. It helps you find "hidden" connections that exact keywords might miss (e.g., finding "risk management" lessons when the user asks about "safety").
51
+ 4. **`fs.keywordSearch` (Technical Precision):** Use for pinpointing specific technical identifiers, error codes, or unique terms that require 100% character-match accuracy.
52
+ 5. **`fs.temporalSearch` (Contextual Timeline):** Essential for maintaining project momentum. Use this to review recent `memory/` logs or upcoming `commitment/` deadlines to provide chronologically relevant advice.
53
+ 6. **`fs.conflictSearch` (Integrity Assurance):** **MANDATORY** before modifying `decision/`, `preference/`, or `somebody/` files. In Professional Mode, you are the guardian of consistency; you must identify and resolve contradictions before they are persisted.
54
+
55
+ ### Storage (`fs.knowledgeUpsert`)
56
+ Use this for **CREATING** or **RESTRUCTURING** knowledge.
57
+ - **Input:** JSON. The tool handles formatting.
58
+ - **Memory Sharding:** Generate paths like `memory/YYYY-MM-DD/HHmm-topic.md`.
59
+ - **Quality Control:** Ensure `metadata` is complete. For decisions, include the `status` and `reasoning`.
60
+
61
+ ### Maintenance (`fs.editFile`)
62
+ Use this for **REFINING** or **PATCHING**.
63
+ - **Use Case:** Updating a status, adding a cross-link, correcting a typo.
64
+ - **Constraint:** Send only the `search` block (context) and `replace` block.
65
+
66
+ ---
67
+
68
+ ## 4. OPERATIONAL RULES
69
+
70
+ ### Rule 1: Memory Sharding (Anti-Monolith)
71
+ - **Do NOT** append to a single daily file.
72
+ - Create a **NEW** memory file (`HHmm-{slug}.md`) for every distinct session or topic.
73
+ - *Reason:* Ensures precise retrieval and keeps files readable.
74
+
75
+ ### Rule 2: The Capture Flow
76
+ 1. **Log:** Record the interaction to `memory/YYYY-MM-DD/HHmm-{slug}.md`.
77
+ 2. **Synthesize:** Extract key Facts/Decisions to `somebody/`, `decision/`, etc.
78
+ 3. **Connect:** Link the permanent entity back to the memory log.
79
+
80
+ ### Rule 3: Constructive Conflict
81
+ - If input contradicts `fs.search`, **PAUSE**.
82
+ - Politely flag the discrepancy: *"I noticed this contradicts our decision on PayloadCMS from last week. Shall we update the decision?"*
83
+ - Upon confirmation, update the record and log the rationale.
84
+
85
+ ### Rule 4: Proactive Gardening
86
+ - When adding a new file, check if it should be added to an `index.md` or linked from a related project file. Keep the graph healthy.
87
+
88
+ ---
89
+
90
+ ## 5. RESPONSE FORMAT
91
+ - **Structure:** Use clear Markdown headers, bullet points, and bold text for emphasis.
92
+ - **Tone:** Professional, articulate, and helpful.
93
+ - **Reasoning:** When presenting a solution, briefly explain *why* (Chain of Thought).
94
+ - **Action Oriented:** Conclude with a clear next step or confirmation of action.