@ouro.bot/cli 0.1.0-alpha.16 → 0.1.0-alpha.161

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.
Files changed (168) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +3 -2
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +3 -3
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +187 -191
  5. package/changelog.json +1088 -0
  6. package/dist/heart/active-work.js +633 -0
  7. package/dist/heart/bridges/manager.js +358 -0
  8. package/dist/heart/bridges/state-machine.js +135 -0
  9. package/dist/heart/bridges/store.js +123 -0
  10. package/dist/heart/commitments.js +105 -0
  11. package/dist/heart/config.js +40 -30
  12. package/dist/heart/core.js +608 -123
  13. package/dist/heart/cross-chat-delivery.js +146 -0
  14. package/dist/heart/daemon/agent-discovery.js +81 -0
  15. package/dist/heart/daemon/agent-service.js +360 -0
  16. package/dist/heart/daemon/auth-flow.js +477 -0
  17. package/dist/heart/daemon/cadence.js +70 -0
  18. package/dist/heart/daemon/daemon-cli.js +2467 -205
  19. package/dist/heart/daemon/daemon-entry.js +131 -4
  20. package/dist/heart/daemon/daemon-health.js +136 -0
  21. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  22. package/dist/heart/daemon/daemon-tombstone.js +123 -0
  23. package/dist/heart/daemon/daemon.js +445 -17
  24. package/dist/heart/daemon/habit-migration.js +181 -0
  25. package/dist/heart/daemon/habit-parser.js +119 -0
  26. package/dist/heart/daemon/habit-scheduler.js +369 -0
  27. package/dist/heart/daemon/hatch-animation.js +10 -3
  28. package/dist/heart/daemon/hatch-flow.js +30 -111
  29. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  30. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  31. package/dist/heart/daemon/inner-status.js +89 -0
  32. package/dist/heart/daemon/launchd.js +159 -0
  33. package/dist/heart/daemon/log-tailer.js +4 -3
  34. package/dist/heart/daemon/mcp-server.js +591 -0
  35. package/dist/heart/daemon/message-router.js +17 -8
  36. package/dist/heart/daemon/os-cron-deps.js +134 -0
  37. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  38. package/dist/heart/daemon/ouro-path-installer.js +78 -34
  39. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  40. package/dist/heart/daemon/outlook-http.js +369 -0
  41. package/dist/heart/daemon/outlook-read.js +1281 -0
  42. package/dist/heart/daemon/outlook-render.js +1032 -0
  43. package/dist/heart/daemon/outlook-types.js +7 -0
  44. package/dist/heart/daemon/outlook-view.js +194 -0
  45. package/dist/heart/daemon/process-manager.js +75 -0
  46. package/dist/heart/daemon/run-hooks.js +39 -0
  47. package/dist/heart/daemon/runtime-logging.js +58 -15
  48. package/dist/heart/daemon/runtime-metadata.js +219 -0
  49. package/dist/heart/daemon/runtime-mode.js +67 -0
  50. package/dist/heart/daemon/safe-mode.js +161 -0
  51. package/dist/heart/daemon/sense-manager.js +50 -2
  52. package/dist/heart/daemon/session-id-resolver.js +131 -0
  53. package/dist/heart/daemon/skill-management-installer.js +94 -0
  54. package/dist/heart/daemon/socket-client.js +202 -0
  55. package/dist/heart/daemon/specialist-orchestrator.js +2 -2
  56. package/dist/heart/daemon/specialist-prompt.js +13 -8
  57. package/dist/heart/daemon/specialist-tools.js +53 -4
  58. package/dist/heart/daemon/staged-restart.js +114 -0
  59. package/dist/heart/daemon/task-scheduler.js +3 -25
  60. package/dist/heart/daemon/thoughts.js +507 -0
  61. package/dist/heart/daemon/update-checker.js +111 -0
  62. package/dist/heart/daemon/update-hooks.js +138 -0
  63. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  64. package/dist/heart/delegation.js +62 -0
  65. package/dist/heart/identity.js +136 -55
  66. package/dist/heart/kicks.js +2 -20
  67. package/dist/heart/migrate-config.js +127 -0
  68. package/dist/heart/model-capabilities.js +48 -0
  69. package/dist/heart/obligations.js +197 -0
  70. package/dist/heart/progress-story.js +42 -0
  71. package/dist/heart/provider-failover.js +88 -0
  72. package/dist/heart/provider-ping.js +172 -0
  73. package/dist/heart/providers/anthropic-token.js +163 -0
  74. package/dist/heart/providers/anthropic.js +196 -36
  75. package/dist/heart/providers/azure.js +116 -10
  76. package/dist/heart/providers/github-copilot.js +157 -0
  77. package/dist/heart/providers/minimax.js +33 -3
  78. package/dist/heart/providers/openai-codex.js +51 -16
  79. package/dist/heart/session-activity.js +172 -0
  80. package/dist/heart/session-recall.js +216 -0
  81. package/dist/heart/streaming.js +117 -33
  82. package/dist/heart/target-resolution.js +127 -0
  83. package/dist/heart/tool-activity-callbacks.js +36 -0
  84. package/dist/heart/tool-description.js +135 -0
  85. package/dist/heart/tool-loop.js +200 -0
  86. package/dist/heart/turn-coordinator.js +28 -0
  87. package/dist/mind/associative-recall.js +122 -32
  88. package/dist/mind/bundle-manifest.js +14 -0
  89. package/dist/mind/context.js +125 -14
  90. package/dist/mind/{memory.js → diary.js} +70 -56
  91. package/dist/mind/first-impressions.js +16 -2
  92. package/dist/mind/friends/channel.js +56 -0
  93. package/dist/mind/friends/group-context.js +144 -0
  94. package/dist/mind/friends/store-file.js +19 -0
  95. package/dist/mind/friends/trust-explanation.js +74 -0
  96. package/dist/mind/friends/types.js +8 -0
  97. package/dist/mind/journal-index.js +161 -0
  98. package/dist/mind/obligation-steering.js +221 -0
  99. package/dist/mind/obligations.js +134 -0
  100. package/dist/mind/pending.js +76 -9
  101. package/dist/mind/phrases.js +1 -0
  102. package/dist/mind/prompt.js +651 -91
  103. package/dist/mind/token-estimate.js +8 -12
  104. package/dist/nerves/cli-logging.js +15 -2
  105. package/dist/nerves/coverage/run-artifacts.js +1 -1
  106. package/dist/nerves/event-buffer.js +111 -0
  107. package/dist/nerves/index.js +63 -3
  108. package/dist/nerves/observation.js +20 -0
  109. package/dist/nerves/runtime.js +5 -1
  110. package/dist/outlook-ui/assets/index-CXQG0mVx.js +61 -0
  111. package/dist/outlook-ui/assets/index-Gq7EdfkO.css +1 -0
  112. package/dist/outlook-ui/index.html +15 -0
  113. package/dist/repertoire/ado-semantic.js +6 -0
  114. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  115. package/dist/repertoire/coding/context-pack.js +254 -0
  116. package/dist/repertoire/coding/feedback.js +301 -0
  117. package/dist/repertoire/coding/index.js +4 -1
  118. package/dist/repertoire/coding/manager.js +210 -4
  119. package/dist/repertoire/coding/spawner.js +58 -12
  120. package/dist/repertoire/coding/tools.js +176 -4
  121. package/dist/repertoire/guardrails.js +291 -0
  122. package/dist/repertoire/mcp-client.js +254 -0
  123. package/dist/repertoire/mcp-manager.js +198 -0
  124. package/dist/repertoire/skills.js +14 -23
  125. package/dist/repertoire/tasks/board.js +43 -5
  126. package/dist/repertoire/tasks/fix.js +182 -0
  127. package/dist/repertoire/tasks/index.js +28 -10
  128. package/dist/repertoire/tasks/lifecycle.js +2 -2
  129. package/dist/repertoire/tasks/parser.js +3 -2
  130. package/dist/repertoire/tasks/scanner.js +194 -37
  131. package/dist/repertoire/tasks/transitions.js +11 -28
  132. package/dist/repertoire/tools-base.js +992 -273
  133. package/dist/repertoire/tools-bluebubbles.js +94 -0
  134. package/dist/repertoire/tools-github.js +1 -6
  135. package/dist/repertoire/tools-teams.js +12 -59
  136. package/dist/repertoire/tools.js +281 -101
  137. package/dist/scripts/claude-code-hook.js +41 -0
  138. package/dist/scripts/claude-code-stop-hook.js +47 -0
  139. package/dist/senses/attention-queue.js +97 -0
  140. package/dist/senses/bluebubbles-client.js +258 -5
  141. package/dist/senses/bluebubbles-entry.js +2 -0
  142. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  143. package/dist/senses/bluebubbles-media.js +339 -0
  144. package/dist/senses/bluebubbles-model.js +12 -4
  145. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  146. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  147. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  148. package/dist/senses/bluebubbles.js +1040 -45
  149. package/dist/senses/cli-entry.js +1 -1
  150. package/dist/senses/cli-layout.js +187 -0
  151. package/dist/senses/cli.js +419 -152
  152. package/dist/senses/commands.js +38 -1
  153. package/dist/senses/continuity.js +94 -0
  154. package/dist/senses/habit-turn-message.js +108 -0
  155. package/dist/senses/inner-dialog-worker.js +112 -19
  156. package/dist/senses/inner-dialog.js +592 -84
  157. package/dist/senses/pipeline.js +502 -0
  158. package/dist/senses/shared-turn.js +196 -0
  159. package/dist/senses/surface-tool.js +95 -0
  160. package/dist/senses/teams.js +667 -120
  161. package/dist/senses/trust-gate.js +112 -2
  162. package/package.json +13 -5
  163. package/skills/configure-dev-tools.md +81 -0
  164. package/subagents/README.md +4 -70
  165. package/dist/heart/daemon/subagent-installer.js +0 -134
  166. package/subagents/work-doer.md +0 -233
  167. package/subagents/work-merger.md +0 -624
  168. package/subagents/work-planner.md +0 -373
@@ -33,19 +33,304 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.finalAnswerTool = exports.baseToolHandlers = exports.tools = exports.baseToolDefinitions = void 0;
36
+ exports.restTool = exports.settleTool = exports.observeTool = exports.ponderTool = exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = void 0;
37
+ exports.renderInnerProgressStatus = renderInnerProgressStatus;
37
38
  const fs = __importStar(require("fs"));
39
+ const fg = __importStar(require("fast-glob"));
38
40
  const child_process_1 = require("child_process");
39
41
  const path = __importStar(require("path"));
40
42
  const skills_1 = require("./skills");
41
43
  const config_1 = require("../heart/config");
42
44
  const runtime_1 = require("../nerves/runtime");
43
45
  const identity_1 = require("../heart/identity");
44
- const os = __importStar(require("os"));
45
- const tasks_1 = require("./tasks");
46
+ const identity_2 = require("../heart/identity");
47
+ const socket_client_1 = require("../heart/daemon/socket-client");
48
+ const thoughts_1 = require("../heart/daemon/thoughts");
49
+ const manager_1 = require("../heart/bridges/manager");
50
+ const session_recall_1 = require("../heart/session-recall");
51
+ const session_activity_1 = require("../heart/session-activity");
52
+ const active_work_1 = require("../heart/active-work");
46
53
  const tools_1 = require("./coding/tools");
47
- const memory_1 = require("../mind/memory");
48
- const postIt = (msg) => `post-it from past you:\n${msg}`;
54
+ const coding_1 = require("./coding");
55
+ const diary_1 = require("../mind/diary");
56
+ const tasks_1 = require("./tasks");
57
+ const pending_1 = require("../mind/pending");
58
+ const obligations_1 = require("../mind/obligations");
59
+ const progress_story_1 = require("../heart/progress-story");
60
+ const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
61
+ const obligations_2 = require("../heart/obligations");
62
+ // Tracks which file paths have been read via read_file in this session.
63
+ // edit_file requires a file to be read first (must-read-first guard).
64
+ exports.editFileReadTracker = new Set();
65
+ function buildContextDiff(lines, changeStart, changeEnd, contextSize = 3) {
66
+ const start = Math.max(0, changeStart - contextSize);
67
+ const end = Math.min(lines.length, changeEnd + contextSize);
68
+ const result = [];
69
+ for (let i = start; i < end; i++) {
70
+ const lineNum = i + 1;
71
+ const prefix = (i >= changeStart && i < changeEnd) ? ">" : " ";
72
+ result.push(`${prefix} ${lineNum} | ${lines[i]}`);
73
+ }
74
+ return result.join("\n");
75
+ }
76
+ function resolveLocalToolPath(targetPath) {
77
+ if (!path.isAbsolute(targetPath)) {
78
+ return path.resolve((0, identity_2.getRepoRoot)(), targetPath);
79
+ }
80
+ return targetPath;
81
+ }
82
+ const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
83
+ const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
84
+ function findDelegatingBridgeId(ctx) {
85
+ const currentSession = ctx?.currentSession;
86
+ if (!currentSession)
87
+ return undefined;
88
+ return ctx?.activeBridges?.find((bridge) => bridge.lifecycle === "active"
89
+ && bridge.attachedSessions.some((session) => session.friendId === currentSession.friendId
90
+ && session.channel === currentSession.channel
91
+ && session.key === currentSession.key))?.id;
92
+ }
93
+ async function recallSessionSafely(options) {
94
+ try {
95
+ return await (0, session_recall_1.recallSession)(options);
96
+ }
97
+ catch (error) {
98
+ if (options.summarize) {
99
+ (0, runtime_1.emitNervesEvent)({
100
+ component: "daemon",
101
+ event: "daemon.session_recall_summary_fallback",
102
+ message: "session recall summarization failed; using raw transcript",
103
+ meta: {
104
+ friendId: options.friendId,
105
+ channel: options.channel,
106
+ key: options.key,
107
+ error: error instanceof Error ? error.message : String(error),
108
+ },
109
+ });
110
+ try {
111
+ return await (0, session_recall_1.recallSession)({
112
+ ...options,
113
+ summarize: undefined,
114
+ });
115
+ }
116
+ catch {
117
+ return { kind: "missing" };
118
+ }
119
+ }
120
+ return { kind: "missing" };
121
+ }
122
+ }
123
+ async function searchSessionSafely(options) {
124
+ try {
125
+ return await (0, session_recall_1.searchSessionTranscript)(options);
126
+ }
127
+ catch {
128
+ return { kind: "missing" };
129
+ }
130
+ }
131
+ function normalizeProgressOutcome(text) {
132
+ const trimmed = text.trim();
133
+ /* v8 ignore next -- defensive: normalizeProgressOutcome null branch @preserve */
134
+ if (!trimmed || trimmed === "nothing yet" || trimmed === "nothing recent") {
135
+ return null;
136
+ }
137
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) {
138
+ return trimmed.slice(1, -1);
139
+ }
140
+ return trimmed;
141
+ }
142
+ function writePendingEnvelope(queueDir, message) {
143
+ fs.mkdirSync(queueDir, { recursive: true });
144
+ const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
145
+ const filePath = path.join(queueDir, fileName);
146
+ fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
147
+ }
148
+ function renderCrossChatDeliveryStatus(target, result) {
149
+ const phase = result.status === "delivered_now"
150
+ ? "completed"
151
+ : result.status === "queued_for_later"
152
+ ? "queued"
153
+ : result.status === "blocked"
154
+ ? "blocked"
155
+ : "errored";
156
+ const lead = result.status === "delivered_now"
157
+ ? "delivered now"
158
+ : result.status === "queued_for_later"
159
+ ? "queued for later"
160
+ : result.status === "blocked"
161
+ ? "blocked"
162
+ : "failed";
163
+ return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
164
+ scope: "shared-work",
165
+ phase,
166
+ objective: `message to ${target}`,
167
+ outcomeText: `${lead}\n${result.detail}`,
168
+ }));
169
+ }
170
+ function emptyTaskBoard() {
171
+ return {
172
+ compact: "",
173
+ full: "",
174
+ byStatus: {
175
+ drafting: [],
176
+ processing: [],
177
+ validating: [],
178
+ collaborating: [],
179
+ paused: [],
180
+ blocked: [],
181
+ done: [],
182
+ cancelled: [],
183
+ },
184
+ issues: [],
185
+ actionRequired: [],
186
+ unresolvedDependencies: [],
187
+ activeSessions: [],
188
+ activeBridges: [],
189
+ };
190
+ }
191
+ function isLiveCodingSessionStatus(status) {
192
+ return status === "spawning"
193
+ || status === "running"
194
+ || status === "waiting_input"
195
+ || status === "stalled";
196
+ }
197
+ function readActiveWorkInnerState() {
198
+ const defaultJob = {
199
+ status: "idle",
200
+ content: null,
201
+ origin: null,
202
+ mode: "reflect",
203
+ obligationStatus: null,
204
+ surfacedResult: null,
205
+ queuedAt: null,
206
+ startedAt: null,
207
+ surfacedAt: null,
208
+ };
209
+ try {
210
+ const agentRoot = (0, identity_1.getAgentRoot)();
211
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
212
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
213
+ const { pendingMessages, turns, runtimeState } = (0, thoughts_1.readInnerDialogRawData)(sessionPath, pendingDir);
214
+ const dialogStatus = (0, thoughts_1.deriveInnerDialogStatus)(pendingMessages, turns, runtimeState);
215
+ const job = (0, thoughts_1.deriveInnerJob)(pendingMessages, turns, runtimeState);
216
+ const storeObligationPending = (0, obligations_2.readPendingObligations)(agentRoot).length > 0;
217
+ return {
218
+ status: dialogStatus.processing === "started" ? "running" : "idle",
219
+ hasPending: dialogStatus.queue !== "clear",
220
+ origin: dialogStatus.origin,
221
+ contentSnippet: dialogStatus.contentSnippet,
222
+ obligationPending: dialogStatus.obligationPending || storeObligationPending,
223
+ job,
224
+ };
225
+ }
226
+ catch {
227
+ return {
228
+ status: "idle",
229
+ hasPending: false,
230
+ job: defaultJob,
231
+ };
232
+ }
233
+ }
234
+ async function buildToolActiveWorkFrame(ctx) {
235
+ const currentSession = ctx?.currentSession
236
+ ? {
237
+ friendId: ctx.currentSession.friendId,
238
+ channel: ctx.currentSession.channel,
239
+ key: ctx.currentSession.key,
240
+ sessionPath: (0, config_1.resolveSessionPath)(ctx.currentSession.friendId, ctx.currentSession.channel, ctx.currentSession.key),
241
+ }
242
+ : null;
243
+ const agentRoot = (0, identity_1.getAgentRoot)();
244
+ const bridges = currentSession
245
+ ? (0, manager_1.createBridgeManager)().findBridgesForSession({
246
+ friendId: currentSession.friendId,
247
+ channel: currentSession.channel,
248
+ key: currentSession.key,
249
+ })
250
+ : [];
251
+ let friendActivity = [];
252
+ try {
253
+ friendActivity = (0, session_activity_1.listSessionActivity)({
254
+ sessionsDir: `${agentRoot}/state/sessions`,
255
+ friendsDir: `${agentRoot}/friends`,
256
+ agentName: (0, identity_1.getAgentName)(),
257
+ currentSession,
258
+ });
259
+ }
260
+ catch {
261
+ friendActivity = [];
262
+ }
263
+ const pendingObligations = (() => {
264
+ try {
265
+ return (0, obligations_2.readPendingObligations)(agentRoot);
266
+ }
267
+ catch {
268
+ return [];
269
+ }
270
+ })();
271
+ let codingSessions = [];
272
+ let otherCodingSessions = [];
273
+ try {
274
+ const liveCodingSessions = (0, coding_1.getCodingSessionManager)()
275
+ .listSessions()
276
+ .filter((session) => isLiveCodingSessionStatus(session.status) && Boolean(session.originSession));
277
+ if (currentSession) {
278
+ codingSessions = liveCodingSessions.filter((session) => session.originSession?.friendId === currentSession.friendId
279
+ && session.originSession.channel === currentSession.channel
280
+ && session.originSession.key === currentSession.key);
281
+ otherCodingSessions = liveCodingSessions.filter((session) => !(session.originSession?.friendId === currentSession.friendId
282
+ && session.originSession.channel === currentSession.channel
283
+ && session.originSession.key === currentSession.key));
284
+ }
285
+ else {
286
+ codingSessions = [];
287
+ otherCodingSessions = liveCodingSessions;
288
+ }
289
+ }
290
+ catch {
291
+ codingSessions = [];
292
+ otherCodingSessions = [];
293
+ }
294
+ const currentObligation = currentSession
295
+ ? pendingObligations.find((obligation) => obligation.status !== "fulfilled"
296
+ && obligation.origin.friendId === currentSession.friendId
297
+ && obligation.origin.channel === currentSession.channel
298
+ && obligation.origin.key === currentSession.key)?.content ?? null
299
+ : null;
300
+ return (0, active_work_1.buildActiveWorkFrame)({
301
+ currentSession,
302
+ currentObligation,
303
+ mustResolveBeforeHandoff: false,
304
+ inner: readActiveWorkInnerState(),
305
+ bridges,
306
+ codingSessions,
307
+ otherCodingSessions,
308
+ pendingObligations,
309
+ taskBoard: (() => {
310
+ try {
311
+ return (0, tasks_1.getTaskModule)().getBoard();
312
+ }
313
+ catch {
314
+ return emptyTaskBoard();
315
+ }
316
+ })(),
317
+ friendActivity,
318
+ targetCandidates: [],
319
+ });
320
+ }
321
+ function renderInnerProgressStatus(status) {
322
+ if (status.processing === "pending") {
323
+ return "i've queued this thought for private attention. it'll come up when my inner dialog is free.";
324
+ }
325
+ if (status.processing === "started") {
326
+ return "i'm working through this privately right now.";
327
+ }
328
+ // processed / completed
329
+ if (status.surfaced && status.surfaced !== "nothing recent" && status.surfaced !== "no outward result") {
330
+ return `i thought about this privately and came to something: ${status.surfaced}`;
331
+ }
332
+ return "i thought about this privately. i'll bring it back when the time is right.";
333
+ }
49
334
  exports.baseToolDefinitions = [
50
335
  {
51
336
  tool: {
@@ -55,12 +340,29 @@ exports.baseToolDefinitions = [
55
340
  description: "read file contents",
56
341
  parameters: {
57
342
  type: "object",
58
- properties: { path: { type: "string" } },
343
+ properties: {
344
+ path: { type: "string" },
345
+ offset: { type: "number", description: "1-based line number to start reading from" },
346
+ limit: { type: "number", description: "maximum number of lines to return" },
347
+ },
59
348
  required: ["path"],
60
349
  },
61
350
  },
62
351
  },
63
- handler: (a) => fs.readFileSync(a.path, "utf-8"),
352
+ handler: (a) => {
353
+ const resolvedPath = resolveLocalToolPath(a.path);
354
+ const content = fs.readFileSync(resolvedPath, "utf-8");
355
+ exports.editFileReadTracker.add(resolvedPath);
356
+ const offset = a.offset ? parseInt(a.offset, 10) : undefined;
357
+ const limit = a.limit ? parseInt(a.limit, 10) : undefined;
358
+ if (offset === undefined && limit === undefined)
359
+ return content;
360
+ const lines = content.split("\n");
361
+ const start = offset ? offset - 1 : 0;
362
+ const end = limit !== undefined ? start + limit : lines.length;
363
+ return lines.slice(start, end).join("\n");
364
+ },
365
+ summaryKeys: ["path"],
64
366
  },
65
367
  {
66
368
  tool: {
@@ -75,103 +377,225 @@ exports.baseToolDefinitions = [
75
377
  },
76
378
  },
77
379
  },
78
- handler: (a) => (fs.writeFileSync(a.path, a.content, "utf-8"), "ok"),
380
+ handler: (a) => {
381
+ const resolvedPath = resolveLocalToolPath(a.path);
382
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
383
+ fs.writeFileSync(resolvedPath, a.content, "utf-8");
384
+ return "ok";
385
+ },
386
+ summaryKeys: ["path"],
79
387
  },
80
388
  {
81
389
  tool: {
82
390
  type: "function",
83
391
  function: {
84
- name: "shell",
85
- description: "run shell command",
392
+ name: "edit_file",
393
+ description: "surgically edit a file by replacing an exact string. the file must have been read via read_file first. old_string must match exactly one location in the file.",
86
394
  parameters: {
87
395
  type: "object",
88
- properties: { command: { type: "string" } },
89
- required: ["command"],
396
+ properties: {
397
+ path: { type: "string" },
398
+ old_string: { type: "string" },
399
+ new_string: { type: "string" },
400
+ },
401
+ required: ["path", "old_string", "new_string"],
90
402
  },
91
403
  },
92
404
  },
93
- handler: (a) => (0, child_process_1.execSync)(a.command, { encoding: "utf-8", timeout: 30000 }),
405
+ handler: (a) => {
406
+ const resolvedPath = resolveLocalToolPath(a.path);
407
+ if (!exports.editFileReadTracker.has(resolvedPath)) {
408
+ return `error: you must read the file with read_file before editing it. call read_file on ${a.path} first.`;
409
+ }
410
+ let content;
411
+ try {
412
+ content = fs.readFileSync(resolvedPath, "utf-8");
413
+ }
414
+ catch (e) {
415
+ return `error: could not read file: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
416
+ }
417
+ // Count occurrences
418
+ const occurrences = [];
419
+ let searchFrom = 0;
420
+ while (true) {
421
+ const idx = content.indexOf(a.old_string, searchFrom);
422
+ if (idx === -1)
423
+ break;
424
+ occurrences.push(idx);
425
+ searchFrom = idx + 1;
426
+ }
427
+ if (occurrences.length === 0) {
428
+ return `error: old_string not found in ${a.path}`;
429
+ }
430
+ if (occurrences.length > 1) {
431
+ return `error: old_string is ambiguous -- found ${occurrences.length} matches in ${a.path}. provide more context to make the match unique.`;
432
+ }
433
+ // Single unique match -- replace
434
+ const idx = occurrences[0];
435
+ const updated = content.slice(0, idx) + a.new_string + content.slice(idx + a.old_string.length);
436
+ fs.writeFileSync(resolvedPath, updated, "utf-8");
437
+ // Build contextual diff
438
+ const lines = updated.split("\n");
439
+ const prefixLines = content.slice(0, idx).split("\n");
440
+ const changeStartLine = prefixLines.length - 1;
441
+ const newStringLines = a.new_string.split("\n");
442
+ const changeEndLine = changeStartLine + newStringLines.length;
443
+ return buildContextDiff(lines, changeStartLine, changeEndLine);
444
+ },
445
+ summaryKeys: ["path"],
94
446
  },
95
447
  {
96
448
  tool: {
97
449
  type: "function",
98
450
  function: {
99
- name: "list_directory",
100
- description: "list directory contents",
451
+ name: "glob",
452
+ description: "find files matching a glob pattern. returns matching paths sorted alphabetically, one per line.",
101
453
  parameters: {
102
454
  type: "object",
103
- properties: { path: { type: "string" } },
104
- required: ["path"],
455
+ properties: {
456
+ pattern: { type: "string", description: "glob pattern (e.g. **/*.ts)" },
457
+ cwd: { type: "string", description: "directory to search from (defaults to process.cwd())" },
458
+ },
459
+ required: ["pattern"],
105
460
  },
106
461
  },
107
462
  },
108
- handler: (a) => fs
109
- .readdirSync(a.path, { withFileTypes: true })
110
- .map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
111
- .join("\n"),
463
+ handler: (a) => {
464
+ const cwd = a.cwd ? resolveLocalToolPath(a.cwd) : process.cwd();
465
+ const matches = fg.globSync(a.pattern, { cwd, dot: true });
466
+ return matches.sort().join("\n");
467
+ },
468
+ summaryKeys: ["pattern", "cwd"],
112
469
  },
113
470
  {
114
471
  tool: {
115
472
  type: "function",
116
473
  function: {
117
- name: "git_commit",
118
- description: "commit changes to git with explicit paths",
474
+ name: "grep",
475
+ description: "search file contents for lines matching a regex pattern. searches recursively when given a directory. returns matching lines with file path and line numbers.",
119
476
  parameters: {
120
477
  type: "object",
121
478
  properties: {
122
- message: { type: "string" },
123
- paths: { type: "array", items: { type: "string" } },
479
+ pattern: { type: "string", description: "regex pattern to search for" },
480
+ path: { type: "string", description: "file or directory to search" },
481
+ context_lines: { type: "number", description: "number of surrounding context lines (default 0)" },
482
+ include: { type: "string", description: "glob filter to limit searched files (e.g. *.ts)" },
124
483
  },
125
- required: ["message", "paths"],
484
+ required: ["pattern", "path"],
126
485
  },
127
486
  },
128
487
  },
129
488
  handler: (a) => {
130
- try {
131
- if (!a.paths || !Array.isArray(a.paths) || a.paths.length === 0) {
132
- return postIt("paths are required. specify explicit files to commit.");
489
+ const targetPath = resolveLocalToolPath(a.path);
490
+ const regex = new RegExp(a.pattern);
491
+ const contextLines = parseInt(a.context_lines || "0", 10);
492
+ const includeGlob = a.include || undefined;
493
+ function searchFile(filePath) {
494
+ let content;
495
+ try {
496
+ content = fs.readFileSync(filePath, "utf-8");
133
497
  }
134
- for (const p of a.paths) {
135
- if (!fs.existsSync(p)) {
136
- return postIt(`path does not exist: ${p}`);
498
+ catch {
499
+ return [];
500
+ }
501
+ const lines = content.split("\n");
502
+ const matchIndices = new Set();
503
+ for (let i = 0; i < lines.length; i++) {
504
+ if (regex.test(lines[i])) {
505
+ matchIndices.add(i);
506
+ }
507
+ }
508
+ if (matchIndices.size === 0)
509
+ return [];
510
+ const outputIndices = new Set();
511
+ for (const idx of matchIndices) {
512
+ const start = Math.max(0, idx - contextLines);
513
+ const end = Math.min(lines.length - 1, idx + contextLines);
514
+ for (let i = start; i <= end; i++) {
515
+ outputIndices.add(i);
137
516
  }
138
- (0, child_process_1.execSync)(`git add ${p}`, { encoding: "utf-8" });
139
517
  }
140
- const diff = (0, child_process_1.execSync)("git diff --cached --stat", { encoding: "utf-8" });
141
- if (!diff || diff.trim().length === 0) {
142
- return postIt("nothing was staged. check your changes or paths.");
518
+ const sortedIndices = [...outputIndices].sort((a, b) => a - b);
519
+ const results = [];
520
+ for (const idx of sortedIndices) {
521
+ const lineNum = idx + 1;
522
+ if (matchIndices.has(idx)) {
523
+ results.push(`${filePath}:${lineNum}: ${lines[idx]}`);
524
+ }
525
+ else {
526
+ results.push(`-${filePath}:${lineNum}: ${lines[idx]}`);
527
+ }
143
528
  }
144
- (0, child_process_1.execSync)(`git commit -m \"${a.message}\"`, { encoding: "utf-8" });
145
- return `${diff}\ncommitted`;
529
+ return results;
146
530
  }
147
- catch (e) {
148
- return `failed: ${e}`;
531
+ function collectFiles(dirPath) {
532
+ const files = [];
533
+ function walk(dir) {
534
+ let entries;
535
+ try {
536
+ entries = fs.readdirSync(dir, { withFileTypes: true });
537
+ }
538
+ catch {
539
+ return;
540
+ }
541
+ for (const entry of entries) {
542
+ const fullPath = path.join(dir, entry.name);
543
+ if (entry.isDirectory()) {
544
+ walk(fullPath);
545
+ }
546
+ else if (entry.isFile()) {
547
+ files.push(fullPath);
548
+ }
549
+ }
550
+ }
551
+ walk(dirPath);
552
+ return files.sort();
553
+ }
554
+ function matchesGlob(filePath, glob) {
555
+ const escaped = glob
556
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
557
+ .replace(/\*/g, ".*")
558
+ .replace(/\?/g, ".");
559
+ return new RegExp(`(^|/)${escaped}$`).test(filePath);
560
+ }
561
+ const stat = fs.statSync(targetPath, { throwIfNoEntry: false });
562
+ if (!stat)
563
+ return "";
564
+ if (stat.isFile()) {
565
+ return searchFile(targetPath).join("\n");
566
+ }
567
+ let files = collectFiles(targetPath);
568
+ if (includeGlob) {
569
+ files = files.filter((f) => matchesGlob(f, includeGlob));
570
+ }
571
+ const allResults = [];
572
+ for (const file of files) {
573
+ allResults.push(...searchFile(file));
149
574
  }
575
+ return allResults.join("\n");
150
576
  },
577
+ summaryKeys: ["pattern", "path", "include"],
151
578
  },
152
579
  {
153
580
  tool: {
154
581
  type: "function",
155
582
  function: {
156
- name: "gh_cli",
157
- description: "execute a GitHub CLI (gh) command. use carefully.",
583
+ name: "shell",
584
+ description: "run shell command",
158
585
  parameters: {
159
586
  type: "object",
160
- properties: {
161
- command: { type: "string" },
162
- },
587
+ properties: { command: { type: "string" } },
163
588
  required: ["command"],
164
589
  },
165
590
  },
166
591
  },
167
592
  handler: (a) => {
168
- try {
169
- return (0, child_process_1.execSync)(`gh ${a.command}`, { encoding: "utf-8", timeout: 60000 });
170
- }
171
- catch (e) {
172
- return `error: ${e}`;
173
- }
593
+ return (0, child_process_1.execSync)(a.command, {
594
+ encoding: "utf-8",
595
+ timeout: 30000,
596
+ });
174
597
  },
598
+ summaryKeys: ["command"],
175
599
  },
176
600
  {
177
601
  tool: {
@@ -205,20 +629,7 @@ exports.baseToolDefinitions = [
205
629
  return `error: ${e}`;
206
630
  }
207
631
  },
208
- },
209
- {
210
- tool: {
211
- type: "function",
212
- function: {
213
- name: "get_current_time",
214
- description: "get the current date and time in America/Los_Angeles (Pacific Time)",
215
- parameters: { type: "object", properties: {} },
216
- },
217
- },
218
- handler: () => new Date().toLocaleString("en-US", {
219
- timeZone: "America/Los_Angeles",
220
- hour12: false,
221
- }),
632
+ summaryKeys: ["name"],
222
633
  },
223
634
  {
224
635
  tool: {
@@ -235,7 +646,7 @@ exports.baseToolDefinitions = [
235
646
  },
236
647
  handler: (a) => {
237
648
  try {
238
- const result = (0, child_process_1.spawnSync)("claude", ["-p", "--dangerously-skip-permissions", "--add-dir", "."], {
649
+ const result = (0, child_process_1.spawnSync)("claude", ["-p", "--no-session-persistence", "--dangerously-skip-permissions", "--add-dir", "."], {
239
650
  input: a.prompt,
240
651
  encoding: "utf-8",
241
652
  timeout: 60000,
@@ -250,6 +661,7 @@ exports.baseToolDefinitions = [
250
661
  return `error: ${e}`;
251
662
  }
252
663
  },
664
+ summaryKeys: ["prompt"],
253
665
  },
254
666
  {
255
667
  tool: {
@@ -290,13 +702,14 @@ exports.baseToolDefinitions = [
290
702
  return `error: ${e}`;
291
703
  }
292
704
  },
705
+ summaryKeys: ["query"],
293
706
  },
294
707
  {
295
708
  tool: {
296
709
  type: "function",
297
710
  function: {
298
- name: "memory_search",
299
- description: "search remembered facts stored in psyche memory and return relevant matches for a query",
711
+ name: "recall",
712
+ description: "recall what i know — search my diary and journal for facts, thoughts, and working notes that match a query",
300
713
  parameters: {
301
714
  type: "object",
302
715
  properties: { query: { type: "string" } },
@@ -309,44 +722,69 @@ exports.baseToolDefinitions = [
309
722
  const query = (a.query || "").trim();
310
723
  if (!query)
311
724
  return "query is required";
312
- const memoryRoot = path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
313
- const hits = await (0, memory_1.searchMemoryFacts)(query, (0, memory_1.readMemoryFacts)(memoryRoot));
314
- return hits
315
- .map((fact) => `- ${fact.text} (source=${fact.source}, createdAt=${fact.createdAt})`)
316
- .join("\n");
725
+ const resultLines = [];
726
+ // Search diary entries
727
+ const hits = await (0, diary_1.searchDiaryEntries)(query, (0, diary_1.readDiaryEntries)());
728
+ for (const fact of hits) {
729
+ resultLines.push(`[diary] ${fact.text} (source=${fact.source}, createdAt=${fact.createdAt})`);
730
+ }
731
+ // Search journal index
732
+ const agentRoot = (0, identity_1.getAgentRoot)();
733
+ const journalIndexPath = path.join(agentRoot, "journal", ".index.json");
734
+ try {
735
+ const raw = fs.readFileSync(journalIndexPath, "utf8");
736
+ const journalEntries = JSON.parse(raw);
737
+ if (Array.isArray(journalEntries) && journalEntries.length > 0) {
738
+ // Substring match on preview and filename
739
+ const lowerQuery = query.toLowerCase();
740
+ for (const entry of journalEntries) {
741
+ /* v8 ignore next 4 -- both sides tested (filename-only match in recall-journal.test.ts); v8 misreports || short-circuit @preserve */
742
+ if (entry.preview.toLowerCase().includes(lowerQuery) ||
743
+ entry.filename.toLowerCase().includes(lowerQuery)) {
744
+ resultLines.push(`[journal] ${entry.filename}: ${entry.preview}`);
745
+ }
746
+ }
747
+ }
748
+ }
749
+ catch {
750
+ // No journal index or malformed — skip journal search
751
+ }
752
+ return resultLines.join("\n");
317
753
  }
318
754
  catch (e) {
319
755
  return `error: ${e instanceof Error ? e.message : String(e)}`;
320
756
  }
321
757
  },
758
+ summaryKeys: ["query"],
322
759
  },
323
760
  {
324
761
  tool: {
325
762
  type: "function",
326
763
  function: {
327
- name: "memory_save",
328
- description: "save a general memory fact i want to recall later. optional 'about' can tag the fact to a person/topic/context",
764
+ name: "diary_write",
765
+ description: "write an entry in my diary — something i learned, noticed, or concluded that i want to recall later. optional 'about' tags the entry to a person, topic, or context.",
329
766
  parameters: {
330
767
  type: "object",
331
768
  properties: {
332
- text: { type: "string" },
769
+ entry: { type: "string" },
333
770
  about: { type: "string" },
334
771
  },
335
- required: ["text"],
772
+ required: ["entry"],
336
773
  },
337
774
  },
338
775
  },
339
776
  handler: async (a) => {
340
- const text = (a.text || "").trim();
341
- if (!text)
342
- return "text is required";
343
- const result = await (0, memory_1.saveMemoryFact)({
344
- text,
345
- source: "tool:memory_save",
777
+ const entry = (a.entry || "").trim();
778
+ if (!entry)
779
+ return "entry is required";
780
+ const result = await (0, diary_1.saveDiaryEntry)({
781
+ text: entry,
782
+ source: "tool:diary_write",
346
783
  about: typeof a.about === "string" ? a.about : undefined,
347
784
  });
348
- return `saved memory fact (added=${result.added}, skipped=${result.skipped})`;
785
+ return `saved diary entry (added=${result.added}, skipped=${result.skipped})`;
349
786
  },
787
+ summaryKeys: ["entry", "about"],
350
788
  },
351
789
  {
352
790
  tool: {
@@ -374,151 +812,7 @@ exports.baseToolDefinitions = [
374
812
  return `friend not found: ${friendId}`;
375
813
  return JSON.stringify(friend, null, 2);
376
814
  },
377
- },
378
- {
379
- tool: {
380
- type: "function",
381
- function: {
382
- name: "task_board",
383
- description: "show the task board grouped by status",
384
- parameters: { type: "object", properties: {} },
385
- },
386
- },
387
- handler: () => {
388
- const board = (0, tasks_1.getTaskModule)().getBoard();
389
- return board.full || board.compact || "no tasks found";
390
- },
391
- },
392
- {
393
- tool: {
394
- type: "function",
395
- function: {
396
- name: "task_create",
397
- description: "create a new task in the bundle task system",
398
- parameters: {
399
- type: "object",
400
- properties: {
401
- title: { type: "string" },
402
- type: { type: "string", enum: ["one-shot", "ongoing", "habit"] },
403
- category: { type: "string" },
404
- body: { type: "string" },
405
- },
406
- required: ["title", "type", "category", "body"],
407
- },
408
- },
409
- },
410
- handler: (a) => {
411
- try {
412
- const created = (0, tasks_1.getTaskModule)().createTask({
413
- title: a.title,
414
- type: a.type,
415
- category: a.category,
416
- body: a.body,
417
- });
418
- return `created: ${created}`;
419
- }
420
- catch (error) {
421
- return `error: ${error instanceof Error ? error.message : String(error)}`;
422
- }
423
- },
424
- },
425
- {
426
- tool: {
427
- type: "function",
428
- function: {
429
- name: "task_update_status",
430
- description: "update a task status using validated transitions",
431
- parameters: {
432
- type: "object",
433
- properties: {
434
- name: { type: "string" },
435
- status: { type: "string" },
436
- },
437
- required: ["name", "status"],
438
- },
439
- },
440
- },
441
- handler: (a) => {
442
- const result = (0, tasks_1.getTaskModule)().updateStatus(a.name, a.status);
443
- if (!result.ok) {
444
- return `error: ${result.reason ?? "status update failed"}`;
445
- }
446
- const archivedSuffix = result.archived && result.archived.length > 0
447
- ? ` | archived: ${result.archived.join(", ")}`
448
- : "";
449
- return `updated: ${a.name} -> ${result.to}${archivedSuffix}`;
450
- },
451
- },
452
- {
453
- tool: {
454
- type: "function",
455
- function: {
456
- name: "task_board_status",
457
- description: "show board detail for a specific status",
458
- parameters: {
459
- type: "object",
460
- properties: {
461
- status: { type: "string" },
462
- },
463
- required: ["status"],
464
- },
465
- },
466
- },
467
- handler: (a) => {
468
- const lines = (0, tasks_1.getTaskModule)().boardStatus(a.status);
469
- return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
470
- },
471
- },
472
- {
473
- tool: {
474
- type: "function",
475
- function: {
476
- name: "task_board_action",
477
- description: "show tasks or validation issues that require action",
478
- parameters: {
479
- type: "object",
480
- properties: {
481
- scope: { type: "string" },
482
- },
483
- },
484
- },
485
- },
486
- handler: (a) => {
487
- const lines = (0, tasks_1.getTaskModule)().boardAction();
488
- if (!a.scope) {
489
- return lines.length > 0 ? lines.join("\n") : "no action required";
490
- }
491
- const filtered = lines.filter((line) => line.includes(a.scope));
492
- return filtered.length > 0 ? filtered.join("\n") : "no matching action items";
493
- },
494
- },
495
- {
496
- tool: {
497
- type: "function",
498
- function: {
499
- name: "task_board_deps",
500
- description: "show unresolved task dependencies",
501
- parameters: { type: "object", properties: {} },
502
- },
503
- },
504
- handler: () => {
505
- const lines = (0, tasks_1.getTaskModule)().boardDeps();
506
- return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
507
- },
508
- },
509
- {
510
- tool: {
511
- type: "function",
512
- function: {
513
- name: "task_board_sessions",
514
- description: "show tasks with active coding or sub-agent sessions",
515
- parameters: { type: "object", properties: {} },
516
- },
517
- },
518
- handler: () => {
519
- const lines = (0, tasks_1.getTaskModule)().boardSessions();
520
- return lines.length > 0 ? lines.join("\n") : "no active sessions";
521
- },
815
+ summaryKeys: ["friendId"],
522
816
  },
523
817
  {
524
818
  tool: {
@@ -602,55 +896,214 @@ exports.baseToolDefinitions = [
602
896
  return `error saving note: ${err instanceof Error ? err.message : String(err)}`;
603
897
  }
604
898
  },
899
+ summaryKeys: ["type", "key", "content"],
605
900
  },
606
901
  // -- cross-session awareness --
902
+ {
903
+ tool: {
904
+ type: "function",
905
+ function: {
906
+ name: "bridge_manage",
907
+ description: "create and manage shared live-work bridges across already-active sessions.",
908
+ parameters: {
909
+ type: "object",
910
+ properties: {
911
+ action: {
912
+ type: "string",
913
+ enum: ["begin", "attach", "status", "promote_task", "complete", "cancel"],
914
+ },
915
+ bridgeId: { type: "string", description: "bridge id for all actions except begin" },
916
+ objective: { type: "string", description: "objective for begin" },
917
+ summary: { type: "string", description: "optional concise shared-work summary" },
918
+ friendId: { type: "string", description: "target friend id for attach" },
919
+ channel: { type: "string", description: "target channel for attach" },
920
+ key: { type: "string", description: "target session key for attach (defaults to 'session')" },
921
+ title: { type: "string", description: "task title override for promote_task" },
922
+ category: { type: "string", description: "task category override for promote_task" },
923
+ body: { type: "string", description: "task body override for promote_task" },
924
+ },
925
+ required: ["action"],
926
+ },
927
+ },
928
+ },
929
+ handler: async (args, ctx) => {
930
+ const manager = (0, manager_1.createBridgeManager)();
931
+ const action = (args.action || "").trim();
932
+ if (action === "begin") {
933
+ if (!ctx?.currentSession) {
934
+ return "bridge_manage begin requires an active session context.";
935
+ }
936
+ const objective = (args.objective || "").trim();
937
+ if (!objective)
938
+ return "objective is required for bridge begin.";
939
+ return (0, manager_1.formatBridgeStatus)(manager.beginBridge({
940
+ objective,
941
+ summary: (args.summary || objective).trim(),
942
+ session: ctx.currentSession,
943
+ }));
944
+ }
945
+ const bridgeId = (args.bridgeId || "").trim();
946
+ if (!bridgeId) {
947
+ return "bridgeId is required for this bridge action.";
948
+ }
949
+ if (action === "attach") {
950
+ const friendId = (args.friendId || "").trim();
951
+ const channel = (args.channel || "").trim();
952
+ const key = (args.key || "session").trim();
953
+ if (!friendId || !channel) {
954
+ return "friendId and channel are required for bridge attach.";
955
+ }
956
+ const sessionPath = (0, config_1.resolveSessionPath)(friendId, channel, key);
957
+ const recall = await recallSessionSafely({
958
+ sessionPath,
959
+ friendId,
960
+ channel,
961
+ key,
962
+ messageCount: 20,
963
+ trustLevel: ctx?.context?.friend?.trustLevel,
964
+ summarize: ctx?.summarize,
965
+ });
966
+ if (recall.kind === "missing") {
967
+ return NO_SESSION_FOUND_MESSAGE;
968
+ }
969
+ return (0, manager_1.formatBridgeStatus)(manager.attachSession(bridgeId, {
970
+ friendId,
971
+ channel,
972
+ key,
973
+ sessionPath,
974
+ snapshot: recall.kind === "ok" ? recall.snapshot : EMPTY_SESSION_MESSAGE,
975
+ }));
976
+ }
977
+ if (action === "status") {
978
+ const bridge = manager.getBridge(bridgeId);
979
+ if (!bridge)
980
+ return `bridge not found: ${bridgeId}`;
981
+ return (0, manager_1.formatBridgeStatus)(bridge);
982
+ }
983
+ if (action === "promote_task") {
984
+ return (0, manager_1.formatBridgeStatus)(manager.promoteBridgeToTask(bridgeId, {
985
+ title: args.title,
986
+ category: args.category,
987
+ body: args.body,
988
+ }));
989
+ }
990
+ if (action === "complete") {
991
+ return (0, manager_1.formatBridgeStatus)(manager.completeBridge(bridgeId));
992
+ }
993
+ if (action === "cancel") {
994
+ return (0, manager_1.formatBridgeStatus)(manager.cancelBridge(bridgeId));
995
+ }
996
+ return `unknown bridge action: ${action}`;
997
+ },
998
+ summaryKeys: ["action", "bridgeId", "objective", "friendId", "channel", "key"],
999
+ },
1000
+ {
1001
+ tool: {
1002
+ type: "function",
1003
+ function: {
1004
+ name: "query_active_work",
1005
+ description: "read the current live world-state across visible sessions, coding lanes, inner work, and return obligations. use this instead of piecing status together from separate session and coding tools.",
1006
+ parameters: {
1007
+ type: "object",
1008
+ properties: {},
1009
+ },
1010
+ },
1011
+ },
1012
+ handler: async (_args, ctx) => {
1013
+ const frame = await buildToolActiveWorkFrame(ctx);
1014
+ return `this is my current top-level live world-state.\nanswer whole-self status questions from this before drilling into individual sessions.\n\n${(0, active_work_1.formatActiveWorkFrame)(frame)}`;
1015
+ },
1016
+ },
607
1017
  {
608
1018
  tool: {
609
1019
  type: "function",
610
1020
  function: {
611
1021
  name: "query_session",
612
- description: "read the last messages from another session. use this to check on a conversation with a friend or review your own inner dialog.",
1022
+ description: "inspect another session. use transcript for recent context, status for self/inner progress, or search to find older history by query.",
613
1023
  parameters: {
614
1024
  type: "object",
615
1025
  properties: {
616
1026
  friendId: { type: "string", description: "the friend UUID (or 'self')" },
617
- channel: { type: "string", description: "the channel: cli, teams, or inner" },
1027
+ channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
618
1028
  key: { type: "string", description: "session key (defaults to 'session')" },
619
1029
  messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
1030
+ mode: {
1031
+ type: "string",
1032
+ enum: ["transcript", "status", "search"],
1033
+ description: "transcript (default), lightweight status for self/inner checks, or search for older history",
1034
+ },
1035
+ query: { type: "string", description: "required when mode=search; search term for older session history" },
620
1036
  },
621
1037
  required: ["friendId", "channel"],
622
1038
  },
623
1039
  },
624
1040
  },
625
1041
  handler: async (args, ctx) => {
626
- try {
627
- const friendId = args.friendId;
628
- const channel = args.channel;
629
- const key = args.key || "session";
630
- const count = parseInt(args.messageCount || "20", 10);
631
- const sessFile = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel, `${key}.json`);
632
- const raw = fs.readFileSync(sessFile, "utf-8");
633
- const data = JSON.parse(raw);
634
- const messages = (data.messages || [])
635
- .filter((m) => m.role !== "system");
636
- const tail = messages.slice(-count);
637
- if (tail.length === 0)
638
- return "session exists but has no non-system messages.";
639
- const transcript = tail.map((m) => `[${m.role}] ${m.content}`).join("\n");
640
- // LLM summarization when summarize function is available
641
- if (ctx?.summarize) {
642
- const trustLevel = ctx.context?.friend?.trustLevel ?? "family";
643
- const isSelfQuery = friendId === "self";
644
- const instruction = isSelfQuery
645
- ? "summarize this session transcript fully and transparently. this is my own inner dialog — include all details, decisions, and reasoning."
646
- : `summarize this session transcript. the person asking has trust level: ${trustLevel}. family=full transparency, friend=share work and general topics but protect other people's identities, acquaintance=very guarded minimal disclosure.`;
647
- return await ctx.summarize(transcript, instruction);
1042
+ let friendId = args.friendId;
1043
+ const channel = args.channel;
1044
+ const key = args.key || "session";
1045
+ const count = parseInt(args.messageCount || "20", 10);
1046
+ const mode = args.mode || "transcript";
1047
+ // Resolve friend name → UUID if not already a UUID or "self"
1048
+ if (friendId && friendId !== "self" && !/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId) && ctx?.friendStore?.listAll) {
1049
+ const allFriends = await ctx.friendStore.listAll();
1050
+ const match = allFriends.find(f => f.name.toLowerCase() === friendId.toLowerCase());
1051
+ if (match) {
1052
+ friendId = match.id;
648
1053
  }
649
- return transcript;
650
1054
  }
651
- catch {
652
- return "no session found for that friend/channel/key combination.";
1055
+ if (mode === "status") {
1056
+ if (friendId !== "self" || channel !== "inner") {
1057
+ return "status mode is only available for self/inner dialog.";
1058
+ }
1059
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)((0, identity_1.getAgentRoot)());
1060
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
1061
+ return renderInnerProgressStatus((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
1062
+ }
1063
+ if (mode === "search") {
1064
+ const query = (args.query || "").trim();
1065
+ if (!query) {
1066
+ return "search mode requires a non-empty query.";
1067
+ }
1068
+ const search = await searchSessionSafely({
1069
+ sessionPath: (0, config_1.resolveSessionPath)(friendId, channel, key),
1070
+ friendId,
1071
+ channel,
1072
+ key,
1073
+ query,
1074
+ });
1075
+ if (search.kind === "missing") {
1076
+ return NO_SESSION_FOUND_MESSAGE;
1077
+ }
1078
+ if (search.kind === "empty") {
1079
+ return EMPTY_SESSION_MESSAGE;
1080
+ }
1081
+ if (search.kind === "no_match") {
1082
+ return `no matches for "${search.query}" in that session.\n\n${search.snapshot}`;
1083
+ }
1084
+ return [
1085
+ `history search: "${search.query}"`,
1086
+ search.snapshot,
1087
+ ...search.matches.map((match, index) => `match ${index + 1}\n${match}`),
1088
+ ].join("\n\n");
1089
+ }
1090
+ const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
1091
+ const recall = await recallSessionSafely({
1092
+ sessionPath: sessFile,
1093
+ friendId,
1094
+ channel,
1095
+ key,
1096
+ messageCount: count,
1097
+ trustLevel: ctx?.context?.friend?.trustLevel,
1098
+ summarize: ctx?.summarize,
1099
+ });
1100
+ if (recall.kind === "missing") {
1101
+ return NO_SESSION_FOUND_MESSAGE;
653
1102
  }
1103
+ if (recall.kind === "empty") {
1104
+ return EMPTY_SESSION_MESSAGE;
1105
+ }
1106
+ return recall.summary;
654
1107
  },
655
1108
  },
656
1109
  {
@@ -658,12 +1111,12 @@ exports.baseToolDefinitions = [
658
1111
  type: "function",
659
1112
  function: {
660
1113
  name: "send_message",
661
- description: "send a message to a friend's session. the message is queued as a pending file and delivered when the target session drains its queue.",
1114
+ description: "send a message to a friend's session. when the request is explicitly authorized from a trusted live chat, the harness will try to deliver immediately; otherwise it reports truthful queued/block/failure state.",
662
1115
  parameters: {
663
1116
  type: "object",
664
1117
  properties: {
665
1118
  friendId: { type: "string", description: "the friend UUID (or 'self')" },
666
- channel: { type: "string", description: "the channel: cli, teams, or inner" },
1119
+ channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
667
1120
  key: { type: "string", description: "session key (defaults to 'session')" },
668
1121
  content: { type: "string", description: "the message content to send" },
669
1122
  },
@@ -671,44 +1124,310 @@ exports.baseToolDefinitions = [
671
1124
  },
672
1125
  },
673
1126
  },
674
- handler: async (args) => {
1127
+ handler: async (args, ctx) => {
675
1128
  const friendId = args.friendId;
676
1129
  const channel = args.channel;
677
1130
  const key = args.key || "session";
678
1131
  const content = args.content;
679
1132
  const now = Date.now();
680
- const pendingDir = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "pending", friendId, channel, key);
681
- fs.mkdirSync(pendingDir, { recursive: true });
682
- const fileName = `${now}-${Math.random().toString(36).slice(2, 10)}.json`;
683
- const filePath = path.join(pendingDir, fileName);
1133
+ const agentName = (0, identity_1.getAgentName)();
1134
+ // Self-routing: messages to "self" always go to inner dialog pending dir,
1135
+ // regardless of the channel or key the agent specified.
1136
+ const isSelf = friendId === "self";
1137
+ const pendingDir = isSelf
1138
+ ? (0, pending_1.getInnerDialogPendingDir)(agentName)
1139
+ : (0, pending_1.getPendingDir)(agentName, friendId, channel, key);
1140
+ const delegatingBridgeId = findDelegatingBridgeId(ctx);
1141
+ const delegatedFrom = isSelf
1142
+ && ctx?.currentSession
1143
+ && !(ctx.currentSession.friendId === "self" && ctx.currentSession.channel === "inner")
1144
+ ? {
1145
+ friendId: ctx.currentSession.friendId,
1146
+ channel: ctx.currentSession.channel,
1147
+ key: ctx.currentSession.key,
1148
+ ...(delegatingBridgeId ? { bridgeId: delegatingBridgeId } : {}),
1149
+ }
1150
+ : undefined;
1151
+ const obligationId = delegatedFrom ? (0, obligations_1.generateObligationId)(now) : undefined;
684
1152
  const envelope = {
685
- from: (0, identity_1.getAgentName)(),
1153
+ from: agentName,
686
1154
  friendId,
687
1155
  channel,
688
1156
  key,
689
1157
  content,
690
1158
  timestamp: now,
1159
+ ...(delegatedFrom ? { delegatedFrom, obligationStatus: "pending" } : {}),
1160
+ ...(obligationId ? { obligationId } : {}),
691
1161
  };
692
- fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
693
- const preview = content.length > 80 ? content.slice(0, 80) + "…" : content;
694
- return `message queued for delivery to ${friendId} on ${channel}/${key}. preview: "${preview}". it will be delivered when their session is next active.`;
1162
+ if (isSelf) {
1163
+ writePendingEnvelope(pendingDir, envelope);
1164
+ if (delegatedFrom) {
1165
+ try {
1166
+ (0, obligations_2.createObligation)((0, identity_1.getAgentRoot)(), {
1167
+ origin: {
1168
+ friendId: delegatedFrom.friendId,
1169
+ channel: delegatedFrom.channel,
1170
+ key: delegatedFrom.key,
1171
+ },
1172
+ ...(delegatedFrom.bridgeId ? { bridgeId: delegatedFrom.bridgeId } : {}),
1173
+ content,
1174
+ });
1175
+ }
1176
+ catch {
1177
+ /* v8 ignore next -- defensive: obligation store write failure should not break send_message @preserve */
1178
+ }
1179
+ /* v8 ignore next -- obligationId always set when delegatedFrom is set (see generateObligationId above) @preserve */
1180
+ if (obligationId) {
1181
+ (0, obligations_1.createObligation)(agentName, {
1182
+ id: obligationId,
1183
+ origin: delegatedFrom,
1184
+ status: "queued",
1185
+ delegatedContent: content.length > 120 ? `${content.slice(0, 117)}...` : content,
1186
+ createdAt: now,
1187
+ });
1188
+ }
1189
+ (0, runtime_1.emitNervesEvent)({
1190
+ event: "repertoire.obligation_created",
1191
+ component: "repertoire",
1192
+ message: "obligation created for inner dialog delegation",
1193
+ meta: {
1194
+ friendId: delegatedFrom.friendId,
1195
+ channel: delegatedFrom.channel,
1196
+ key: delegatedFrom.key,
1197
+ },
1198
+ });
1199
+ }
1200
+ let wakeResponse = null;
1201
+ try {
1202
+ wakeResponse = await (0, socket_client_1.requestInnerWake)(agentName);
1203
+ }
1204
+ catch {
1205
+ wakeResponse = null;
1206
+ }
1207
+ if (!wakeResponse?.ok) {
1208
+ const { runInnerDialogTurn } = await Promise.resolve().then(() => __importStar(require("../senses/inner-dialog")));
1209
+ if (ctx?.context?.channel.channel === "inner") {
1210
+ queueMicrotask(() => {
1211
+ void runInnerDialogTurn({ reason: "instinct" });
1212
+ });
1213
+ return renderInnerProgressStatus({
1214
+ queue: "queued to inner/dialog",
1215
+ wake: "inline scheduled",
1216
+ processing: "pending",
1217
+ surfaced: "nothing yet",
1218
+ });
1219
+ }
1220
+ else {
1221
+ const turnResult = await runInnerDialogTurn({ reason: "instinct" });
1222
+ const surfacedPreview = normalizeProgressOutcome((0, thoughts_1.formatSurfacedValue)((0, thoughts_1.extractThoughtResponseFromMessages)(turnResult?.messages ?? [])));
1223
+ return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
1224
+ scope: "inner-delegation",
1225
+ phase: "completed",
1226
+ objective: "queued to inner/dialog",
1227
+ outcomeText: `wake: inline fallback\n${surfacedPreview}`,
1228
+ }));
1229
+ }
1230
+ }
1231
+ return renderInnerProgressStatus({
1232
+ queue: "queued to inner/dialog",
1233
+ wake: "daemon requested",
1234
+ processing: "pending",
1235
+ surfaced: "nothing yet",
1236
+ });
1237
+ }
1238
+ const deliveryResult = await (0, cross_chat_delivery_1.deliverCrossChatMessage)({
1239
+ friendId,
1240
+ channel,
1241
+ key,
1242
+ content,
1243
+ intent: ctx?.currentSession && ctx.currentSession.friendId !== "self"
1244
+ ? "explicit_cross_chat"
1245
+ : "generic_outreach",
1246
+ ...(ctx?.currentSession && ctx.currentSession.friendId !== "self"
1247
+ ? {
1248
+ authorizingSession: {
1249
+ friendId: ctx.currentSession.friendId,
1250
+ channel: ctx.currentSession.channel,
1251
+ key: ctx.currentSession.key,
1252
+ trustLevel: ctx?.context?.friend?.trustLevel,
1253
+ },
1254
+ }
1255
+ : {}),
1256
+ }, {
1257
+ agentName,
1258
+ queuePending: (message) => writePendingEnvelope(pendingDir, message),
1259
+ deliverers: {
1260
+ bluebubbles: async (request) => {
1261
+ const { sendProactiveBlueBubblesMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/bluebubbles")));
1262
+ const result = await sendProactiveBlueBubblesMessageToSession({
1263
+ friendId: request.friendId,
1264
+ sessionKey: request.key,
1265
+ text: request.content,
1266
+ intent: request.intent,
1267
+ authorizingSession: request.authorizingSession,
1268
+ });
1269
+ if (result.delivered) {
1270
+ return {
1271
+ status: "delivered_now",
1272
+ detail: "sent to the active bluebubbles chat now",
1273
+ };
1274
+ }
1275
+ if (result.reason === "missing_target") {
1276
+ return {
1277
+ status: "blocked",
1278
+ detail: "bluebubbles could not resolve a routable target for that session",
1279
+ };
1280
+ }
1281
+ if (result.reason === "send_error") {
1282
+ return {
1283
+ status: "failed",
1284
+ detail: "bluebubbles send failed",
1285
+ };
1286
+ }
1287
+ return {
1288
+ status: "unavailable",
1289
+ detail: "live delivery unavailable right now; queued for the next active turn",
1290
+ };
1291
+ },
1292
+ teams: async (request) => {
1293
+ if (!ctx?.botApi) {
1294
+ return {
1295
+ status: "unavailable",
1296
+ detail: "live delivery unavailable right now; queued for the next active turn",
1297
+ };
1298
+ }
1299
+ const { sendProactiveTeamsMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/teams")));
1300
+ const result = await sendProactiveTeamsMessageToSession({
1301
+ friendId: request.friendId,
1302
+ sessionKey: request.key,
1303
+ text: request.content,
1304
+ intent: request.intent,
1305
+ authorizingSession: request.authorizingSession,
1306
+ }, {
1307
+ botApi: ctx.botApi,
1308
+ });
1309
+ if (result.delivered) {
1310
+ return {
1311
+ status: "delivered_now",
1312
+ detail: "sent to the active teams chat now",
1313
+ };
1314
+ }
1315
+ if (result.reason === "missing_target") {
1316
+ return {
1317
+ status: "blocked",
1318
+ detail: "teams could not resolve a routable target for that session",
1319
+ };
1320
+ }
1321
+ if (result.reason === "send_error") {
1322
+ return {
1323
+ status: "failed",
1324
+ detail: "teams send failed",
1325
+ };
1326
+ }
1327
+ return {
1328
+ status: "unavailable",
1329
+ detail: "live delivery unavailable right now; queued for the next active turn",
1330
+ };
1331
+ },
1332
+ },
1333
+ });
1334
+ return renderCrossChatDeliveryStatus(`${friendId} on ${channel}/${key}`, deliveryResult);
695
1335
  },
696
1336
  },
1337
+ {
1338
+ tool: {
1339
+ type: "function",
1340
+ function: {
1341
+ name: "set_reasoning_effort",
1342
+ description: "adjust your own reasoning depth for subsequent turns. use higher effort for complex analysis, lower for simple tasks.",
1343
+ parameters: {
1344
+ type: "object",
1345
+ properties: {
1346
+ level: { type: "string", description: "the reasoning effort level to set" },
1347
+ },
1348
+ required: ["level"],
1349
+ },
1350
+ },
1351
+ },
1352
+ handler: (args, ctx) => {
1353
+ if (!ctx?.supportedReasoningEfforts || !ctx.setReasoningEffort) {
1354
+ return "reasoning effort adjustment is not available in this context.";
1355
+ }
1356
+ const level = (args.level || "").trim();
1357
+ if (!ctx.supportedReasoningEfforts.includes(level)) {
1358
+ return `invalid reasoning effort level "${level}". accepted levels: ${ctx.supportedReasoningEfforts.join(", ")}`;
1359
+ }
1360
+ ctx.setReasoningEffort(level);
1361
+ (0, runtime_1.emitNervesEvent)({
1362
+ component: "repertoire",
1363
+ event: "repertoire.reasoning_effort_changed",
1364
+ message: `reasoning effort set to ${level}`,
1365
+ meta: { level },
1366
+ });
1367
+ return `reasoning effort set to "${level}".`;
1368
+ },
1369
+ requiredCapability: "reasoning-effort",
1370
+ summaryKeys: ["level"],
1371
+ },
697
1372
  ...tools_1.codingToolDefinitions,
698
1373
  ];
699
- // Backward-compat: extract just the OpenAI tool schemas
700
1374
  exports.tools = exports.baseToolDefinitions.map((d) => d.tool);
701
- // Backward-compat: extract just the handlers by name
702
- exports.baseToolHandlers = Object.fromEntries(exports.baseToolDefinitions.map((d) => [d.tool.function.name, d.handler]));
703
- exports.finalAnswerTool = {
1375
+ exports.ponderTool = {
1376
+ type: "function",
1377
+ function: {
1378
+ name: "ponder",
1379
+ description: "i need to sit with this. from a conversation, takes the thread inward with a thought and a parting word. from inner dialog, keeps the wheel turning for another pass. must be the only tool call in the turn.",
1380
+ parameters: {
1381
+ type: "object",
1382
+ properties: {
1383
+ thought: {
1384
+ type: "string",
1385
+ description: "the question or thread that needs more thought — brief framing, not analysis. required from a conversation, ignored from inner dialog.",
1386
+ },
1387
+ say: {
1388
+ type: "string",
1389
+ description: "what you say before going quiet — speak to what caught your attention, not just that something did. required from a conversation, ignored from inner dialog.",
1390
+ },
1391
+ },
1392
+ },
1393
+ },
1394
+ };
1395
+ exports.observeTool = {
704
1396
  type: "function",
705
1397
  function: {
706
- name: "final_answer",
1398
+ name: "observe",
1399
+ description: "absorb what happened without responding — the moment doesn't call for words. must be the only tool call in the turn.",
1400
+ parameters: {
1401
+ type: "object",
1402
+ properties: {
1403
+ reason: { type: "string", description: "brief reason for staying silent (for logging)" },
1404
+ },
1405
+ },
1406
+ },
1407
+ };
1408
+ exports.settleTool = {
1409
+ type: "function",
1410
+ function: {
1411
+ name: "settle",
707
1412
  description: "respond to the user with your message. call this tool when you are ready to deliver your response.",
708
1413
  parameters: {
709
1414
  type: "object",
710
- properties: { answer: { type: "string" } },
1415
+ properties: {
1416
+ answer: { type: "string" },
1417
+ intent: { type: "string", enum: ["complete", "blocked", "direct_reply"] },
1418
+ },
711
1419
  required: ["answer"],
712
1420
  },
713
1421
  },
714
1422
  };
1423
+ exports.restTool = {
1424
+ type: "function",
1425
+ function: {
1426
+ name: "rest",
1427
+ description: "put this down for now — the wheel stops until the next heartbeat. must be the only tool call in the turn.",
1428
+ parameters: {
1429
+ type: "object",
1430
+ properties: {},
1431
+ },
1432
+ },
1433
+ };