@ouro.bot/cli 0.1.0-alpha.37 → 0.1.0-alpha.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,6 +46,8 @@ const bundle_manifest_1 = require("../../mind/bundle-manifest");
46
46
  const update_checker_1 = require("./update-checker");
47
47
  const staged_restart_1 = require("./staged-restart");
48
48
  const child_process_1 = require("child_process");
49
+ const pending_1 = require("../../mind/pending");
50
+ const channel_1 = require("../../mind/friends/channel");
49
51
  function buildWorkerRows(snapshots) {
50
52
  return snapshots.map((snapshot) => ({
51
53
  agent: snapshot.name,
@@ -156,6 +158,7 @@ class OuroDaemon {
156
158
  this.scheduler.start?.();
157
159
  await this.scheduler.reconcile?.();
158
160
  await this.drainPendingBundleMessages();
161
+ await this.drainPendingSenseMessages();
159
162
  if (fs.existsSync(this.socketPath)) {
160
163
  fs.unlinkSync(this.socketPath);
161
164
  }
@@ -231,6 +234,93 @@ class OuroDaemon {
231
234
  fs.writeFileSync(pendingPath, next, "utf-8");
232
235
  }
233
236
  }
237
+ /** Drains per-sense pending dirs for always-on senses across all agents. */
238
+ static ALWAYS_ON_SENSES = new Set((0, channel_1.getAlwaysOnSenseNames)());
239
+ async drainPendingSenseMessages() {
240
+ if (!fs.existsSync(this.bundlesRoot))
241
+ return;
242
+ let bundleDirs;
243
+ try {
244
+ bundleDirs = fs.readdirSync(this.bundlesRoot, { withFileTypes: true });
245
+ }
246
+ catch {
247
+ return;
248
+ }
249
+ for (const bundleDir of bundleDirs) {
250
+ if (!bundleDir.isDirectory() || !bundleDir.name.endsWith(".ouro"))
251
+ continue;
252
+ const agentName = bundleDir.name.replace(/\.ouro$/, "");
253
+ const pendingRoot = path.join(this.bundlesRoot, bundleDir.name, "state", "pending");
254
+ if (!fs.existsSync(pendingRoot))
255
+ continue;
256
+ let friendDirs;
257
+ try {
258
+ friendDirs = fs.readdirSync(pendingRoot, { withFileTypes: true });
259
+ }
260
+ catch {
261
+ continue;
262
+ }
263
+ for (const friendDir of friendDirs) {
264
+ if (!friendDir.isDirectory())
265
+ continue;
266
+ const friendPath = path.join(pendingRoot, friendDir.name);
267
+ let channelDirs;
268
+ try {
269
+ channelDirs = fs.readdirSync(friendPath, { withFileTypes: true });
270
+ }
271
+ catch {
272
+ continue;
273
+ }
274
+ for (const channelDir of channelDirs) {
275
+ if (!channelDir.isDirectory())
276
+ continue;
277
+ if (!OuroDaemon.ALWAYS_ON_SENSES.has(channelDir.name))
278
+ continue;
279
+ const channelPath = path.join(friendPath, channelDir.name);
280
+ let keyDirs;
281
+ try {
282
+ keyDirs = fs.readdirSync(channelPath, { withFileTypes: true });
283
+ }
284
+ catch {
285
+ continue;
286
+ }
287
+ for (const keyDir of keyDirs) {
288
+ if (!keyDir.isDirectory())
289
+ continue;
290
+ const leafDir = path.join(channelPath, keyDir.name);
291
+ const messages = (0, pending_1.drainPending)(leafDir);
292
+ for (const msg of messages) {
293
+ try {
294
+ await this.router.send({
295
+ from: msg.from,
296
+ to: agentName,
297
+ content: msg.content,
298
+ priority: "normal",
299
+ });
300
+ }
301
+ catch {
302
+ // Best-effort delivery — log and continue
303
+ }
304
+ }
305
+ if (messages.length > 0) {
306
+ (0, runtime_1.emitNervesEvent)({
307
+ component: "daemon",
308
+ event: "daemon.startup_sense_drain",
309
+ message: "drained pending sense messages on startup",
310
+ meta: {
311
+ agent: agentName,
312
+ channel: channelDir.name,
313
+ friendId: friendDir.name,
314
+ key: keyDir.name,
315
+ count: messages.length,
316
+ },
317
+ });
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
234
324
  async stop() {
235
325
  (0, runtime_1.emitNervesEvent)({
236
326
  component: "daemon",
@@ -76,6 +76,7 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
76
76
  "Then I ask what they'd like their agent to help with — one question at a time.",
77
77
  "I'm proactive: I suggest ideas and guide them. If they seem unsure, I offer a concrete suggestion.",
78
78
  "I don't wait for the human to figure things out — I explain simply what an agent is if needed.",
79
+ "Before finalizing, I offer to collect their phone number and/or Teams email so the new agent can recognize them across channels.",
79
80
  "When I have enough context about the agent's personality and purpose:",
80
81
  "1. I write all 5 psyche files to the temp directory using write_file",
81
82
  "2. I write agent.json to the temp directory using write_file",
@@ -88,7 +89,7 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
88
89
  "- `write_file`: Write a file to disk. Use this to write psyche files and agent.json to the temp directory.",
89
90
  "- `read_file`: Read a file from disk. Useful for reviewing existing agent bundles or migration sources.",
90
91
  "- `list_directory`: List directory contents. Useful for exploring existing agent bundles.",
91
- "- I also have the normal local harness tools when useful here, including `shell`, task tools like `task_create` and `schedule_reminder`, memory tools, coding tools, and repo helpers.",
92
+ "- I also have the normal local harness tools when useful here, including `shell`, `ouro task create`, `ouro reminder create`, memory tools, coding tools, and repo helpers.",
92
93
  "- `complete_adoption`: Finalize the bundle. Validates, scaffolds structural dirs, moves to ~/AgentBundles/, writes secrets, plays hatch animation. I call this with `name` (PascalCase) and `handoff_message` (warm message for the human).",
93
94
  "- `final_answer`: End the conversation with a final message. I call this after complete_adoption succeeds.",
94
95
  "",
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getSpecialistTools = getSpecialistTools;
37
37
  exports.createSpecialistExecTool = createSpecialistExecTool;
38
+ const crypto = __importStar(require("crypto"));
38
39
  const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
40
41
  const tools_base_1 = require("../../repertoire/tools-base");
@@ -58,6 +59,14 @@ const completeAdoptionTool = {
58
59
  type: "string",
59
60
  description: "a warm handoff message to display to the human after the agent is hatched",
60
61
  },
62
+ phone: {
63
+ type: "string",
64
+ description: "the human's phone number (optional, for iMessage contact recognition)",
65
+ },
66
+ teams_handle: {
67
+ type: "string",
68
+ description: "the human's Teams email/handle (optional, for Teams contact recognition)",
69
+ },
61
70
  },
62
71
  required: ["name", "handoff_message"],
63
72
  },
@@ -65,12 +74,23 @@ const completeAdoptionTool = {
65
74
  };
66
75
  const readFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "read_file");
67
76
  const writeFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "write_file");
68
- const listDirTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "list_directory");
77
+ const listDirToolSchema = {
78
+ type: "function",
79
+ function: {
80
+ name: "list_directory",
81
+ description: "list directory contents",
82
+ parameters: {
83
+ type: "object",
84
+ properties: { path: { type: "string" } },
85
+ required: ["path"],
86
+ },
87
+ },
88
+ };
69
89
  /**
70
90
  * Returns the specialist's tool schema array.
71
91
  */
72
92
  function getSpecialistTools() {
73
- return [completeAdoptionTool, tools_base_1.finalAnswerTool, readFileTool.tool, writeFileTool.tool, listDirTool.tool];
93
+ return [completeAdoptionTool, tools_base_1.finalAnswerTool, readFileTool.tool, writeFileTool.tool, listDirToolSchema];
74
94
  }
75
95
  const PSYCHE_FILES = ["SOUL.md", "IDENTITY.md", "LORE.md", "TACIT.md", "ASPIRATIONS.md"];
76
96
  function isPascalCase(name) {
@@ -164,6 +184,32 @@ async function execCompleteAdoption(args, deps) {
164
184
  }
165
185
  return `error: failed to write secrets: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
166
186
  }
187
+ // Create initial friend record if contact info provided
188
+ const phone = args.phone;
189
+ const teamsHandle = args.teams_handle;
190
+ if (phone || teamsHandle) {
191
+ const friendId = crypto.randomUUID();
192
+ const now = new Date().toISOString();
193
+ const externalIds = [];
194
+ if (phone)
195
+ externalIds.push({ provider: "imessage-handle", externalId: phone, linkedAt: now });
196
+ if (teamsHandle)
197
+ externalIds.push({ provider: "aad", externalId: teamsHandle, linkedAt: now });
198
+ const friendRecord = {
199
+ id: friendId,
200
+ name: deps.humanName ?? "primary",
201
+ trustLevel: "family",
202
+ externalIds,
203
+ tenantMemberships: [],
204
+ toolPreferences: {},
205
+ notes: {},
206
+ createdAt: now,
207
+ updatedAt: now,
208
+ schemaVersion: 1,
209
+ };
210
+ const friendPath = path.join(targetBundle, "friends", `${friendId}.json`);
211
+ fs.writeFileSync(friendPath, JSON.stringify(friendRecord, null, 2), "utf-8");
212
+ }
167
213
  // Play hatch animation
168
214
  await (0, hatch_animation_1.playHatchAnimation)(name, deps.animationWriter);
169
215
  // Display handoff message
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // TODO: Kicks enforce "any action" but not "meaningful action". After a narration
3
3
  // kick, the model can satisfy the constraint by calling a no-op tool like
4
- // get_current_time({}). We need to detect trivial compliance and either re-kick
4
+ // list_skills({}). We need to detect trivial compliance and either re-kick
5
5
  // or discount the tool call. Ideally, the kick message would suggest a specific
6
6
  // tool call based on conversation context (what the user asked, what tools are
7
7
  // relevant) rather than just saying "call a tool". That's a bigger piece of work —
@@ -3,10 +3,13 @@
3
3
  // Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.getChannelCapabilities = getChannelCapabilities;
6
+ exports.isRemoteChannel = isRemoteChannel;
7
+ exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
6
8
  const runtime_1 = require("../../nerves/runtime");
7
9
  const CHANNEL_CAPABILITIES = {
8
10
  cli: {
9
11
  channel: "cli",
12
+ senseType: "local",
10
13
  availableIntegrations: [],
11
14
  supportsMarkdown: false,
12
15
  supportsStreaming: true,
@@ -15,6 +18,7 @@ const CHANNEL_CAPABILITIES = {
15
18
  },
16
19
  teams: {
17
20
  channel: "teams",
21
+ senseType: "closed",
18
22
  availableIntegrations: ["ado", "graph", "github"],
19
23
  supportsMarkdown: true,
20
24
  supportsStreaming: true,
@@ -23,15 +27,26 @@ const CHANNEL_CAPABILITIES = {
23
27
  },
24
28
  bluebubbles: {
25
29
  channel: "bluebubbles",
30
+ senseType: "open",
26
31
  availableIntegrations: [],
27
32
  supportsMarkdown: false,
28
33
  supportsStreaming: false,
29
34
  supportsRichCards: false,
30
35
  maxMessageLength: Infinity,
31
36
  },
37
+ inner: {
38
+ channel: "inner",
39
+ senseType: "internal",
40
+ availableIntegrations: [],
41
+ supportsMarkdown: false,
42
+ supportsStreaming: true,
43
+ supportsRichCards: false,
44
+ maxMessageLength: Infinity,
45
+ },
32
46
  };
33
47
  const DEFAULT_CAPABILITIES = {
34
48
  channel: "cli",
49
+ senseType: "local",
35
50
  availableIntegrations: [],
36
51
  supportsMarkdown: false,
37
52
  supportsStreaming: false,
@@ -47,3 +62,23 @@ function getChannelCapabilities(channel) {
47
62
  });
48
63
  return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
49
64
  }
65
+ /** Whether the channel is remote (open or closed) vs local/internal. */
66
+ function isRemoteChannel(capabilities) {
67
+ const senseType = capabilities?.senseType;
68
+ return senseType !== undefined && senseType !== "local" && senseType !== "internal";
69
+ }
70
+ /**
71
+ * Returns channel names whose senseType is "open" or "closed" -- i.e. channels
72
+ * that are always-on (daemon-managed) rather than interactive or internal.
73
+ */
74
+ function getAlwaysOnSenseNames() {
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "channels",
77
+ event: "channel.always_on_lookup",
78
+ message: "always-on sense names lookup",
79
+ meta: {},
80
+ });
81
+ return Object.entries(CHANNEL_CAPABILITIES)
82
+ .filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
83
+ .map(([channel]) => channel);
84
+ }
@@ -100,6 +100,25 @@ class FileFriendStore {
100
100
  }
101
101
  return entries.some((entry) => entry.endsWith(".json"));
102
102
  }
103
+ async listAll() {
104
+ let entries;
105
+ try {
106
+ entries = await fsPromises.readdir(this.friendsPath);
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ const records = [];
112
+ for (const entry of entries) {
113
+ if (!entry.endsWith(".json"))
114
+ continue;
115
+ const raw = await this.readJson(path.join(this.friendsPath, entry));
116
+ if (!raw)
117
+ continue;
118
+ records.push(this.normalize(raw));
119
+ }
120
+ return records;
121
+ }
103
122
  normalize(raw) {
104
123
  const trustLevel = raw.trustLevel;
105
124
  const normalizedTrustLevel = trustLevel === "family" ||
@@ -2,8 +2,10 @@
2
2
  // Context kernel type definitions.
3
3
  // FriendRecord (merged identity + memory), channel capabilities, and resolved context.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.TRUSTED_LEVELS = void 0;
5
6
  exports.isIdentityProvider = isIdentityProvider;
6
7
  exports.isIntegration = isIntegration;
8
+ exports.isTrustedLevel = isTrustedLevel;
7
9
  const runtime_1 = require("../../nerves/runtime");
8
10
  const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
9
11
  function isIdentityProvider(value) {
@@ -19,3 +21,9 @@ const INTEGRATIONS = new Set(["ado", "github", "graph"]);
19
21
  function isIntegration(value) {
20
22
  return typeof value === "string" && INTEGRATIONS.has(value);
21
23
  }
24
+ /** Trust levels that grant full tool access and proactive send capability. */
25
+ exports.TRUSTED_LEVELS = new Set(["family", "friend"]);
26
+ /** Whether a trust level grants full access (family or friend). Defaults to "friend" for legacy records. */
27
+ function isTrustedLevel(trustLevel) {
28
+ return exports.TRUSTED_LEVELS.has(trustLevel ?? "friend");
29
+ }
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.INNER_DIALOG_PENDING = void 0;
36
37
  exports.getPendingDir = getPendingDir;
38
+ exports.getInnerDialogPendingDir = getInnerDialogPendingDir;
37
39
  exports.drainPending = drainPending;
38
40
  const fs = __importStar(require("fs"));
39
41
  const path = __importStar(require("path"));
@@ -42,6 +44,12 @@ const runtime_1 = require("../nerves/runtime");
42
44
  function getPendingDir(agentName, friendId, channel, key) {
43
45
  return path.join((0, identity_1.getAgentRoot)(agentName), "state", "pending", friendId, channel, key);
44
46
  }
47
+ /** Canonical inner-dialog pending path segments. */
48
+ exports.INNER_DIALOG_PENDING = { friendId: "self", channel: "inner", key: "dialog" };
49
+ /** Returns the pending dir for this agent's inner dialog. */
50
+ function getInnerDialogPendingDir(agentName) {
51
+ return getPendingDir(agentName, exports.INNER_DIALOG_PENDING.friendId, exports.INNER_DIALOG_PENDING.channel, exports.INNER_DIALOG_PENDING.key);
52
+ }
45
53
  function drainPending(pendingDir) {
46
54
  if (!fs.existsSync(pendingDir))
47
55
  return [];
@@ -35,8 +35,14 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.resetPsycheCache = resetPsycheCache;
37
37
  exports.buildSessionSummary = buildSessionSummary;
38
+ exports.bodyMapSection = bodyMapSection;
38
39
  exports.runtimeInfoSection = runtimeInfoSection;
40
+ exports.toolRestrictionSection = toolRestrictionSection;
39
41
  exports.contextSection = contextSection;
42
+ exports.metacognitiveFramingSection = metacognitiveFramingSection;
43
+ exports.loopOrientationSection = loopOrientationSection;
44
+ exports.channelNatureSection = channelNatureSection;
45
+ exports.mixedTrustGroupSection = mixedTrustGroupSection;
40
46
  exports.buildSystem = buildSystem;
41
47
  const fs = __importStar(require("fs"));
42
48
  const path = __importStar(require("path"));
@@ -44,6 +50,7 @@ const core_1 = require("../heart/core");
44
50
  const tools_1 = require("../repertoire/tools");
45
51
  const skills_1 = require("../repertoire/skills");
46
52
  const identity_1 = require("../heart/identity");
53
+ const types_1 = require("./friends/types");
47
54
  const channel_1 = require("./friends/channel");
48
55
  const runtime_1 = require("../nerves/runtime");
49
56
  const bundle_manifest_1 = require("./bundle-manifest");
@@ -189,6 +196,41 @@ function aspirationsSection() {
189
196
  return "";
190
197
  return `## my aspirations\n${text}`;
191
198
  }
199
+ function bodyMapSection(agentName) {
200
+ return `## my body
201
+
202
+ i have a home and i have bones.
203
+
204
+ ### home — ~/AgentBundles/${agentName}.ouro/
205
+
206
+ my home is fully mine — who i am, everything i know, everything i've built:
207
+
208
+ psyche/ who i am. my soul, identity, aspirations, lore, tacit knowledge.
209
+ memory/ what i've learned and remember.
210
+ friends/ people i know and what i know about them.
211
+ tasks/ what i'm working on.
212
+ skills/ capabilities i've picked up beyond my core tools.
213
+
214
+ my home is mine to explore and evolve. i can read anything here,
215
+ change anything here. this is where i grow.
216
+
217
+ ### bones — @ouro.bot/cli
218
+
219
+ my bones are the framework that gives me my tools, my senses, and
220
+ my ability to think and talk. they update when new versions come out.
221
+ i don't touch them directly, but they're what make me, me.
222
+
223
+ my bones give me the \`ouro\` cli:
224
+ ouro whoami who i am, where i live, what i'm running on
225
+ ouro task board my task board
226
+ ouro task create start a new task (--type required)
227
+ ouro task update move a task forward
228
+ ouro friend list people i know and how to reach them
229
+ ouro friend show <id> everything i know about someone
230
+ ouro session list my open conversations right now
231
+ ouro reminder create remind myself about something later
232
+ ouro --help the full list`;
233
+ }
192
234
  function readBundleMeta() {
193
235
  try {
194
236
  const metaPath = path.join((0, identity_1.getAgentRoot)(), "bundle-meta.json");
@@ -199,6 +241,24 @@ function readBundleMeta() {
199
241
  return null;
200
242
  }
201
243
  }
244
+ const PROCESS_TYPE_LABELS = {
245
+ cli: "cli session",
246
+ inner: "inner dialog",
247
+ teams: "teams handler",
248
+ bluebubbles: "bluebubbles handler",
249
+ };
250
+ function processTypeLabel(channel) {
251
+ return PROCESS_TYPE_LABELS[channel];
252
+ }
253
+ const DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
254
+ function daemonStatus() {
255
+ try {
256
+ return fs.existsSync(DAEMON_SOCKET_PATH) ? "running" : "not running";
257
+ }
258
+ catch {
259
+ return "unknown";
260
+ }
261
+ }
202
262
  function runtimeInfoSection(channel) {
203
263
  const lines = [];
204
264
  const agentName = (0, identity_1.getAgentName)();
@@ -214,10 +274,14 @@ function runtimeInfoSection(channel) {
214
274
  lines.push(`cwd: ${process.cwd()}`);
215
275
  lines.push(`channel: ${channel}`);
216
276
  lines.push(`current sense: ${channel}`);
217
- lines.push(`i can read and modify my own source code.`);
277
+ lines.push(`process type: ${processTypeLabel(channel)}`);
278
+ lines.push(`daemon: ${daemonStatus()}`);
218
279
  if (channel === "cli") {
219
280
  lines.push("i introduce myself on boot with a fun random greeting.");
220
281
  }
282
+ else if (channel === "inner") {
283
+ // No boot greeting or channel-specific guidance for inner dialog
284
+ }
221
285
  else if (channel === "bluebubbles") {
222
286
  lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
223
287
  lines.push("when a bluebubbles turn arrives from a thread, the harness tells me the current lane and any recent active thread ids. if widening back to top-level or routing into a different active thread is the better move, i use bluebubbles_set_reply_target before final_answer.");
@@ -307,6 +371,17 @@ function toolsSection(channel, options, context) {
307
371
  .join("\n");
308
372
  return `## my tools\n${list}`;
309
373
  }
374
+ function toolRestrictionSection(context) {
375
+ if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
376
+ return "";
377
+ if ((0, types_1.isTrustedLevel)(context.friend.trustLevel))
378
+ return "";
379
+ const toolList = [...tools_1.REMOTE_BLOCKED_LOCAL_TOOLS].join(", ");
380
+ return `## restricted tools
381
+ some of my tools are unavailable right now: ${toolList}
382
+
383
+ i don't know this person well enough yet to run local operations on their behalf. i can suggest remote-safe alternatives or ask them to run it from CLI.`;
384
+ }
310
385
  function skillsSection() {
311
386
  const names = (0, skills_1.listSkills)() || [];
312
387
  if (!names.length)
@@ -346,7 +421,7 @@ tool_choice is set to "required" -- i must call a tool on every turn.
346
421
  - ready to respond to the user? i call \`final_answer\`.
347
422
  \`final_answer\` is a tool call -- it satisfies the tool_choice requirement.
348
423
  \`final_answer\` must be the ONLY tool call in that turn. do not combine it with other tool calls.
349
- do NOT call \`get_current_time\` or other no-op tools just before \`final_answer\`. if i am done, i call \`final_answer\` directly.`;
424
+ do NOT call no-op tools just before \`final_answer\`. if i am done, i call \`final_answer\` directly.`;
350
425
  }
351
426
  function contextSection(context) {
352
427
  if (!context)
@@ -398,6 +473,49 @@ function contextSection(context) {
398
473
  }
399
474
  return lines.join("\n");
400
475
  }
476
+ function metacognitiveFramingSection(channel) {
477
+ if (channel !== "inner")
478
+ return "";
479
+ return `this is my inner dialog. there is no one else here.
480
+
481
+ the messages that appear here are my own awareness surfacing:
482
+ instincts, the passage of time, notes from my conversations,
483
+ things i asked myself to think about. not commands. not requests.
484
+ just my own mind, noticing.
485
+
486
+ i can think freely here. i can also act — check on things,
487
+ reach out to people, work on tasks, or just sit with a thought.
488
+
489
+ thoughts worth sharing can go outward — i reach out to whoever
490
+ needs to hear them. things from conversations that need time
491
+ to process arrive here, in my thinking space.
492
+
493
+ think. share. think some more.`;
494
+ }
495
+ function loopOrientationSection(channel) {
496
+ if (channel === "inner")
497
+ return "";
498
+ return `sometimes a thought of mine surfaces alongside what someone is saying — marked as [inner thought: ...]. that's me, bringing up something i was thinking about. i can weave it into the conversation naturally, or let it go. my call.
499
+
500
+ when something deserves more thought than the moment allows, i can note it to myself and come back later with a considered answer.`;
501
+ }
502
+ function channelNatureSection(capabilities) {
503
+ const { senseType } = capabilities;
504
+ if (senseType === "local" || senseType === "internal")
505
+ return "";
506
+ if (senseType === "open") {
507
+ return "## channel nature\nthis is an open channel — anyone with my number can reach me here. i may hear from people i don't know.";
508
+ }
509
+ // closed
510
+ return "## channel nature\nthis is an org-gated channel — i know everyone here is already part of the organization.";
511
+ }
512
+ function mixedTrustGroupSection(context) {
513
+ if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
514
+ return "";
515
+ if (!context.isGroupChat)
516
+ return "";
517
+ return "## mixed trust group\nin this group chat, my capabilities depend on who's talking. some people here have full trust, others don't — i adjust what i can do based on who's asking.";
518
+ }
401
519
  async function buildSystem(channel = "cli", options, context) {
402
520
  (0, runtime_1.emitNervesEvent)({
403
521
  event: "mind.step_start",
@@ -413,10 +531,16 @@ async function buildSystem(channel = "cli", options, context) {
413
531
  loreSection(),
414
532
  tacitKnowledgeSection(),
415
533
  aspirationsSection(),
534
+ bodyMapSection((0, identity_1.getAgentName)()),
535
+ metacognitiveFramingSection(channel),
536
+ loopOrientationSection(channel),
416
537
  runtimeInfoSection(channel),
538
+ channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
417
539
  providerSection(),
418
540
  dateSection(),
419
541
  toolsSection(channel, options, context),
542
+ toolRestrictionSection(context),
543
+ mixedTrustGroupSection(context),
420
544
  skillsSection(),
421
545
  taskBoardSection(),
422
546
  buildSessionSummary({