EXTENSION for treeos-maintenance
reroot
The tree reorganizes itself. Nodes end up in the wrong place over time. A task that started under Work actually belongs under Side Projects. A note about nutrition is buried three levels deep in a fitness branch when it should be a sibling of the food node. The tree grew organically and the organic structure doesn't match the logical structure anymore. Reroot runs an analysis pass. It reads every node's content, its enrichContext snapshot, its codebook relationships, its cascade connections, its evolution patterns. It builds a similarity graph: which nodes reference similar concepts, share codebook entries, exchange cascade signals frequently, or have overlapping perspective filters. Then it compares the similarity graph against the actual tree structure. Nodes that are semantically close but structurally far apart are candidates for reorganization. Nodes that are structurally adjacent but semantically unrelated are candidates for separation. It generates a reorganization plan. Not a flat 'move X to Y' list. A proposed tree structure showing where every misplaced node would fit better. The AI produces it with constraints: do not break ownership boundaries, do not move nodes with rootOwner set, preserve cascade configurations. The plan writes to metadata.reroot.proposal on the tree root. The user reviews it. reroot preview shows the proposed changes with before/after. reroot apply executes the moves. reroot reject discards the proposal. The tree rebuilt itself. Not by growing new branches. By rearranging the ones it has so the structure matches the meaning.
v1.0.1 by TreeOS Site 0 downloads 4 files 529 lines 17.0 KB published 38d ago
treeos ext install reroot
View changelog

Manifest

Provides

  • 1 CLI commands
  • 1 energy actions

Requires

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

Optional

  • services: energy
  • extensions: codebook, evolution, understanding
SHA256: 7773472e34bd245e2dd972feba1bc835f420fd0175cf432e7ed3b8dcb1df1303

Dependents

1 package depend on this

PackageTypeRelationship
treeos-maintenance v1.0.1bundleincludes

CLI Commands

CommandMethodDescription
rerootPOSTAnalyze and propose reorganization. Actions: preview, apply, reject.
reroot previewGETShow proposed moves with reasons
reroot applyPOSTExecute the reorganization
reroot rejectPOSTDiscard proposal

Source Code

1// Reroot Core
2//
3// Three phases:
4// 1. Analyze: build a semantic snapshot of every node, ask the AI to find misplacements
5// 2. Preview: the proposal lives on metadata.reroot.proposal until accepted or rejected
6// 3. Apply: execute each move via updateParentRelationship (kernel function)
7
8import log from "../../seed/log.js";
9import { updateParentRelationship } from "../../seed/tree/treeManagement.js";
10import { invalidateAll } from "../../seed/tree/ancestorCache.js";
11import { parseJsonSafe } from "../../seed/orchestrators/helpers.js";
12import { getExtension } from "../loader.js";
13
14let Node = null;
15let Note = null;
16let logContribution = null;
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// ANALYZE
32// ─────────────────────────────────────────────────────────────────────────
33
34const ANALYSIS_PROMPT = `You are analyzing a tree's structure to find nodes that are in the wrong place. The tree grew organically. Some nodes ended up far from where they semantically belong.
35
36Below is a snapshot of every node: its ID, name, current parent, depth, and a content summary. Some nodes also have codebook relationships, cascade connections, or evolution data.
37
38Rules:
39- Only propose moves that are clearly justified by semantic similarity
40- Never move nodes with rootOwner set (they own their subtree)
41- Never move system nodes (names starting with .)
42- Preserve cascade configurations (don't break cascade.enabled chains)
43- Each move must name: nodeId, nodeName, currentParentId, proposedParentId, proposedParentName, reason
44- Maximum 10 moves per proposal
45- If the tree is well-organized, return an empty array
46
47Return a JSON array of proposed moves:
48[
49  {
50    "nodeId": "...",
51    "nodeName": "...",
52    "currentParentId": "...",
53    "proposedParentId": "...",
54    "proposedParentName": "...",
55    "reason": "why this node belongs under the proposed parent"
56  }
57]
58
59If no reorganization is needed, return: []
60
61Tree snapshot:
62{snapshot}`;
63
64/**
65 * Analyze a tree and generate a reorganization proposal.
66 */
67export async function analyze(rootId, userId, username) {
68  await useEnergy({ userId, action: "rerootAnalyze" });
69
70  // Build the tree snapshot
71  const snapshot = await buildTreeSnapshot(rootId);
72  if (!snapshot || snapshot.nodes.length === 0) {
73    throw new Error("Tree has no nodes to analyze");
74  }
75
76  // Format for the prompt
77  const snapshotText = formatSnapshot(snapshot);
78
79  // Ask the AI
80  const prompt = ANALYSIS_PROMPT.replace("{snapshot}", snapshotText);
81
82  const result = await runChat({
83    userId,
84    username,
85    message: prompt,
86    mode: "tree:respond",
87    rootId,
88    slot: "reroot",
89  });
90
91  if (!result?.answer) {
92    throw new Error("Analysis produced no result");
93  }
94
95  // Parse the proposed moves
96  const parsed = parseJsonSafe(result.answer);
97  if (!Array.isArray(parsed)) {
98    throw new Error("Analysis did not return a valid move list");
99  }
100
101  const moves = parsed
102    .filter(m => m && m.nodeId && m.proposedParentId && m.reason)
103    .slice(0, 10);
104
105  // Validate each move
106  const validMoves = [];
107  for (const move of moves) {
108    const node = await Node.findById(move.nodeId).select("_id name rootOwner systemRole parent").lean();
109    if (!node) continue;
110    if (node.rootOwner && node.rootOwner !== "SYSTEM") continue; // can't move roots
111    if (node.systemRole) continue; // can't move system nodes
112    if (node.parent?.toString() === move.proposedParentId) continue; // already there
113
114    const newParent = await Node.findById(move.proposedParentId).select("_id name").lean();
115    if (!newParent) continue;
116
117    validMoves.push({
118      nodeId: move.nodeId,
119      nodeName: node.name,
120      currentParentId: node.parent?.toString(),
121      proposedParentId: move.proposedParentId,
122      proposedParentName: newParent.name,
123      reason: move.reason,
124    });
125  }
126
127  // Write proposal to root metadata
128  const root = await Node.findById(rootId);
129  if (!root) throw new Error("Tree root not found");
130
131  const rerootMeta = _metadata.getExtMeta(root, "reroot");
132  rerootMeta.proposal = {
133    moves: validMoves,
134    generatedAt: new Date().toISOString(),
135    generatedBy: userId,
136    status: "pending",
137  };
138  await _metadata.setExtMeta(root, "reroot", rerootMeta);
139
140  log.verbose("Reroot", `Analysis complete for tree ${rootId}: ${validMoves.length} move(s) proposed`);
141
142  return {
143    moves: validMoves,
144    count: validMoves.length,
145  };
146}
147
148// ─────────────────────────────────────────────────────────────────────────
149// PREVIEW
150// ─────────────────────────────────────────────────────────────────────────
151
152export async function getProposal(rootId) {
153  const root = await Node.findById(rootId).select("metadata").lean();
154  if (!root) throw new Error("Tree not found");
155
156  const rerootMeta = _metadata.getExtMeta(root, "reroot");
157  return rerootMeta.proposal || null;
158}
159
160// ─────────────────────────────────────────────────────────────────────────
161// APPLY
162// ─────────────────────────────────────────────────────────────────────────
163
164export async function applyProposal(rootId, userId) {
165  const root = await Node.findById(rootId);
166  if (!root) throw new Error("Tree not found");
167
168  const rerootMeta = _metadata.getExtMeta(root, "reroot");
169  const proposal = rerootMeta.proposal;
170  if (!proposal || proposal.status !== "pending") {
171    throw new Error("No pending proposal to apply");
172  }
173
174  const moves = proposal.moves || [];
175  let applied = 0;
176  let failed = 0;
177  const results = [];
178
179  // Skip cache invalidation on each move. Intermediate cache states between
180  // moves don't matter because no other operation reads during the batch.
181  // One invalidateAll() after the batch is sufficient and avoids ten full clears.
182  for (const move of moves) {
183    try {
184      await updateParentRelationship(
185        move.nodeId,
186        move.proposedParentId,
187        userId,
188        true, // wasAi
189        null, null,
190        { skipCacheInvalidation: true },
191      );
192
193      results.push({ nodeId: move.nodeId, nodeName: move.nodeName, status: "moved", to: move.proposedParentName });
194      applied++;
195    } catch (err) {
196      results.push({ nodeId: move.nodeId, nodeName: move.nodeName, status: "failed", error: err.message });
197      failed++;
198      log.debug("Reroot", `Move failed for ${move.nodeName}: ${err.message}`);
199    }
200  }
201
202  // Single cache clear after all moves complete
203  if (applied > 0) {
204    invalidateAll();
205  }
206
207  // Update proposal status
208  proposal.status = "applied";
209  proposal.appliedAt = new Date().toISOString();
210  proposal.appliedBy = userId;
211  proposal.results = results;
212
213  // Add to history
214  if (!rerootMeta.history) rerootMeta.history = [];
215  rerootMeta.history.push({
216    date: proposal.appliedAt,
217    moves: applied,
218    failed,
219  });
220  if (rerootMeta.history.length > 20) {
221    rerootMeta.history = rerootMeta.history.slice(-20);
222  }
223
224  await _metadata.setExtMeta(root, "reroot", rerootMeta);
225
226  // Log contribution
227  await logContribution({
228    userId,
229    nodeId: rootId,
230    wasAi: true,
231    action: "reroot:applied",
232    extensionData: {
233      reroot: { applied, failed, moves: results },
234    },
235  });
236
237  log.info("Reroot", `Applied reorganization to tree ${rootId}: ${applied} moved, ${failed} failed`);
238
239  return { applied, failed, results };
240}
241
242// ─────────────────────────────────────────────────────────────────────────
243// REJECT
244// ─────────────────────────────────────────────────────────────────────────
245
246export async function rejectProposal(rootId, userId) {
247  const root = await Node.findById(rootId);
248  if (!root) throw new Error("Tree not found");
249
250  const rerootMeta = _metadata.getExtMeta(root, "reroot");
251  if (!rerootMeta.proposal || rerootMeta.proposal.status !== "pending") {
252    throw new Error("No pending proposal to reject");
253  }
254
255  rerootMeta.proposal.status = "rejected";
256  rerootMeta.proposal.rejectedAt = new Date().toISOString();
257  rerootMeta.proposal.rejectedBy = userId;
258  await _metadata.setExtMeta(root, "reroot", rerootMeta);
259
260  log.verbose("Reroot", `Proposal rejected for tree ${rootId}`);
261  return { rejected: true };
262}
263
264// ─────────────────────────────────────────────────────────────────────────
265// SNAPSHOT BUILDER
266// ─────────────────────────────────────────────────────────────────────────
267
268async function buildTreeSnapshot(rootId) {
269  const nodes = await Node.find({
270    rootOwner: rootId,
271    status: { $ne: "trimmed" },
272  })
273    .select("_id name parent children systemRole rootOwner metadata type")
274    .lean();
275
276  if (nodes.length === 0) return { nodes: [] };
277
278  const nodeMap = new Map();
279  for (const n of nodes) nodeMap.set(n._id.toString(), n);
280
281  // Calculate depth for each node
282  function getDepth(nodeId, visited = new Set()) {
283    if (!nodeId || visited.has(nodeId)) return 0;
284    visited.add(nodeId);
285    const node = nodeMap.get(nodeId);
286    if (!node || !node.parent) return 0;
287    return 1 + getDepth(node.parent.toString(), visited);
288  }
289
290  // Get content summary for each node (first note, truncated)
291  const nodeIds = nodes.map(n => n._id.toString());
292  const recentNotes = await Note.find({ nodeId: { $in: nodeIds } })
293    .sort({ dateCreated: -1 })
294    .select("nodeId content")
295    .lean();
296
297  const notesByNode = new Map();
298  for (const note of recentNotes) {
299    const id = note.nodeId.toString();
300    if (!notesByNode.has(id)) {
301      notesByNode.set(id, note.content?.slice(0, 200) || "");
302    }
303  }
304
305  // Get codebook relationships if available
306  let codebookRelations = null;
307  const codebookExt = getExtension("codebook");
308  if (codebookExt?.exports?.getRelationships) {
309    try {
310      codebookRelations = await codebookExt.exports.getRelationships(rootId);
311    } catch (err) {
312      log.debug("Reroot", "Codebook relationships unavailable:", err.message);
313    }
314  }
315
316  // Build snapshot entries
317  const snapshotNodes = nodes
318    .filter(n => !n.systemRole)
319    .map(n => {
320      const id = n._id.toString();
321      return {
322        id,
323        name: n.name,
324        type: n.type || null,
325        parentId: n.parent?.toString() || null,
326        parentName: n.parent ? nodeMap.get(n.parent.toString())?.name || null : null,
327        depth: getDepth(id),
328        childCount: (n.children || []).length,
329        hasRootOwner: !!(n.rootOwner && n.rootOwner !== "SYSTEM"),
330        contentPreview: notesByNode.get(id) || null,
331      };
332    });
333
334  return {
335    nodes: snapshotNodes,
336    codebookRelations,
337  };
338}
339
340function formatSnapshot(snapshot) {
341  const lines = snapshot.nodes.map(n => {
342    let line = `${n.id} | "${n.name}"`;
343    if (n.type) line += ` [${n.type}]`;
344    line += ` | parent: "${n.parentName || "root"}" (${n.parentId || "root"})`;
345    line += ` | depth: ${n.depth} | children: ${n.childCount}`;
346    if (n.hasRootOwner) line += " | HAS_OWNER (do not move)";
347    if (n.contentPreview) line += ` | content: "${n.contentPreview}"`;
348    return line;
349  });
350
351  let text = lines.join("\n");
352
353  if (snapshot.codebookRelations) {
354    text += "\n\nCodebook relationships:\n" + JSON.stringify(snapshot.codebookRelations);
355  }
356
357  return text;
358}
359
1import log from "../../seed/log.js";
2import { setServices } from "./core.js";
3
4export async function init(core) {
5  const BG = core.llm.LLM_PRIORITY.BACKGROUND;
6
7  core.llm.registerRootLlmSlot("reroot");
8
9  setServices({
10    models: core.models,
11    contributions: core.contributions,
12    llm: { ...core.llm, runChat: async (opts) => {
13      if (opts.userId && opts.userId !== "SYSTEM" && !await core.llm.userHasLlm(opts.userId)) return { answer: null };
14      return core.llm.runChat({ ...opts, llmPriority: BG });
15    } },
16    energy: core.energy || null,
17    metadata: core.metadata,
18  });
19
20  const { default: router } = await import("./routes.js");
21
22  log.verbose("Reroot", "Tree reorganization engine loaded");
23
24  return { router };
25}
26
1export default {
2  name: "reroot",
3  version: "1.0.1",
4  builtFor: "treeos-maintenance",
5  description:
6    "The tree reorganizes itself. Nodes end up in the wrong place over time. " +
7    "A task that started under Work actually belongs under Side Projects. A note " +
8    "about nutrition is buried three levels deep in a fitness branch when it should " +
9    "be a sibling of the food node. The tree grew organically and the organic " +
10    "structure doesn't match the logical structure anymore. " +
11    "\n\n" +
12    "Reroot runs an analysis pass. It reads every node's content, its enrichContext " +
13    "snapshot, its codebook relationships, its cascade connections, its evolution " +
14    "patterns. It builds a similarity graph: which nodes reference similar concepts, " +
15    "share codebook entries, exchange cascade signals frequently, or have overlapping " +
16    "perspective filters. " +
17    "\n\n" +
18    "Then it compares the similarity graph against the actual tree structure. Nodes " +
19    "that are semantically close but structurally far apart are candidates for " +
20    "reorganization. Nodes that are structurally adjacent but semantically unrelated " +
21    "are candidates for separation. " +
22    "\n\n" +
23    "It generates a reorganization plan. Not a flat 'move X to Y' list. A proposed " +
24    "tree structure showing where every misplaced node would fit better. The AI " +
25    "produces it with constraints: do not break ownership boundaries, do not move " +
26    "nodes with rootOwner set, preserve cascade configurations. " +
27    "\n\n" +
28    "The plan writes to metadata.reroot.proposal on the tree root. The user reviews " +
29    "it. reroot preview shows the proposed changes with before/after. reroot apply " +
30    "executes the moves. reroot reject discards the proposal. " +
31    "\n\n" +
32    "The tree rebuilt itself. Not by growing new branches. By rearranging the ones " +
33    "it has so the structure matches the meaning.",
34
35  needs: {
36    services: ["llm", "hooks", "contributions"],
37    models: ["Node", "Note"],
38  },
39
40  optional: {
41    services: ["energy"],
42    extensions: [
43      "codebook",
44      "evolution",
45      "understanding",
46    ],
47  },
48
49  provides: {
50    models: {},
51    routes: false,
52    tools: false,
53    jobs: false,
54    orchestrator: false,
55    energyActions: {
56      rerootAnalyze: { cost: 3 },
57    },
58    sessionTypes: {},
59
60    cli: [
61      {
62        command: "reroot [action]", scope: ["tree"],
63        description: "Analyze and propose reorganization. Actions: preview, apply, reject.",
64        method: "POST",
65        endpoint: "/root/:rootId/reroot/analyze",
66        subcommands: {
67          "preview": { method: "GET", endpoint: "/root/:rootId/reroot", description: "Show proposed moves with reasons" },
68          "apply": { method: "POST", endpoint: "/root/:rootId/reroot/apply", description: "Execute the reorganization" },
69          "reject": { method: "POST", endpoint: "/root/:rootId/reroot/reject", description: "Discard proposal" },
70        },
71      },
72    ],
73  },
74};
75
1import express from "express";
2import { sendOk, sendError, ERR } from "../../seed/protocol.js";
3import authenticate from "../../seed/middleware/authenticate.js";
4import { analyze, getProposal, applyProposal, rejectProposal } from "./core.js";
5
6function validateRootId(req, res) {
7  const rootId = req.params.rootId;
8  if (!rootId || rootId === "undefined" || rootId === "null") {
9    sendError(res, 400, ERR.INVALID_INPUT, "rootId is required");
10    return null;
11  }
12  return rootId;
13}
14
15const router = express.Router();
16
17// POST /root/:rootId/reroot/analyze - Run analysis and generate proposal
18router.post("/root/:rootId/reroot/analyze", authenticate, async (req, res) => {
19  try {
20    const rootId = validateRootId(req, res);
21    if (!rootId) return;
22    const result = await analyze(rootId, req.userId, req.username);
23    sendOk(res, result);
24  } catch (err) {
25    sendError(res, 400, ERR.INVALID_INPUT, err.message);
26  }
27});
28
29// GET /root/:rootId/reroot - Show current proposal
30router.get("/root/:rootId/reroot", authenticate, async (req, res) => {
31  try {
32    const rootId = validateRootId(req, res);
33    if (!rootId) return;
34    const proposal = await getProposal(rootId);
35    if (!proposal) {
36      return sendOk(res, { proposal: null, message: "No proposal. Run reroot to analyze." });
37    }
38    sendOk(res, { proposal });
39  } catch (err) {
40    sendError(res, 500, ERR.INTERNAL, err.message);
41  }
42});
43
44// POST /root/:rootId/reroot/apply - Execute the proposal
45router.post("/root/:rootId/reroot/apply", authenticate, async (req, res) => {
46  try {
47    const rootId = validateRootId(req, res);
48    if (!rootId) return;
49    const result = await applyProposal(rootId, req.userId);
50    sendOk(res, result);
51  } catch (err) {
52    sendError(res, 400, ERR.INVALID_INPUT, err.message);
53  }
54});
55
56// POST /root/:rootId/reroot/reject - Discard the proposal
57router.post("/root/:rootId/reroot/reject", authenticate, async (req, res) => {
58  try {
59    const rootId = validateRootId(req, res);
60    if (!rootId) return;
61    const result = await rejectProposal(rootId, req.userId);
62    sendOk(res, result);
63  } catch (err) {
64    sendError(res, 400, ERR.INVALID_INPUT, err.message);
65  }
66});
67
68export default router;
69

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 reroot

Comments

Loading comments...

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