EXTENSION seed
split
The tree detects when a branch has outgrown its parent. Evolution metrics show the branch has more activity than the rest of the tree combined. Boundary analysis shows it has low cohesion with its siblings. Purpose coherence is dropping because the branch is pulling the thesis in a direction the rest of the tree does not follow. The branch has its own identity through persona. Its own cascade topology. Its own codebook with the user. Split reads all of these signals and proposes: this branch should be its own tree. This is not export. Export strips accumulated data and reproduces form. This is mitosis. The branch takes everything it earned with it. Evolution patterns, codebook dictionaries, memory connections, explore maps, persona definitions, cascade topology. Nothing is stripped. The branch is not starting over. It is graduating. Analysis scores each direct child branch on six dimensions: activity ratio from evolution, similarity score from boundary, coherence against root thesis from purpose, persona divergence, codebook isolation (unique vs shared terms), and cascade self-containment (what percentage of signals originate or terminate within the branch). Each installed intelligence extension adds one dimension. Without any, split has nothing to analyze. Execution creates a new root tree from the branch. Moves all descendant nodes preserving hierarchy via updateParentRelationship. Carries all metadata (structural and accumulated). Sets rootOwner to the current user. Leaves a note on the old parent recording the split. Creates a channel between the old parent and the new root so signals can still flow. The connection remains until the user cuts it. Intent integration: if a branch scores above configurable thresholds on every available metric, intent can propose the split autonomously. The tree notices it has outgrown its own structure. The user reviews. The user decides. But the tree said something. The full lifecycle: a seed is planted, a tree grows, branches form, one branch outgrows the tree, the tree splits, the branch becomes a new tree, the new tree grows, eventually it drops a seed through seed-export, the seed is planted on another land. Birth, growth, mitosis, reproduction, teaching. The biology is complete.
v1.0.1 by TreeOS Site 0 downloads 5 files 742 lines 26.9 KB published 38d ago
treeos ext install split
View changelog

Manifest

Provides

  • routes
  • tools
  • 1 CLI commands
  • 2 energy actions

Requires

  • services: hooks, llm, contributions
  • models: Node, Note

Optional

  • services: energy
  • extensions: evolution, boundary, purpose, persona, codebook, long-memory, channels, inverse-tree, phase, intent
SHA256: 906f2a9fe394e1a1a23942a53ea05c5180f9769c925095fc42e908525ecd0ed4

Dependents

1 package depend on this

PackageTypeRelationship
treeos v1.0.1osstandalone

CLI Commands

CommandMethodDescription
splitPOSTBranch mitosis. Actions: preview, execute, history.
split previewPOSTShow what would happen if this branch splits
split executePOSTSplit this branch into its own tree
split historyGETPast splits from this tree

Source Code

1// Split Core
2//
3// Detect when a branch has outgrown its tree and execute mitosis.
4// Analysis reads signals from every installed intelligence extension.
5// Execution moves the branch into a new root tree with all metadata intact.
6
7import log from "../../seed/log.js";
8import { getDescendantIds } from "../../seed/tree/treeFetch.js";
9import { updateParentRelationship } from "../../seed/tree/treeManagement.js";
10import { invalidateAll } from "../../seed/tree/ancestorCache.js";
11import { getExtension } from "../loader.js";
12import { v4 as uuidv4 } from "uuid";
13
14let Node = null;
15let Note = null;
16let logContribution = async () => {};
17let runChat = null;
18let useEnergy = async () => ({ energyUsed: 0 });
19let _metadata = null;
20
21export function setServices({ models, contributions, llm, energy, metadata }) {
22  Node = models.Node;
23  Note = models.Note;
24  logContribution = contributions.logContribution;
25  runChat = llm.runChat;
26  if (energy?.useEnergy) useEnergy = energy.useEnergy;
27  if (metadata) _metadata = metadata;
28}
29
30// ─────────────────────────────────────────────────────────────────────────
31// DIMENSION SCORERS
32// Each scorer reads one intelligence extension's data and returns
33// { score: 0-1, detail: string, available: true } or { available: false }
34// Score = how strongly this dimension suggests the branch should split.
35// 1.0 = definitely should split. 0.0 = no reason to split.
36// ─────────────────────────────────────────────────────────────────────────
37
38async function scoreActivity(branchId, rootId, branchNodeIds) {
39  const ext = getExtension("evolution");
40  if (!ext?.exports?.getEvolutionReport) return { available: false };
41
42  try {
43    const report = await ext.exports.getEvolutionReport(rootId);
44    if (!report?.fitness) return { available: false };
45
46    // Count activity in branch vs rest of tree
47    const branchSet = new Set(branchNodeIds);
48    let branchActivity = 0;
49    let totalActivity = 0;
50    for (const [nodeId, fitness] of Object.entries(report.fitness)) {
51      const activity = fitness.activity || fitness.score || 0;
52      totalActivity += activity;
53      if (branchSet.has(nodeId)) branchActivity += activity;
54    }
55
56    if (totalActivity === 0) return { available: false };
57    const ratio = branchActivity / totalActivity;
58    return {
59      available: true,
60      score: Math.min(1, ratio * 1.2), // > 83% activity = score 1.0
61      detail: `${Math.round(ratio * 100)}% of tree activity is in this branch`,
62    };
63  } catch { return { available: false }; }
64}
65
66async function scoreBoundary(branchId, rootId) {
67  const ext = getExtension("boundary");
68  if (!ext?.exports?.getBoundaryReport) return { available: false };
69
70  try {
71    const report = await ext.exports.getBoundaryReport(rootId);
72    if (!report?.branches?.[branchId]) return { available: false };
73
74    const branchData = report.branches[branchId];
75    // Low similarity to siblings = high split score
76    // Find this branch's average similarity to all other branches
77    const blurred = (report.findings || []).filter(
78      f => f.type === "blurred" && f.branches?.includes(branchId),
79    );
80    const avgSimilarity = blurred.length > 0
81      ? blurred.reduce((s, f) => s + (f.similarity || 0), 0) / blurred.length
82      : 0;
83
84    // Invert: low similarity to siblings = high split score
85    const score = 1 - avgSimilarity;
86    return {
87      available: true,
88      score: Math.max(0, Math.min(1, score)),
89      detail: `${(avgSimilarity * 100).toFixed(0)}% average similarity to sibling branches`,
90    };
91  } catch { return { available: false }; }
92}
93
94async function scoreCoherence(branchId, rootId) {
95  const ext = getExtension("purpose");
96  if (!ext?.exports?.getThesis) return { available: false };
97
98  try {
99    const thesis = await ext.exports.getThesis(rootId);
100    if (!thesis?.coherence) return { available: false };
101
102    // Check if the branch has its own purpose that diverges from root
103    const branchNode = await Node.findById(branchId).select("metadata").lean();
104    if (!branchNode) return { available: false };
105    const branchPurpose = _metadata.getExtMeta(branchNode, "purpose");
106
107    if (branchPurpose?.coherence != null) {
108      // Low coherence against root thesis = high split score
109      const score = 1 - branchPurpose.coherence;
110      return {
111        available: true,
112        score: Math.max(0, Math.min(1, score)),
113        detail: `Coherence against root thesis: ${(branchPurpose.coherence * 100).toFixed(0)}%`,
114      };
115    }
116
117    return { available: false };
118  } catch { return { available: false }; }
119}
120
121async function scorePersona(branchId, rootId) {
122  const branchNode = await Node.findById(branchId).select("metadata").lean();
123  const rootNode = await Node.findById(rootId).select("metadata").lean();
124  if (!branchNode || !rootNode) return { available: false };
125
126  const branchPersona = _metadata.getExtMeta(branchNode, "persona");
127  const rootPersona = _metadata.getExtMeta(rootNode, "persona");
128
129  if (!branchPersona?.name) return { available: false };
130
131  // Has its own persona different from root
132  const diverged = !rootPersona?.name || branchPersona.name !== rootPersona.name;
133  return {
134    available: true,
135    score: diverged ? 0.8 : 0.2,
136    detail: diverged
137      ? `Has its own persona ("${branchPersona.name}") different from root ("${rootPersona?.name || "none"}")`
138      : `Same persona as root ("${branchPersona.name}")`,
139  };
140}
141
142async function scoreCodebook(branchId, rootId, branchNodeIds) {
143  const ext = getExtension("codebook");
144  if (!ext?.exports?.getCodebookStats) return { available: false };
145
146  try {
147    const stats = await ext.exports.getCodebookStats(rootId);
148    if (!stats?.entries) return { available: false };
149
150    const branchSet = new Set(branchNodeIds);
151    let branchEntries = 0;
152    let sharedEntries = 0;
153    let totalEntries = stats.totalEntries || 0;
154
155    if (stats.byNode) {
156      for (const [nodeId, count] of Object.entries(stats.byNode)) {
157        if (branchSet.has(nodeId)) branchEntries += count;
158      }
159    }
160    // Estimate shared as entries referenced both inside and outside branch
161    sharedEntries = Math.max(0, totalEntries - branchEntries);
162
163    if (totalEntries === 0) return { available: false };
164
165    // High isolation (few shared terms) = high split score
166    const isolation = branchEntries > 0 ? 1 - (sharedEntries / (branchEntries + sharedEntries)) : 0;
167    return {
168      available: true,
169      score: Math.max(0, Math.min(1, isolation)),
170      detail: `${branchEntries} unique entries, ${sharedEntries} shared with parent`,
171    };
172  } catch { return { available: false }; }
173}
174
175async function scoreCascade(branchId, branchNodeIds) {
176  // Check what % of cascade signals originate or terminate in this branch
177  const branchSet = new Set(branchNodeIds);
178  let branchSignals = 0;
179  let totalSignals = 0;
180
181  // Read cascade config from branch nodes
182  for (const nodeId of branchNodeIds.slice(0, 100)) {
183    const node = await Node.findById(nodeId).select("metadata").lean();
184    if (!node) continue;
185    const cascadeMeta = _metadata.getExtMeta(node, "cascade");
186    if (cascadeMeta?.enabled) branchSignals++;
187    totalSignals++;
188  }
189
190  if (totalSignals === 0) return { available: false };
191
192  const containment = branchSignals / totalSignals;
193  return {
194    available: true,
195    score: Math.max(0, Math.min(1, containment)),
196    detail: `${Math.round(containment * 100)}% of branch nodes are cascade-enabled`,
197  };
198}
199
200// ─────────────────────────────────────────────────────────────────────────
201// ANALYZE
202// ─────────────────────────────────────────────────────────────────────────
203
204export async function analyze(rootId, userId) {
205  await useEnergy({ userId, action: "splitAnalyze" });
206
207  const root = await Node.findById(rootId).select("_id name children rootOwner").lean();
208  if (!root) throw new Error("Tree root not found");
209  if (!root.rootOwner) throw new Error("Node is not a tree root");
210
211  // Get direct children (branches to analyze)
212  const branches = [];
213  for (const childId of root.children || []) {
214    const child = await Node.findById(childId).select("_id name systemRole").lean();
215    if (!child || child.systemRole) continue;
216
217    const descendantIds = await getDescendantIds(childId.toString(), { maxResults: 10000 });
218
219    const dimensions = {};
220    dimensions.activity = await scoreActivity(childId.toString(), rootId, descendantIds);
221    dimensions.boundary = await scoreBoundary(childId.toString(), rootId);
222    dimensions.coherence = await scoreCoherence(childId.toString(), rootId);
223    dimensions.persona = await scorePersona(childId.toString(), rootId);
224    dimensions.codebook = await scoreCodebook(childId.toString(), rootId, descendantIds);
225    dimensions.cascade = await scoreCascade(childId.toString(), descendantIds);
226
227    const available = Object.values(dimensions).filter(d => d.available);
228    const avgScore = available.length > 0
229      ? available.reduce((s, d) => s + d.score, 0) / available.length
230      : 0;
231
232    branches.push({
233      branchId: childId.toString(),
234      branchName: child.name,
235      nodeCount: descendantIds.length,
236      dimensions,
237      availableDimensions: available.length,
238      averageScore: Math.round(avgScore * 100) / 100,
239      recommendation: avgScore >= 0.7 ? "strong candidate for split"
240        : avgScore >= 0.5 ? "possible candidate"
241        : "no split recommended",
242    });
243  }
244
245  // Sort by score descending
246  branches.sort((a, b) => b.averageScore - a.averageScore);
247
248  log.verbose("Split", `Analyzed ${branches.length} branches of ${root.name}`);
249
250  return {
251    rootId,
252    rootName: root.name,
253    branches,
254    topCandidate: branches.length > 0 && branches[0].averageScore >= 0.5
255      ? branches[0] : null,
256  };
257}
258
259// ─────────────────────────────────────────────────────────────────────────
260// PREVIEW
261// ─────────────────────────────────────────────────────────────────────────
262
263export async function preview(branchId, userId) {
264  const branch = await Node.findById(branchId).select("_id name parent rootOwner").lean();
265  if (!branch) throw new Error("Branch not found");
266  if (branch.rootOwner) throw new Error("This node is already a tree root");
267
268  const descendantIds = await getDescendantIds(branchId, { maxResults: 10000 });
269
270  // Count metadata namespaces that will travel with the branch
271  const metadataNamespaces = new Set();
272  const sampleNodes = descendantIds.slice(0, 50);
273  for (const nodeId of sampleNodes) {
274    const node = await Node.findById(nodeId).select("metadata").lean();
275    if (!node) continue;
276    const meta = node.metadata instanceof Map
277      ? [...node.metadata.keys()]
278      : Object.keys(node.metadata || {});
279    for (const ns of meta) metadataNamespaces.add(ns);
280  }
281
282  return {
283    branchId,
284    branchName: branch.name,
285    parentId: branch.parent?.toString(),
286    nodeCount: descendantIds.length,
287    metadataCarried: [...metadataNamespaces].sort(),
288    willCreate: `New root tree "${branch.name}"`,
289    willMove: `${descendantIds.length} nodes preserving hierarchy`,
290    willLeave: `Split note on parent node`,
291    willConnect: `Channel between parent and new root`,
292  };
293}
294
295// ─────────────────────────────────────────────────────────────────────────
296// EXECUTE
297// ─────────────────────────────────────────────────────────────────────────
298
299export async function execute(branchId, userId, username) {
300  await useEnergy({ userId, action: "splitExecute" });
301
302  const branch = await Node.findById(branchId).select("_id name parent rootOwner").lean();
303  if (!branch) throw new Error("Branch not found");
304  if (branch.rootOwner) throw new Error("This node is already a tree root");
305
306  const parentId = branch.parent?.toString();
307  if (!parentId) throw new Error("Branch has no parent");
308
309  // Find the current tree root
310  let currentRootId = null;
311  let cursor = await Node.findById(parentId).select("_id parent rootOwner").lean();
312  while (cursor) {
313    if (cursor.rootOwner) { currentRootId = cursor._id.toString(); break; }
314    if (!cursor.parent) break;
315    cursor = await Node.findById(cursor.parent).select("_id parent rootOwner").lean();
316  }
317
318  // Detach from parent and promote to root
319  // 1. Remove from parent's children
320  await Node.updateOne({ _id: parentId }, { $pull: { children: branchId } });
321
322  // 2. Set branch as root (rootOwner = userId, parent = null)
323  await Node.updateOne({ _id: branchId }, {
324    $set: { rootOwner: userId, parent: null },
325  });
326
327  // 3. Update all descendants: rootOwner stays as-is for delegated branches,
328  //    but non-delegated nodes need rootOwner cleared (they inherit from the new root).
329  //    Actually: rootOwner on descendants that pointed to the OLD tree root
330  //    should now point to the NEW root (the branch itself).
331  const descendantIds = await getDescendantIds(branchId, { maxResults: 10000 });
332  // Only update descendants that had rootOwner = old tree root
333  if (currentRootId) {
334    await Node.updateMany(
335      { _id: { $in: descendantIds }, rootOwner: currentRootId },
336      { $set: { rootOwner: branchId } },
337    );
338  }
339
340  // Invalidate cache since we changed the tree topology
341  invalidateAll();
342
343  // 4. Leave a note on the old parent
344  try {
345    const { createNote } = await import("../../seed/tree/notes.js");
346    await createNote({
347      contentType: "text",
348      content: `${branch.name} split into its own tree on ${new Date().toISOString().slice(0, 10)}. ` +
349        `${descendantIds.length} nodes moved.`,
350      userId,
351      nodeId: parentId,
352    });
353  } catch (err) {
354    log.debug("Split", `Failed to leave split note on parent: ${err.message}`);
355  }
356
357  // 5. Create a channel between old parent and new root (if channels extension installed)
358  let channelCreated = false;
359  const channelsExt = getExtension("channels");
360  if (channelsExt?.exports?.createChannel) {
361    try {
362      await channelsExt.exports.createChannel({
363        sourceNodeId: parentId,
364        targetNodeId: branchId,
365        channelName: `split-${branch.name.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30)}`,
366        direction: "bidirectional",
367        userId,
368      });
369      channelCreated = true;
370    } catch (err) {
371      log.debug("Split", `Failed to create post-split channel: ${err.message}`);
372    }
373  }
374
375  // 6. Record split in history on the old tree root
376  if (currentRootId) {
377    try {
378      const oldRoot = await Node.findById(currentRootId);
379      if (oldRoot) {
380        const splitMeta = _metadata.getExtMeta(oldRoot, "split");
381        if (!splitMeta.history) splitMeta.history = [];
382        splitMeta.history.push({
383          branchId,
384          branchName: branch.name,
385          nodeCount: descendantIds.length,
386          splitAt: new Date().toISOString(),
387          splitBy: userId,
388          newRootId: branchId,
389        });
390        await _metadata.setExtMeta(oldRoot, "split", splitMeta);
391      }
392    } catch (err) {
393      log.debug("Split", `Failed to record split history: ${err.message}`);
394    }
395  }
396
397  // 7. Add user's roots list (navigation extension)
398  const navExt = getExtension("navigation");
399  if (navExt?.exports?.addRoot) {
400    try {
401      await navExt.exports.addRoot(userId, branchId);
402    } catch (err) {
403      log.debug("Split", `Failed to add new root to navigation: ${err.message}`);
404    }
405  }
406
407  // Log contribution
408  await logContribution({
409    userId,
410    nodeId: branchId,
411    wasAi: false,
412    action: "split:executed",
413    extensionData: {
414      split: {
415        fromTreeId: currentRootId,
416        branchName: branch.name,
417        nodeCount: descendantIds.length,
418        channelCreated,
419      },
420    },
421  });
422
423  log.info("Split", `Branch "${branch.name}" split from tree ${currentRootId} into new root (${descendantIds.length} nodes)`);
424
425  return {
426    newRootId: branchId,
427    newRootName: branch.name,
428    nodeCount: descendantIds.length,
429    fromTreeId: currentRootId,
430    channelCreated,
431    splitAt: new Date().toISOString(),
432  };
433}
434
435// ─────────────────────────────────────────────────────────────────────────
436// HISTORY
437// ─────────────────────────────────────────────────────────────────────────
438
439export async function getHistory(rootId) {
440  const root = await Node.findById(rootId).select("metadata").lean();
441  if (!root) throw new Error("Tree root not found");
442  const meta = _metadata.getExtMeta(root, "split");
443  return { history: meta.history || [] };
444}
445
1import log from "../../seed/log.js";
2import tools from "./tools.js";
3import { setServices, analyze, preview, execute, getHistory } from "./core.js";
4
5export async function init(core) {
6  core.llm.registerRootLlmSlot?.("split");
7
8  setServices({
9    models: core.models,
10    contributions: core.contributions,
11    llm: core.llm,
12    energy: core.energy || null,
13    metadata: core.metadata,
14  });
15
16  const { default: router } = await import("./routes.js");
17
18  log.info("Split", "Branch mitosis loaded");
19
20  return {
21    router,
22    tools,
23    exports: {
24      analyze,
25      preview,
26      execute,
27      getHistory,
28    },
29  };
30}
31
1export default {
2  name: "split",
3  version: "1.0.1",
4  builtFor: "seed",
5  description:
6    "The tree detects when a branch has outgrown its parent. Evolution metrics show " +
7    "the branch has more activity than the rest of the tree combined. Boundary analysis " +
8    "shows it has low cohesion with its siblings. Purpose coherence is dropping because " +
9    "the branch is pulling the thesis in a direction the rest of the tree does not follow. " +
10    "The branch has its own identity through persona. Its own cascade topology. Its own " +
11    "codebook with the user." +
12    "\n\n" +
13    "Split reads all of these signals and proposes: this branch should be its own tree." +
14    "\n\n" +
15    "This is not export. Export strips accumulated data and reproduces form. This is " +
16    "mitosis. The branch takes everything it earned with it. Evolution patterns, codebook " +
17    "dictionaries, memory connections, explore maps, persona definitions, cascade topology. " +
18    "Nothing is stripped. The branch is not starting over. It is graduating." +
19    "\n\n" +
20    "Analysis scores each direct child branch on six dimensions: activity ratio from " +
21    "evolution, similarity score from boundary, coherence against root thesis from purpose, " +
22    "persona divergence, codebook isolation (unique vs shared terms), and cascade self-containment " +
23    "(what percentage of signals originate or terminate within the branch). Each installed " +
24    "intelligence extension adds one dimension. Without any, split has nothing to analyze." +
25    "\n\n" +
26    "Execution creates a new root tree from the branch. Moves all descendant nodes " +
27    "preserving hierarchy via updateParentRelationship. Carries all metadata (structural " +
28    "and accumulated). Sets rootOwner to the current user. Leaves a note on the old parent " +
29    "recording the split. Creates a channel between the old parent and the new root so " +
30    "signals can still flow. The connection remains until the user cuts it." +
31    "\n\n" +
32    "Intent integration: if a branch scores above configurable thresholds on every " +
33    "available metric, intent can propose the split autonomously. The tree notices it has " +
34    "outgrown its own structure. The user reviews. The user decides. But the tree said " +
35    "something." +
36    "\n\n" +
37    "The full lifecycle: a seed is planted, a tree grows, branches form, one branch " +
38    "outgrows the tree, the tree splits, the branch becomes a new tree, the new tree " +
39    "grows, eventually it drops a seed through seed-export, the seed is planted on " +
40    "another land. Birth, growth, mitosis, reproduction, teaching. The biology is complete.",
41
42  needs: {
43    services: ["hooks", "llm", "contributions"],
44    models: ["Node", "Note"],
45  },
46
47  optional: {
48    services: ["energy"],
49    extensions: [
50      "evolution",
51      "boundary",
52      "purpose",
53      "persona",
54      "codebook",
55      "long-memory",
56      "channels",
57      "inverse-tree",
58      "phase",
59      "intent",
60    ],
61  },
62
63  provides: {
64    models: {},
65    routes: "./routes.js",
66    tools: true,
67    jobs: false,
68    orchestrator: false,
69    energyActions: {
70      splitAnalyze: { cost: 2 },
71      splitExecute: { cost: 3 },
72    },
73    sessionTypes: {},
74
75    hooks: {
76      fires: [],
77      listens: [],
78    },
79
80    cli: [
81      {
82        command: "split [action]", scope: ["tree"],
83        description: "Branch mitosis. Actions: preview, execute, history.",
84        method: "POST",
85        endpoint: "/root/:rootId/split/analyze",
86        subcommands: {
87          preview: {
88            method: "POST",
89            endpoint: "/node/:nodeId/split/preview",
90            description: "Show what would happen if this branch splits",
91          },
92          execute: {
93            method: "POST",
94            endpoint: "/node/:nodeId/split/execute",
95            description: "Split this branch into its own tree",
96          },
97          history: {
98            method: "GET",
99            endpoint: "/root/:rootId/split/history",
100            description: "Past splits from this tree",
101          },
102        },
103      },
104    ],
105  },
106};
107
1import express from "express";
2import { sendOk, sendError, ERR } from "../../seed/protocol.js";
3import authenticate from "../../seed/middleware/authenticate.js";
4import { analyze, preview, execute, getHistory } from "./core.js";
5
6const router = express.Router();
7
8// POST /root/:rootId/split/analyze - Analyze all branches for split candidates
9router.post("/root/:rootId/split/analyze", authenticate, async (req, res) => {
10  try {
11    const result = await analyze(req.params.rootId, req.userId);
12    sendOk(res, result);
13  } catch (err) {
14    sendError(res, 400, ERR.INVALID_INPUT, err.message);
15  }
16});
17
18// POST /node/:nodeId/split/preview - Preview what a split would do
19router.post("/node/:nodeId/split/preview", authenticate, async (req, res) => {
20  try {
21    const result = await preview(req.params.nodeId, req.userId);
22    sendOk(res, result);
23  } catch (err) {
24    sendError(res, 400, ERR.INVALID_INPUT, err.message);
25  }
26});
27
28// POST /node/:nodeId/split/execute - Execute the split
29router.post("/node/:nodeId/split/execute", authenticate, async (req, res) => {
30  try {
31    const result = await execute(req.params.nodeId, req.userId, req.username);
32    sendOk(res, result, 201);
33  } catch (err) {
34    sendError(res, 400, ERR.INVALID_INPUT, err.message);
35  }
36});
37
38// GET /root/:rootId/split/history - Past splits from this tree
39router.get("/root/:rootId/split/history", authenticate, async (req, res) => {
40  try {
41    const result = await getHistory(req.params.rootId);
42    sendOk(res, result);
43  } catch (err) {
44    sendError(res, 400, ERR.INVALID_INPUT, err.message);
45  }
46});
47
48export default router;
49
1import { z } from "zod";
2import { analyze, preview, execute, getHistory } from "./core.js";
3
4export default [
5  {
6    name: "split-analyze",
7    description:
8      "Analyze all branches of the current tree to find split candidates. Scores each " +
9      "branch on activity, boundary cohesion, purpose coherence, persona divergence, " +
10      "codebook isolation, and cascade containment.",
11    schema: {
12      rootId: z.string().describe("Tree root to analyze."),
13      userId: z.string().describe("Injected by server. Ignore."),
14      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
15      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
16    },
17    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
18    handler: async ({ rootId, userId }) => {
19      try {
20        const result = await analyze(rootId, userId);
21        const summary = {
22          rootName: result.rootName,
23          branchesAnalyzed: result.branches.length,
24          topCandidate: result.topCandidate ? {
25            name: result.topCandidate.branchName,
26            score: result.topCandidate.averageScore,
27            nodeCount: result.topCandidate.nodeCount,
28            recommendation: result.topCandidate.recommendation,
29          } : null,
30          allBranches: result.branches.map(b => ({
31            name: b.branchName,
32            score: b.averageScore,
33            nodeCount: b.nodeCount,
34            recommendation: b.recommendation,
35          })),
36        };
37        return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
38      } catch (err) {
39        return { content: [{ type: "text", text: `Analysis failed: ${err.message}` }] };
40      }
41    },
42  },
43  {
44    name: "split-preview",
45    description:
46      "Preview what would happen if a specific branch splits into its own tree. " +
47      "Shows node count, metadata carried, and connections created.",
48    schema: {
49      nodeId: z.string().describe("Branch node to preview splitting."),
50      userId: z.string().describe("Injected by server. Ignore."),
51      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
52      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
53    },
54    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
55    handler: async ({ nodeId, userId }) => {
56      try {
57        const result = await preview(nodeId, userId);
58        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
59      } catch (err) {
60        return { content: [{ type: "text", text: `Preview failed: ${err.message}` }] };
61      }
62    },
63  },
64  {
65    name: "split-execute",
66    description:
67      "Execute a branch split. The branch becomes its own root tree. All nodes and " +
68      "metadata move with it. A channel is created back to the parent.",
69    schema: {
70      nodeId: z.string().describe("Branch node to split into a new tree."),
71      userId: z.string().describe("Injected by server. Ignore."),
72      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
73      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
74    },
75    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
76    handler: async ({ nodeId, userId }) => {
77      try {
78        const User = (await import("../../seed/models/user.js")).default;
79        const user = await User.findById(userId).select("username").lean();
80        const result = await execute(nodeId, userId, user?.username || "system");
81        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
82      } catch (err) {
83        return { content: [{ type: "text", text: `Split failed: ${err.message}` }] };
84      }
85    },
86  },
87  {
88    name: "split-history",
89    description: "Show past splits from this tree.",
90    schema: {
91      rootId: z.string().describe("Tree root to check."),
92      userId: z.string().describe("Injected by server. Ignore."),
93      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
94      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
95    },
96    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
97    handler: async ({ rootId }) => {
98      try {
99        const result = await getHistory(rootId);
100        if (result.history.length === 0) {
101          return { content: [{ type: "text", text: "No splits have occurred from this tree." }] };
102        }
103        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
104      } catch (err) {
105        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
106      }
107    },
108  },
109];
110

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 split

Comments

Loading comments...

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