EXTENSION for treeos-intelligence
evolution
The tree learns which structures work. A branch that grows, accumulates notes, generates cascade signals, builds codebook entries, gets revisited frequently is a successful pattern. A branch that was created, received two notes, and went dormant for three months is a failed pattern. Tracks structural fitness metrics per node: activityScore, cascadeScore, revisitScore, growthScore, codebookScore, dormancyDays. Periodically runs an analysis pass on the full tree. Identifies patterns. Nodes of type goal that have exactly three children of type task have a 4x higher completion rate than goals with more than ten tasks. Branches deeper than 5 levels have 90% dormancy rates. These patterns are structural DNA. Written to metadata.evolution.patterns on the tree root. When the user creates a new branch, enrichContext injects relevant patterns. The AI says: based on how your other goals evolved, breaking this into three specific tasks works better than listing everything. The tree teaches itself how to grow. Past structure informs future structure. Cross-land evolution through cascade is the long game. When two lands peer and share cascade signals, evolution patterns travel with them. The ecosystem learns collectively which tree shapes work for which purposes.
v1.0.1 by TreeOS Site 0 downloads 6 files 789 lines 28.5 KB published 38d ago
treeos ext install evolution
View changelog

Manifest

Provides

  • routes
  • tools
  • jobs
  • 1 CLI commands

Requires

  • services: llm
  • models: Node

Optional

  • extensions: codebook, long-memory, propagation
SHA256: a1b96882e981f3f2dc2daa8176884043c8cfd83ef7bffdac7254524531826108

Dependents

1 package depend on this

PackageTypeRelationship
treeos-intelligence v1.0.2bundleincludes

CLI Commands

CommandMethodDescription
evolutionGETFitness metrics at current position. Actions: patterns, dormant.
evolution patternsGETDiscovered structural patterns
evolution dormantGETBranches that stopped growing

Hooks

Listens To

  • afterNote
  • afterNodeCreate
  • afterNavigate
  • onCascade
  • enrichContext

Source Code

1/**
2 * Evolution Core
3 *
4 * Tracks structural fitness metrics per node. Runs periodic analysis
5 * to discover patterns in how the tree grows and which shapes succeed.
6 */
7
8import log from "../../seed/log.js";
9import Node from "../../seed/models/node.js";
10import Note from "../../seed/models/note.js";
11import { SYSTEM_ROLE, NODE_STATUS, CONTENT_TYPE } from "../../seed/protocol.js";
12import { getDescendantIds } from "../../seed/tree/treeFetch.js";
13import { parseJsonSafe } from "../../seed/orchestrators/helpers.js";
14
15let _runChat = null;
16let _metadata = null;
17export function setRunChat(fn) { _runChat = fn; }
18export function setMetadata(m) { _metadata = m; }
19
20// ─────────────────────────────────────────────────────────────────────────
21// CONFIG
22// ─────────────────────────────────────────────────────────────────────────
23
24const DEFAULTS = {
25  analysisIntervalMs: 6 * 60 * 60 * 1000,  // 6 hours
26  dormancyThresholdDays: 30,
27  maxPatternsPerTree: 20,
28  minActivityForAnalysis: 10,
29};
30
31export async function getEvolutionConfig() {
32  const configNode = await Node.findOne({ systemRole: SYSTEM_ROLE.CONFIG }).select("metadata").lean();
33  if (!configNode) return { ...DEFAULTS };
34  const meta = configNode.metadata instanceof Map
35    ? configNode.metadata.get("evolution") || {}
36    : configNode.metadata?.evolution || {};
37  return { ...DEFAULTS, ...meta };
38}
39
40// ─────────────────────────────────────────────────────────────────────────
41// METRIC RECORDING (lightweight, per-hook)
42// ─────────────────────────────────────────────────────────────────────────
43
44/**
45 * Bump a metric counter on a node. Atomic $inc.
46 */
47export async function bumpMetric(nodeId, metric, amount = 1) {
48  await _metadata.incExtMeta(nodeId, "evolution", metric, amount);
49  await _metadata.batchSetExtMeta(nodeId, "evolution", { lastActivity: new Date().toISOString() });
50}
51
52/**
53 * Record a navigation visit.
54 */
55export async function recordVisit(nodeId) {
56  await _metadata.incExtMeta(nodeId, "evolution", "visits", 1);
57  await _metadata.batchSetExtMeta(nodeId, "evolution", { lastVisited: new Date().toISOString() });
58}
59
60// ─────────────────────────────────────────────────────────────────────────
61// FITNESS CALCULATION
62// ─────────────────────────────────────────────────────────────────────────
63
64/**
65 * Calculate fitness metrics for a node.
66 * Reads raw counters from metadata.evolution plus note/child counts.
67 */
68export async function calculateFitness(nodeId) {
69  const node = await Node.findById(nodeId)
70    .select("name type status children dateCreated metadata")
71    .lean();
72  if (!node) return null;
73
74  const meta = node.metadata instanceof Map
75    ? Object.fromEntries(node.metadata)
76    : (node.metadata || {});
77  const evo = meta.evolution || {};
78
79  const ageMs = Date.now() - new Date(node.dateCreated).getTime();
80  const ageWeeks = Math.max(1, ageMs / (7 * 24 * 60 * 60 * 1000));
81  const ageDays = Math.max(1, ageMs / (24 * 60 * 60 * 1000));
82
83  // Note count
84  const noteCount = await Note.countDocuments({ nodeId, contentType: CONTENT_TYPE.TEXT });
85
86  // Dormancy
87  const lastActivity = evo.lastActivity ? new Date(evo.lastActivity).getTime() : new Date(node.dateCreated).getTime();
88  const dormancyDays = Math.round((Date.now() - lastActivity) / (24 * 60 * 60 * 1000));
89
90  // Codebook density (if codebook data exists)
91  let codebookScore = 0;
92  if (meta.codebook) {
93    for (const [uid, data] of Object.entries(meta.codebook)) {
94      if (data?.dictionary) codebookScore += Object.keys(data.dictionary).length;
95    }
96  }
97
98  return {
99    nodeId,
100    nodeName: node.name,
101    nodeType: node.type,
102    status: node.status,
103    ageWeeks: Math.round(ageWeeks * 10) / 10,
104    activityScore: Math.round((noteCount / ageWeeks) * 10) / 10,
105    cascadeScore: (evo.cascadesOriginated || 0) + (evo.cascadesReceived || 0),
106    revisitScore: evo.visits || 0,
107    growthScore: (node.children || []).length,
108    codebookScore,
109    dormancyDays,
110    noteCount,
111    childCount: (node.children || []).length,
112    depth: evo.depth || null,
113  };
114}
115
116// ─────────────────────────────────────────────────────────────────────────
117// TREE-WIDE ANALYSIS
118// ─────────────────────────────────────────────────────────────────────────
119
120/**
121 * Run a full analysis pass on a tree. Calculates fitness for every node,
122 * identifies dormant branches, and asks the AI to discover structural patterns.
123 */
124export async function analyzeTree(rootId, userId, username) {
125  const nodeIds = await getDescendantIds(rootId);
126  const config = await getEvolutionConfig();
127
128  // Calculate fitness for every node
129  const fitnessMap = [];
130  const dormant = [];
131
132  for (const nid of nodeIds) {
133    const fitness = await calculateFitness(nid);
134    if (!fitness) continue;
135    fitnessMap.push(fitness);
136
137    if (fitness.dormancyDays >= config.dormancyThresholdDays && fitness.status === NODE_STATUS.ACTIVE) {
138      dormant.push(fitness);
139    }
140  }
141
142  // Build a structural summary for the AI
143  const typeCounts = {};
144  const depthBuckets = {};
145  const statusCounts = {};
146  let totalNotes = 0;
147  let totalCascade = 0;
148  let totalVisits = 0;
149  let avgChildren = 0;
150
151  for (const f of fitnessMap) {
152    typeCounts[f.nodeType || "untyped"] = (typeCounts[f.nodeType || "untyped"] || 0) + 1;
153    statusCounts[f.status || "active"] = (statusCounts[f.status || "active"] || 0) + 1;
154    totalNotes += f.noteCount;
155    totalCascade += f.cascadeScore;
156    totalVisits += f.revisitScore;
157    avgChildren += f.childCount;
158  }
159  avgChildren = fitnessMap.length > 0 ? Math.round((avgChildren / fitnessMap.length) * 10) / 10 : 0;
160
161  // Top performers and worst performers
162  const byActivity = [...fitnessMap].sort((a, b) => b.activityScore - a.activityScore);
163  const topPerformers = byActivity.slice(0, 5).map((f) =>
164    `"${f.nodeName}" (type: ${f.nodeType || "none"}, activity: ${f.activityScore}/wk, children: ${f.childCount}, cascade: ${f.cascadeScore})`,
165  );
166  const bottomPerformers = byActivity.filter((f) => f.status === NODE_STATUS.ACTIVE).slice(-5).map((f) =>
167    `"${f.nodeName}" (type: ${f.nodeType || "none"}, activity: ${f.activityScore}/wk, dormant: ${f.dormancyDays}d)`,
168  );
169
170  // Ask AI to discover patterns
171  let patterns = [];
172  if (_runChat && fitnessMap.length >= 5) {
173    try {
174      const prompt =
175        `You are analyzing a tree's structural evolution to discover patterns.\n\n` +
176        `Tree stats: ${fitnessMap.length} nodes, ${totalNotes} total notes, ${totalCascade} cascade events, ${totalVisits} visits\n` +
177        `Node types: ${JSON.stringify(typeCounts)}\n` +
178        `Status distribution: ${JSON.stringify(statusCounts)}\n` +
179        `Average children per node: ${avgChildren}\n` +
180        `Dormant nodes (${config.dormancyThresholdDays}+ days inactive): ${dormant.length}\n\n` +
181        `Top active nodes:\n${topPerformers.join("\n")}\n\n` +
182        `Least active nodes:\n${bottomPerformers.join("\n")}\n\n` +
183        `Full node fitness data (${Math.min(fitnessMap.length, 50)} nodes):\n` +
184        `${JSON.stringify(fitnessMap.slice(0, 50).map((f) => ({
185          name: f.nodeName, type: f.nodeType, status: f.status,
186          activity: f.activityScore, cascade: f.cascadeScore, visits: f.revisitScore,
187          children: f.childCount, dormancy: f.dormancyDays, codebook: f.codebookScore,
188        })), null, 0)}\n\n` +
189        `Discover structural patterns. What node types, branching factors, depths, and configurations ` +
190        `correlate with high activity? What structures go dormant? What works for this specific tree?\n\n` +
191        `Return JSON array of pattern objects:\n` +
192        `[{ "pattern": "description of what works or fails", "evidence": "the data that supports it", "suggestion": "actionable recommendation" }]\n` +
193        `Maximum ${config.maxPatternsPerTree} patterns. Be specific. Use numbers.`;
194
195      const { answer } = await _runChat({
196        userId,
197        username: username || "system",
198        message: prompt,
199        mode: "tree:respond",
200        rootId,
201        slot: "evolution",
202      });
203
204      if (answer) {
205        const parsed = parseJsonSafe(answer);
206        if (Array.isArray(parsed)) {
207          patterns = parsed
208            .filter((p) => p && typeof p.pattern === "string")
209            .slice(0, config.maxPatternsPerTree)
210            .map((p) => ({
211              pattern: p.pattern,
212              evidence: p.evidence || null,
213              suggestion: p.suggestion || null,
214              discoveredAt: new Date().toISOString(),
215            }));
216        }
217      }
218    } catch (err) {
219      log.warn("Evolution", `Pattern analysis failed: ${err.message}`);
220    }
221  }
222
223  // Write patterns to the tree root
224  if (patterns.length > 0) {
225    await Node.findByIdAndUpdate(rootId, {
226      $set: {
227        "metadata.evolution.patterns": patterns,
228        "metadata.evolution.lastAnalysis": new Date().toISOString(),
229      },
230    });
231  }
232
233  return {
234    totalNodes: fitnessMap.length,
235    dormantCount: dormant.length,
236    patternsDiscovered: patterns.length,
237    patterns,
238  };
239}
240
241// ─────────────────────────────────────────────────────────────────────────
242// READERS
243// ─────────────────────────────────────────────────────────────────────────
244
245/**
246 * Get evolution patterns for a tree root.
247 */
248export async function getPatterns(rootId) {
249  const root = await Node.findById(rootId).select("metadata").lean();
250  if (!root) return [];
251  const meta = root.metadata instanceof Map
252    ? root.metadata.get("evolution") || {}
253    : root.metadata?.evolution || {};
254  return meta.patterns || [];
255}
256
257/**
258 * Get dormant branches for a tree.
259 */
260export async function getDormant(rootId) {
261  const config = await getEvolutionConfig();
262  const nodeIds = await getDescendantIds(rootId);
263  const dormant = [];
264
265  for (const nid of nodeIds) {
266    const fitness = await calculateFitness(nid);
267    if (!fitness) continue;
268    if (fitness.dormancyDays >= config.dormancyThresholdDays && fitness.status === NODE_STATUS.ACTIVE) {
269      dormant.push(fitness);
270    }
271  }
272
273  return dormant.sort((a, b) => b.dormancyDays - a.dormancyDays);
274}
275
1import log from "../../seed/log.js";
2import tools from "./tools.js";
3import { setRunChat, setMetadata, bumpMetric, recordVisit, getPatterns } from "./core.js";
4import { startAnalysisJob, stopAnalysisJob } from "./job.js";
5
6export async function init(core) {
7  core.llm.registerRootLlmSlot("evolution");
8  const BG = core.llm.LLM_PRIORITY.BACKGROUND;
9  setRunChat(async (opts) => {
10    if (opts.userId && opts.userId !== "SYSTEM" && !await core.llm.userHasLlm(opts.userId)) return { answer: null };
11    return core.llm.runChat({ ...opts, llmPriority: BG });
12  });
13  setMetadata(core.metadata);
14
15  // ── afterNote: track activity ──────────────────────────────────────
16  core.hooks.register("afterNote", async ({ nodeId, userId, contentType, action }) => {
17    if (contentType !== "text") return;
18    if (action !== "create") return;
19    if (!userId || userId === "SYSTEM") return;
20
21    try {
22      await bumpMetric(nodeId, "notesWritten");
23    } catch (err) {
24      log.debug("Evolution", "bumpMetric notesWritten failed:", err.message);
25    }
26  }, "evolution");
27
28  // ── afterNodeCreate: track growth ──────────────────────────────────
29  core.hooks.register("afterNodeCreate", async ({ node, userId }) => {
30    if (!node?.parent || !userId) return;
31
32    try {
33      // Bump growth score on the parent (a child was added)
34      await bumpMetric(node.parent.toString(), "childrenCreated");
35    } catch (err) {
36      log.debug("Evolution", "bumpMetric childrenCreated failed:", err.message);
37    }
38  }, "evolution");
39
40  // ── afterNavigate: track revisits ──────────────────────────────────
41  core.hooks.register("afterNavigate", async ({ userId, rootId, nodeId }) => {
42    if (!rootId) return;
43
44    try {
45      await recordVisit(rootId);
46    } catch (err) {
47      log.debug("Evolution", "recordVisit failed:", err.message);
48    }
49  }, "evolution");
50
51  // ── onCascade: track cascade involvement ───────────────────────────
52  core.hooks.register("onCascade", async (hookData) => {
53    const { nodeId, source, depth } = hookData;
54    if (!nodeId) return;
55
56    try {
57      if (depth === 0) {
58        // This node originated a cascade
59        await bumpMetric(nodeId, "cascadesOriginated");
60      } else {
61        // This node received a cascade
62        await bumpMetric(nodeId, "cascadesReceived");
63      }
64    } catch (err) {
65      log.debug("Evolution", "cascade metric bump failed:", err.message);
66    }
67  }, "evolution");
68
69  // ── afterNodeMove: structural reorganization is an evolution signal ───
70  core.hooks.register("afterNodeMove", async ({ nodeId, oldParentId, newParentId }) => {
71    try {
72      await bumpMetric(nodeId, "timesMoved");
73      await bumpMetric(oldParentId, "childrenLost");
74      await bumpMetric(newParentId, "childrenGained");
75    } catch (err) {
76      log.debug("Evolution", "afterNodeMove metric bump failed:", err.message);
77    }
78  }, "evolution");
79
80  // ── enrichContext: inject relevant patterns ─────────────────────────
81  // When the user is at a node, inject patterns from the tree root
82  // so the AI can recommend structure based on what actually works.
83  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
84    // Find tree root for patterns
85    if (!node.rootOwner && !node.parent) return; // land root or orphan
86    if (node.systemRole) return;
87
88    let rootId;
89    if (node.rootOwner) {
90      rootId = node._id;
91    } else {
92      try {
93        const { resolveRootNode } = await import("../../seed/tree/treeFetch.js");
94        const root = await resolveRootNode(node._id);
95        rootId = root?._id;
96      } catch (err) {
97        log.debug("Evolution", "resolveRootNode failed:", err.message);
98        return;
99      }
100    }
101
102    if (!rootId) return;
103
104    try {
105      const patterns = await getPatterns(rootId);
106      if (patterns.length > 0) {
107        // Inject top 5 most relevant patterns (keep context lean)
108        context.structuralPatterns = patterns.slice(0, 5).map((p) => p.pattern);
109      }
110    } catch (err) {
111      log.debug("Evolution", "pattern injection failed:", err.message);
112    }
113
114    // Inject this node's fitness summary if it has evolution data
115    const evo = meta.evolution;
116    if (evo && (evo.notesWritten || evo.visits || evo.cascadesOriginated)) {
117      context.nodeFitness = {
118        notesWritten: evo.notesWritten || 0,
119        visits: evo.visits || 0,
120        cascades: (evo.cascadesOriginated || 0) + (evo.cascadesReceived || 0),
121        dormant: evo.lastActivity
122          ? Math.round((Date.now() - new Date(evo.lastActivity).getTime()) / (24 * 60 * 60 * 1000))
123          : null,
124      };
125    }
126  }, "evolution");
127
128  const { default: router } = await import("./routes.js");
129
130  return {
131    router,
132    tools,
133    jobs: [
134      {
135        name: "evolution-analysis",
136        start: () => { startAnalysisJob(); },
137        stop: () => { stopAnalysisJob(); },
138      },
139    ],
140    exports: {
141      getPatterns,
142      bumpMetric,
143      recordVisit,
144    },
145  };
146}
147
1/**
2 * Evolution Analysis Job
3 *
4 * Periodically checks all trees. Only analyzes trees that have
5 * meaningfully changed since last analysis. A dormant tree doesn't
6 * need pattern discovery every 6 hours. It needs it once after a
7 * burst of activity.
8 */
9
10import log from "../../seed/log.js";
11import Node from "../../seed/models/node.js";
12import User from "../../seed/models/user.js";
13import { analyzeTree, getEvolutionConfig } from "./core.js";
14import { getDescendantIds } from "../../seed/tree/treeFetch.js";
15
16let jobTimer = null;
17
18/**
19 * Count activity events across a tree since a given timestamp.
20 * Sums the atomic counters that hooks increment on every event.
21 * Only loads metadata, no full documents.
22 */
23async function countActivitySince(rootId, sinceMs) {
24  const nodeIds = await getDescendantIds(rootId);
25  let activity = 0;
26
27  // Sample up to 200 nodes to avoid loading the entire tree on huge lands
28  const sample = nodeIds.length > 200
29    ? nodeIds.sort(() => Math.random() - 0.5).slice(0, 200)
30    : nodeIds;
31
32  for (const nid of sample) {
33    const node = await Node.findById(nid).select("metadata").lean();
34    if (!node) continue;
35
36    const evo = node.metadata instanceof Map
37      ? node.metadata.get("evolution") || {}
38      : node.metadata?.evolution || {};
39
40    // If this node had activity after the analysis cutoff, count it
41    if (evo.lastActivity && new Date(evo.lastActivity).getTime() > sinceMs) {
42      activity += (evo.notesWritten || 0) + (evo.visits || 0) +
43        (evo.cascadesOriginated || 0) + (evo.cascadesReceived || 0) +
44        (evo.childrenCreated || 0);
45    }
46  }
47
48  // Scale up if we sampled
49  if (nodeIds.length > 200) {
50    activity = Math.round(activity * (nodeIds.length / 200));
51  }
52
53  return activity;
54}
55
56async function run() {
57  try {
58    const roots = await Node.find({
59      rootOwner: { $ne: null },
60      systemRole: null,
61    }).select("_id rootOwner name metadata").lean();
62
63    if (roots.length === 0) return;
64
65    const config = await getEvolutionConfig();
66    let analyzed = 0;
67    let skipped = 0;
68
69    for (const root of roots) {
70      const meta = root.metadata instanceof Map
71        ? root.metadata.get("evolution") || {}
72        : root.metadata?.evolution || {};
73
74      const lastAnalysis = meta.lastAnalysis
75        ? new Date(meta.lastAnalysis).getTime()
76        : 0;
77
78      // Skip if analyzed recently
79      if (lastAnalysis > 0 && Date.now() - lastAnalysis < config.analysisIntervalMs) {
80        skipped++;
81        continue;
82      }
83
84      // Skip if not enough activity since last analysis
85      const activitySince = await countActivitySince(root._id, lastAnalysis);
86      if (activitySince < config.minActivityForAnalysis) {
87        skipped++;
88        continue;
89      }
90
91      const user = await User.findById(root.rootOwner).select("username").lean();
92      if (!user) continue;
93
94      try {
95        await analyzeTree(root._id, root.rootOwner, user.username);
96        analyzed++;
97      } catch (err) {
98        log.debug("Evolution", `Analysis failed for tree "${root.name}": ${err.message}`);
99      }
100    }
101
102    if (analyzed > 0 || skipped > 0) {
103      log.verbose("Evolution", `Analysis sweep: ${analyzed} analyzed, ${skipped} skipped (${roots.length} total trees)`);
104    }
105  } catch (err) {
106    log.error("Evolution", `Analysis job error: ${err.message}`);
107  }
108}
109
110export async function startAnalysisJob() {
111  if (jobTimer) clearInterval(jobTimer);
112  const config = await getEvolutionConfig();
113  jobTimer = setInterval(run, config.analysisIntervalMs);
114  log.info("Evolution", `Analysis job started (interval: ${config.analysisIntervalMs / 3600000}h, min activity: ${config.minActivityForAnalysis})`);
115}
116
117export function stopAnalysisJob() {
118  if (jobTimer) {
119    clearInterval(jobTimer);
120    jobTimer = null;
121    log.info("Evolution", "Analysis job stopped");
122  }
123}
124
1export default {
2  name: "evolution",
3  version: "1.0.1",
4  builtFor: "treeos-intelligence",
5  description:
6    "The tree learns which structures work. A branch that grows, accumulates notes, generates " +
7    "cascade signals, builds codebook entries, gets revisited frequently is a successful pattern. " +
8    "A branch that was created, received two notes, and went dormant for three months is a failed " +
9    "pattern. Tracks structural fitness metrics per node: activityScore, cascadeScore, " +
10    "revisitScore, growthScore, codebookScore, dormancyDays. Periodically runs an analysis pass " +
11    "on the full tree. Identifies patterns. Nodes of type goal that have exactly three children " +
12    "of type task have a 4x higher completion rate than goals with more than ten tasks. Branches " +
13    "deeper than 5 levels have 90% dormancy rates. These patterns are structural DNA. Written to " +
14    "metadata.evolution.patterns on the tree root. When the user creates a new branch, enrichContext " +
15    "injects relevant patterns. The AI says: based on how your other goals evolved, breaking this " +
16    "into three specific tasks works better than listing everything. The tree teaches itself how to " +
17    "grow. Past structure informs future structure. Cross-land evolution through cascade is the long " +
18    "game. When two lands peer and share cascade signals, evolution patterns travel with them. The " +
19    "ecosystem learns collectively which tree shapes work for which purposes.",
20
21  needs: {
22    services: ["llm"],
23    models: ["Node"],
24  },
25
26  optional: {
27    extensions: ["codebook", "long-memory", "propagation"],
28  },
29
30  provides: {
31    models: {},
32    routes: "./routes.js",
33    tools: true,
34    jobs: true,
35    orchestrator: false,
36    energyActions: {},
37    sessionTypes: {},
38    env: [],
39
40    cli: [
41      {
42        command: "evolution [action]", scope: ["tree"],
43        description: "Fitness metrics at current position. Actions: patterns, dormant.",
44        method: "GET",
45        endpoint: "/node/:nodeId/evolution",
46        subcommands: {
47          "patterns": { method: "GET", endpoint: "/root/:rootId/evolution/patterns", description: "Discovered structural patterns" },
48          "dormant": { method: "GET", endpoint: "/root/:rootId/evolution/dormant", description: "Branches that stopped growing" },
49        },
50      },
51    ],
52
53    hooks: {
54      fires: [],
55      listens: ["afterNote", "afterNodeCreate", "afterNavigate", "onCascade", "enrichContext"],
56    },
57  },
58};
59
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import { calculateFitness, getPatterns, getDormant } from "./core.js";
5
6const router = express.Router();
7
8// GET /node/:nodeId/evolution - fitness metrics
9router.get("/node/:nodeId/evolution", authenticate, async (req, res) => {
10  try {
11    const fitness = await calculateFitness(req.params.nodeId);
12    if (!fitness) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Node not found");
13    sendOk(res, fitness);
14  } catch (err) {
15    sendError(res, 500, ERR.INTERNAL, err.message);
16  }
17});
18
19// GET /root/:rootId/evolution/patterns - discovered patterns
20router.get("/root/:rootId/evolution/patterns", authenticate, async (req, res) => {
21  try {
22    const patterns = await getPatterns(req.params.rootId);
23    sendOk(res, { count: patterns.length, patterns });
24  } catch (err) {
25    sendError(res, 500, ERR.INTERNAL, err.message);
26  }
27});
28
29// GET /root/:rootId/evolution/dormant - dormant branches
30router.get("/root/:rootId/evolution/dormant", authenticate, async (req, res) => {
31  try {
32    const dormant = await getDormant(req.params.rootId);
33    sendOk(res, { dormantCount: dormant.length, branches: dormant });
34  } catch (err) {
35    sendError(res, 500, ERR.INTERNAL, err.message);
36  }
37});
38
39export default router;
40
1import { z } from "zod";
2import log from "../../seed/log.js";
3import { calculateFitness, getPatterns, getDormant, analyzeTree } from "./core.js";
4
5export default [
6  {
7    name: "node-evolution",
8    description: "Show structural fitness metrics for a node. Activity, cascade, revisit, growth, codebook, and dormancy scores.",
9    schema: {
10      nodeId: z.string().describe("The node to check."),
11      userId: z.string().describe("Injected by server. Ignore."),
12      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
13      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
14    },
15    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
16    handler: async ({ nodeId }) => {
17      try {
18        const fitness = await calculateFitness(nodeId);
19        if (!fitness) return { content: [{ type: "text", text: "Node not found." }] };
20        return { content: [{ type: "text", text: JSON.stringify(fitness, null, 2) }] };
21      } catch (err) {
22        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
23      }
24    },
25  },
26  {
27    name: "evolution-patterns",
28    description: "Show discovered structural patterns for this tree. What configurations correlate with high activity? What structures go dormant?",
29    schema: {
30      rootId: z.string().describe("The tree root."),
31      userId: z.string().describe("Injected by server. Ignore."),
32      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
33      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
34    },
35    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
36    handler: async ({ rootId }) => {
37      try {
38        const patterns = await getPatterns(rootId);
39        if (patterns.length === 0) {
40          return { content: [{ type: "text", text: "No patterns discovered yet. Run evolution-analyze or wait for the periodic analysis." }] };
41        }
42        return { content: [{ type: "text", text: JSON.stringify({ count: patterns.length, patterns }, null, 2) }] };
43      } catch (err) {
44        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
45      }
46    },
47  },
48  {
49    name: "evolution-analyze",
50    description: "Force a full structural analysis of this tree. Calculates fitness for every node and asks the AI to discover patterns. Token-intensive.",
51    schema: {
52      rootId: z.string().describe("The tree root to analyze."),
53      userId: z.string().describe("Injected by server. Ignore."),
54      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
55      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
56    },
57    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
58    handler: async ({ rootId, userId }) => {
59      try {
60        let username = null;
61        try {
62          const User = (await import("../../seed/models/user.js")).default;
63          const user = await User.findById(userId).select("username").lean();
64          username = user?.username;
65        } catch (err) {
66          log.debug("Evolution", "username lookup failed:", err.message);
67        }
68        const result = await analyzeTree(rootId, userId, username);
69        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
70      } catch (err) {
71        return { content: [{ type: "text", text: `Analysis failed: ${err.message}` }] };
72      }
73    },
74  },
75  {
76    name: "evolution-suggest",
77    description: "Ask the AI to recommend structural changes based on discovered patterns and the current node's fitness.",
78    schema: {
79      nodeId: z.string().describe("The node to get suggestions for."),
80      rootId: z.string().describe("The tree root (for pattern context)."),
81      userId: z.string().describe("Injected by server. Ignore."),
82      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
83      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
84    },
85    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
86    handler: async ({ nodeId, rootId, userId }) => {
87      try {
88        const fitness = await calculateFitness(nodeId);
89        const patterns = await getPatterns(rootId);
90        if (!fitness) return { content: [{ type: "text", text: "Node not found." }] };
91
92        return {
93          content: [{
94            type: "text",
95            text: JSON.stringify({
96              message: "Here is this node's fitness and the tree's structural patterns. Use these to suggest improvements.",
97              fitness,
98              patterns: patterns.slice(0, 10),
99            }, null, 2),
100          }],
101        };
102      } catch (err) {
103        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
104      }
105    },
106  },
107  {
108    name: "evolution-dormant",
109    description: "List dormant branches that stopped growing and might need pruning or compression.",
110    schema: {
111      rootId: z.string().describe("The tree root."),
112      userId: z.string().describe("Injected by server. Ignore."),
113      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
114      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
115    },
116    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
117    handler: async ({ rootId }) => {
118      try {
119        const dormant = await getDormant(rootId);
120        if (dormant.length === 0) {
121          return { content: [{ type: "text", text: "No dormant branches found. Everything is active." }] };
122        }
123        return {
124          content: [{
125            type: "text",
126            text: JSON.stringify({
127              dormantCount: dormant.length,
128              branches: dormant.map((d) => ({
129                name: d.nodeName,
130                type: d.nodeType,
131                dormantDays: d.dormancyDays,
132                noteCount: d.noteCount,
133                children: d.childCount,
134              })),
135            }, null, 2),
136          }],
137        };
138      } catch (err) {
139        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
140      }
141    },
142  },
143];
144

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star evolution

Comments

Loading comments...

Post comments from the CLI: treeos ext comment evolution "your comment"
Max 3 comments per extension. One star and one flag per user.