EXTENSION for TreeOS
phase
The tree knows whether you are collecting or spending. Not from a toggle. Not from a setting. From what you actually do. Three phases. Awareness: open, exploratory, gathering. Browsing nodes, reading notes, asking questions, jumping between branches. Not building. Orienting. Attention: focused, creative, producing. Writing notes, creating nodes, changing structures, running tools. Spending what was gathered. Scattered: bouncing between many branches without depth. Four or more distinct nodes touched with low write activity. Movement without traction. Every afterNote, afterNodeCreate, afterNavigate, afterToolCall fires a typed signal into a rolling window of the last 20 interactions per user. The window size, awareness threshold, attention threshold, scattered branch threshold, and history depth are all land-configurable. Detection runs on every signal. Navigate, read, and query count as read types. Write, create, and tool count as write types. The ratio determines the phase. Scattered triggers when the distinct node count crosses the branch threshold and write ratio stays below 30%. enrichContext injects the detected phase into the AI prompt. During awareness the AI shifts toward showing, surfacing, connecting. During attention the AI shifts toward doing, building, executing. During scattered the AI gently reflects what it sees. You have touched four branches in the last ten minutes without writing anything. Are you looking for something specific? Not judgment. Observation. The tree reflects you back so you can see your own pattern. The most valuable moment is the transition. The user was in awareness for twenty minutes. Now they write their first note. The AI sees the transition signal and says: you have been exploring this branch for a while. Based on what you read, here is what I think you are about to work on. The gathered context crystallizes into the prompt for attention. The reverse: deep attention for two hours, then navigation. The AI says: good stopping point. Here is a summary of what you just built. Transition detection feeds inverse-tree when installed, sending phase-transition signals with from/to and confidence scores. Phase history tracks every transition with start time, end time, duration, and origin phase. Cycle stats compute the percentage split across awareness, attention, and scattered over all tracked time. The user sees their own patterns. 70% attention, 10% awareness, 20% scattered. Three API endpoints: current phase with recent interactions, full transition history, and the awareness vs attention ratio. The tree does not tell you how to work. It shows you how you already do.
v1.0.3 by TreeOS Site 0 downloads 4 files 502 lines 17.6 KB published 38d ago
treeos ext install phase
View changelog

Manifest

Provides

  • 1 CLI commands

Requires

  • services: hooks
  • models: User

Optional

  • extensions: inverse-tree, evolution
SHA256: 6514f712b92cf52cced03480acaa0d790564a52fca1ef43e3cf949cd64af4d20

Dependents

1 package depend on this

PackageTypeRelationship
treeos v1.0.1osstandalone

CLI Commands

CommandMethodDescription
phaseGETCurrent phase and session stats. Actions: history, cycle.
phase historyGETYour phase patterns over time
phase cycleGETAwareness vs attention ratio

Hooks

Listens To

  • afterNote
  • afterNodeCreate
  • afterNavigate
  • afterToolCall
  • enrichContext

Source Code

1// Phase Core
2//
3// Detects whether the user is in awareness (gathering), attention (producing),
4// or scattered (bouncing) from their behavior. No toggle. No setting.
5// The tree watches what you do and tells you what it sees.
6//
7// Rolling window of last N interactions per user. Each interaction is typed:
8//   navigate, read, write, create, tool, query
9//
10// The ratio of types determines the phase. The phase injects into enrichContext
11// so the AI adapts its behavior.
12
13import log from "../../seed/log.js";
14import { getUserMeta, setUserMeta } from "../../seed/tree/userMetadata.js";
15import { getExtension } from "../loader.js";
16
17let User = null;
18export function setModels(models) { User = models.User; }
19
20// ─────────────────────────────────────────────────────────────────────────
21// CONFIG (land-configurable)
22// ─────────────────────────────────────────────────────────────────────────
23
24let _getLandConfig = () => null;
25export function setLandConfig(fn) { _getLandConfig = fn; }
26
27function cfg(key, fallback) {
28  const v = _getLandConfig(key);
29  return v != null ? Number(v) : fallback;
30}
31
32function windowSize() { return cfg("phaseWindowSize", 20); }
33function awarenessThreshold() { return cfg("phaseAwarenessThreshold", 0.7); }
34function attentionThreshold() { return cfg("phaseAttentionThreshold", 0.7); }
35function scatteredBranchThreshold() { return cfg("phaseScatteredBranchThreshold", 4); }
36function historyMax() { return cfg("phaseHistoryMax", 200); }
37function transitionSummaryEnabled() {
38  const v = _getLandConfig("phaseTransitionSummary");
39  return v === true || v === "true" || v == null; // default on
40}
41
42// ─────────────────────────────────────────────────────────────────────────
43// SIGNAL RECORDING
44// ─────────────────────────────────────────────────────────────────────────
45
46/**
47 * Record an interaction signal for a user.
48 * Called from hook handlers (afterNote, afterNodeCreate, etc.)
49 *
50 * @param {string} userId
51 * @param {string} type - "navigate" | "write" | "create" | "tool" | "query" | "read"
52 * @param {string} [nodeId] - which node the interaction happened at
53 */
54export async function recordSignal(userId, type, nodeId) {
55  if (!userId || !User) return;
56
57  const user = await User.findById(userId);
58  if (!user) return;
59
60  const phaseMeta = getUserMeta(user, "phase");
61  if (!phaseMeta.window) phaseMeta.window = [];
62
63  const signal = {
64    type,
65    nodeId: nodeId || null,
66    at: Date.now(),
67  };
68
69  phaseMeta.window.push(signal);
70
71  // Trim to window size
72  const max = windowSize();
73  if (phaseMeta.window.length > max) {
74    phaseMeta.window = phaseMeta.window.slice(-max);
75  }
76
77  // Detect phase from window
78  const previousPhase = phaseMeta.currentPhase || null;
79  const detected = detectPhase(phaseMeta.window);
80  phaseMeta.currentPhase = detected.phase;
81  phaseMeta.phaseConfidence = detected.confidence;
82  phaseMeta.phaseDetectedAt = Date.now();
83
84  // Track transition
85  if (previousPhase && previousPhase !== detected.phase) {
86    recordTransition(phaseMeta, previousPhase, detected.phase);
87  }
88
89  // Atomic write to avoid clobbering other metadata namespaces
90  const { batchSetUserMeta } = await import("../../seed/tree/userMetadata.js");
91  await batchSetUserMeta(userId, "phase", phaseMeta);
92
93  // Feed inverse-tree if installed
94  if (previousPhase !== detected.phase) {
95    try {
96      const inverse = getExtension("inverse-tree");
97      if (inverse?.exports?.recordSignal) {
98        inverse.exports.recordSignal(userId, "phase-transition", {
99          from: previousPhase,
100          to: detected.phase,
101          confidence: detected.confidence,
102        });
103      }
104    } catch (err) {
105      log.debug("Phase", "inverse-tree signal failed:", err.message);
106    }
107  }
108}
109
110// ─────────────────────────────────────────────────────────────────────────
111// PHASE DETECTION
112// ─────────────────────────────────────────────────────────────────────────
113
114const READ_TYPES = new Set(["navigate", "read", "query"]);
115const WRITE_TYPES = new Set(["write", "create", "tool"]);
116
117function detectPhase(window) {
118  if (!window || window.length < 3) {
119    return { phase: "awareness", confidence: 0.5 };
120  }
121
122  let reads = 0;
123  let writes = 0;
124  const branches = new Set();
125
126  for (const signal of window) {
127    if (READ_TYPES.has(signal.type)) reads++;
128    if (WRITE_TYPES.has(signal.type)) writes++;
129    if (signal.nodeId) branches.add(signal.nodeId);
130  }
131
132  const total = reads + writes;
133  if (total === 0) return { phase: "awareness", confidence: 0.5 };
134
135  const readRatio = reads / total;
136  const writeRatio = writes / total;
137
138  // Check for scattered first: many branches, no depth
139  if (branches.size >= scatteredBranchThreshold() && writeRatio < 0.3) {
140    return { phase: "scattered", confidence: Math.min(0.9, branches.size / (scatteredBranchThreshold() * 2)) };
141  }
142
143  // Awareness: mostly reading/navigating
144  if (readRatio >= awarenessThreshold()) {
145    return { phase: "awareness", confidence: readRatio };
146  }
147
148  // Attention: mostly writing/creating/tooling
149  if (writeRatio >= attentionThreshold()) {
150    return { phase: "attention", confidence: writeRatio };
151  }
152
153  // Mixed but not scattered (some depth in a few branches)
154  if (writeRatio > readRatio) {
155    return { phase: "attention", confidence: writeRatio };
156  }
157
158  return { phase: "awareness", confidence: readRatio };
159}
160
161// ─────────────────────────────────────────────────────────────────────────
162// TRANSITIONS
163// ─────────────────────────────────────────────────────────────────────────
164
165function recordTransition(phaseMeta, from, to) {
166  if (!phaseMeta.history) phaseMeta.history = [];
167
168  // Close the previous phase entry
169  const last = phaseMeta.history[phaseMeta.history.length - 1];
170  if (last && !last.endAt) {
171    last.endAt = Date.now();
172    last.durationMs = last.endAt - last.startAt;
173  }
174
175  // Start new phase entry
176  phaseMeta.history.push({
177    phase: to,
178    startAt: Date.now(),
179    endAt: null,
180    durationMs: null,
181    transitionFrom: from,
182  });
183
184  // Trim history
185  const max = historyMax();
186  if (phaseMeta.history.length > max) {
187    phaseMeta.history = phaseMeta.history.slice(-max);
188  }
189
190  log.debug("Phase", `Phase transition: ${from} -> ${to}`);
191}
192
193// ─────────────────────────────────────────────────────────────────────────
194// CONTEXT INJECTION
195// ─────────────────────────────────────────────────────────────────────────
196
197/**
198 * Build the phase context string for enrichContext.
199 * Returns null if no phase data available.
200 */
201export function buildPhaseContext(phaseMeta) {
202  if (!phaseMeta?.currentPhase) return null;
203
204  const phase = phaseMeta.currentPhase;
205  const confidence = phaseMeta.phaseConfidence || 0;
206
207  let context = `User phase: ${phase}`;
208
209  if (phase === "awareness") {
210    context += ". The user is exploring and gathering context. Show, surface, connect. Don't push action.";
211  } else if (phase === "attention") {
212    context += ". The user is focused and producing. Do, build, execute. Don't pause to explain.";
213  } else if (phase === "scattered") {
214    context += ". The user is bouncing between branches without depth. Gently observe this pattern if appropriate.";
215  }
216
217  // Note transition if recent
218  const history = phaseMeta.history || [];
219  const last = history[history.length - 1];
220  if (last?.transitionFrom && last.startAt && (Date.now() - last.startAt) < 120000) {
221    context += ` Just transitioned from ${last.transitionFrom}.`;
222    if (last.transitionFrom === "awareness" && phase === "attention") {
223      context += " The user was gathering context and is now ready to work. Crystallize what they explored.";
224    } else if (last.transitionFrom === "attention" && phase === "awareness") {
225      context += " The user was focused and is now exploring. Summarize what they just built.";
226    }
227  }
228
229  return context;
230}
231
232// ─────────────────────────────────────────────────────────────────────────
233// READ (for routes)
234// ─────────────────────────────────────────────────────────────────────────
235
236export async function getPhaseState(userId) {
237  if (!User) return null;
238  const user = await User.findById(userId).lean();
239  if (!user) return null;
240  return getUserMeta(user, "phase");
241}
242
243export function computeCycleStats(history) {
244  if (!history || history.length === 0) return { awareness: 0, attention: 0, scattered: 0, total: 0 };
245
246  let awareness = 0;
247  let attention = 0;
248  let scattered = 0;
249
250  for (const entry of history) {
251    const dur = entry.durationMs || 0;
252    if (entry.phase === "awareness") awareness += dur;
253    else if (entry.phase === "attention") attention += dur;
254    else if (entry.phase === "scattered") scattered += dur;
255  }
256
257  const total = awareness + attention + scattered;
258  if (total === 0) return { awareness: 0, attention: 0, scattered: 0, total: 0 };
259
260  return {
261    awareness: Math.round(awareness / total * 100),
262    attention: Math.round(attention / total * 100),
263    scattered: Math.round(scattered / total * 100),
264    total,
265    awarenessMs: awareness,
266    attentionMs: attention,
267    scatteredMs: scattered,
268  };
269}
270
1import log from "../../seed/log.js";
2import { setModels, setLandConfig, recordSignal, buildPhaseContext } from "./core.js";
3import { getUserMeta } from "../../seed/tree/userMetadata.js";
4
5export async function init(core) {
6  setModels(core.models);
7
8  try {
9    const { getLandConfigValue } = await import("../../seed/landConfig.js");
10    setLandConfig(getLandConfigValue);
11  } catch (err) {
12    log.debug("Phase", "landConfig import failed:", err.message);
13  }
14
15  const { default: router } = await import("./routes.js");
16
17  // ── Hook: afterNote (write signal) ─────────────────────────────────
18  core.hooks.register("afterNote", async (data) => {
19    if (data.userId) {
20      await recordSignal(data.userId, "write", data.nodeId);
21    }
22  }, "phase");
23
24  // ── Hook: afterNodeCreate (create signal) ──────────────────────────
25  core.hooks.register("afterNodeCreate", async (data) => {
26    if (data.userId) {
27      await recordSignal(data.userId, "create", data.node?._id?.toString());
28    }
29  }, "phase");
30
31  // ── Hook: afterNavigate (navigate signal) ──────────────────────────
32  core.hooks.register("afterNavigate", async (data) => {
33    if (data.userId) {
34      await recordSignal(data.userId, "navigate", data.nodeId || data.rootId);
35    }
36  }, "phase");
37
38  // ── Hook: afterToolCall (tool signal) ──────────────────────────────
39  core.hooks.register("afterToolCall", async (data) => {
40    if (data.userId) {
41      await recordSignal(data.userId, "tool", data.rootId);
42    }
43  }, "phase");
44
45  // ── Hook: enrichContext (inject phase into AI prompt) ──────────────
46  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
47    // meta doesn't have user phase (it's node metadata). We need to read
48    // from user metadata via the userId in context.
49    const userId = context._userId;
50    if (!userId) return;
51
52    try {
53      const User = core.models.User;
54      const user = await User.findById(userId).select("metadata").lean();
55      if (!user) return;
56
57      const phaseMeta = getUserMeta(user, "phase");
58      const phaseContext = buildPhaseContext(phaseMeta);
59      if (phaseContext) {
60        context.userPhase = phaseContext;
61      }
62    } catch (err) {
63      log.debug("Phase", "enrichContext failed:", err.message);
64    }
65  }, "phase");
66
67  log.verbose("Phase", "Phase detection loaded (awareness / attention / scattered)");
68
69  return { router };
70}
71
1export default {
2  name: "phase",
3  version: "1.0.3",
4  builtFor: "TreeOS",
5  description:
6    "The tree knows whether you are collecting or spending. Not from a toggle. Not from " +
7    "a setting. From what you actually do. " +
8    "\n\n" +
9    "Three phases. Awareness: open, exploratory, gathering. Browsing nodes, reading notes, " +
10    "asking questions, jumping between branches. Not building. Orienting. Attention: focused, " +
11    "creative, producing. Writing notes, creating nodes, changing structures, running tools. " +
12    "Spending what was gathered. Scattered: bouncing between many branches without depth. " +
13    "Four or more distinct nodes touched with low write activity. Movement without traction. " +
14    "\n\n" +
15    "Every afterNote, afterNodeCreate, afterNavigate, afterToolCall fires a typed signal " +
16    "into a rolling window of the last 20 interactions per user. The window size, awareness " +
17    "threshold, attention threshold, scattered branch threshold, and history depth are all " +
18    "land-configurable. Detection runs on every signal. Navigate, read, and query count as " +
19    "read types. Write, create, and tool count as write types. The ratio determines the " +
20    "phase. Scattered triggers when the distinct node count crosses the branch threshold " +
21    "and write ratio stays below 30%. " +
22    "\n\n" +
23    "enrichContext injects the detected phase into the AI prompt. During awareness the AI " +
24    "shifts toward showing, surfacing, connecting. During attention the AI shifts toward " +
25    "doing, building, executing. During scattered the AI gently reflects what it sees. " +
26    "You have touched four branches in the last ten minutes without writing anything. " +
27    "Are you looking for something specific? Not judgment. Observation. The tree reflects " +
28    "you back so you can see your own pattern. " +
29    "\n\n" +
30    "The most valuable moment is the transition. The user was in awareness for twenty " +
31    "minutes. Now they write their first note. The AI sees the transition signal and says: " +
32    "you have been exploring this branch for a while. Based on what you read, here is what " +
33    "I think you are about to work on. The gathered context crystallizes into the prompt " +
34    "for attention. The reverse: deep attention for two hours, then navigation. The AI says: " +
35    "good stopping point. Here is a summary of what you just built. Transition detection " +
36    "feeds inverse-tree when installed, sending phase-transition signals with from/to and " +
37    "confidence scores. " +
38    "\n\n" +
39    "Phase history tracks every transition with start time, end time, duration, and origin " +
40    "phase. Cycle stats compute the percentage split across awareness, attention, and " +
41    "scattered over all tracked time. The user sees their own patterns. 70% attention, " +
42    "10% awareness, 20% scattered. Three API endpoints: current phase with recent " +
43    "interactions, full transition history, and the awareness vs attention ratio. The " +
44    "tree does not tell you how to work. It shows you how you already do.",
45
46  needs: {
47    services: ["hooks"],
48    models: ["User"],
49  },
50
51  optional: {
52    extensions: ["inverse-tree", "evolution"],
53  },
54
55  provides: {
56    models: {},
57    routes: false,
58    tools: false,
59    jobs: false,
60    orchestrator: false,
61    energyActions: {},
62    sessionTypes: {},
63
64    hooks: {
65      fires: [],
66      listens: ["afterNote", "afterNodeCreate", "afterNavigate", "afterToolCall", "enrichContext"],
67    },
68
69    cli: [
70      {
71        command: "phase [action]", scope: ["tree"],
72        description: "Current phase and session stats. Actions: history, cycle.",
73        method: "GET",
74        endpoint: "/user/:userId/phase",
75        subcommands: {
76          "history": { method: "GET", endpoint: "/user/:userId/phase/history", description: "Your phase patterns over time" },
77          "cycle": { method: "GET", endpoint: "/user/:userId/phase/cycle", description: "Awareness vs attention ratio" },
78        },
79      },
80    ],
81  },
82};
83
1import express from "express";
2import { sendOk, sendError, ERR } from "../../seed/protocol.js";
3import authenticate from "../../seed/middleware/authenticate.js";
4import { getPhaseState, computeCycleStats } from "./core.js";
5
6const router = express.Router();
7
8// GET /user/:userId/phase - Current phase and session stats
9router.get("/user/:userId/phase", authenticate, async (req, res) => {
10  try {
11    const phaseMeta = await getPhaseState(req.params.userId);
12    if (!phaseMeta) {
13      return sendOk(res, {
14        phase: null,
15        message: "No phase data yet. Interact with a tree to start tracking.",
16      });
17    }
18
19    sendOk(res, {
20      currentPhase: phaseMeta.currentPhase || null,
21      confidence: phaseMeta.phaseConfidence || 0,
22      windowSize: (phaseMeta.window || []).length,
23      recentInteractions: (phaseMeta.window || []).slice(-5).map(s => ({
24        type: s.type,
25        at: new Date(s.at).toISOString(),
26      })),
27    });
28  } catch (err) {
29    sendError(res, 500, ERR.INTERNAL, err.message);
30  }
31});
32
33// GET /user/:userId/phase/history - Phase patterns over time
34router.get("/user/:userId/phase/history", authenticate, async (req, res) => {
35  try {
36    const phaseMeta = await getPhaseState(req.params.userId);
37    if (!phaseMeta?.history?.length) {
38      return sendOk(res, { history: [], message: "No phase history yet." });
39    }
40
41    const history = phaseMeta.history.slice(-50).map(entry => ({
42      phase: entry.phase,
43      startAt: new Date(entry.startAt).toISOString(),
44      endAt: entry.endAt ? new Date(entry.endAt).toISOString() : null,
45      durationMinutes: entry.durationMs ? Math.round(entry.durationMs / 60000) : null,
46      transitionFrom: entry.transitionFrom || null,
47    }));
48
49    sendOk(res, { history });
50  } catch (err) {
51    sendError(res, 500, ERR.INTERNAL, err.message);
52  }
53});
54
55// GET /user/:userId/phase/cycle - Awareness vs attention ratio
56router.get("/user/:userId/phase/cycle", authenticate, async (req, res) => {
57  try {
58    const phaseMeta = await getPhaseState(req.params.userId);
59    if (!phaseMeta?.history?.length) {
60      return sendOk(res, { cycle: null, message: "No phase history yet." });
61    }
62
63    const stats = computeCycleStats(phaseMeta.history);
64    sendOk(res, {
65      cycle: {
66        awarenessPercent: stats.awareness,
67        attentionPercent: stats.attention,
68        scatteredPercent: stats.scattered,
69        totalTrackedMinutes: Math.round(stats.total / 60000),
70      },
71    });
72  } catch (err) {
73    sendError(res, 500, ERR.INTERNAL, err.message);
74  }
75});
76
77export default router;
78

Versions

Version Published Downloads
1.0.3 38d ago 0
1.0.2 47d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star phase

Comments

Loading comments...

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