@ouro.bot/cli 0.1.0-alpha.13 → 0.1.0-alpha.130

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 (126) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
  2. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  3. package/README.md +147 -205
  4. package/changelog.json +808 -0
  5. package/dist/heart/active-work.js +622 -0
  6. package/dist/heart/bridges/manager.js +358 -0
  7. package/dist/heart/bridges/state-machine.js +135 -0
  8. package/dist/heart/bridges/store.js +123 -0
  9. package/dist/heart/commitments.js +105 -0
  10. package/dist/heart/config.js +66 -21
  11. package/dist/heart/core.js +518 -100
  12. package/dist/heart/cross-chat-delivery.js +146 -0
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/auth-flow.js +432 -0
  15. package/dist/heart/daemon/daemon-cli.js +1516 -195
  16. package/dist/heart/daemon/daemon-entry.js +43 -2
  17. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  18. package/dist/heart/daemon/daemon.js +261 -1
  19. package/dist/heart/daemon/hatch-animation.js +10 -3
  20. package/dist/heart/daemon/hatch-flow.js +7 -72
  21. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  22. package/dist/heart/daemon/launchd.js +159 -0
  23. package/dist/heart/daemon/log-tailer.js +4 -3
  24. package/dist/heart/daemon/message-router.js +17 -8
  25. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  26. package/dist/heart/daemon/ouro-path-installer.js +57 -29
  27. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  28. package/dist/heart/daemon/process-manager.js +13 -0
  29. package/dist/heart/daemon/run-hooks.js +37 -0
  30. package/dist/heart/daemon/runtime-logging.js +58 -15
  31. package/dist/heart/daemon/runtime-metadata.js +219 -0
  32. package/dist/heart/daemon/runtime-mode.js +67 -0
  33. package/dist/heart/daemon/sense-manager.js +50 -2
  34. package/dist/heart/daemon/skill-management-installer.js +94 -0
  35. package/dist/heart/daemon/socket-client.js +202 -0
  36. package/dist/heart/daemon/specialist-orchestrator.js +2 -2
  37. package/dist/heart/daemon/specialist-prompt.js +7 -4
  38. package/dist/heart/daemon/specialist-tools.js +52 -3
  39. package/dist/heart/daemon/staged-restart.js +114 -0
  40. package/dist/heart/daemon/thoughts.js +507 -0
  41. package/dist/heart/daemon/update-checker.js +111 -0
  42. package/dist/heart/daemon/update-hooks.js +138 -0
  43. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  44. package/dist/heart/delegation.js +62 -0
  45. package/dist/heart/identity.js +64 -21
  46. package/dist/heart/kicks.js +1 -19
  47. package/dist/heart/model-capabilities.js +48 -0
  48. package/dist/heart/obligations.js +197 -0
  49. package/dist/heart/progress-story.js +42 -0
  50. package/dist/heart/provider-failover.js +88 -0
  51. package/dist/heart/provider-ping.js +159 -0
  52. package/dist/heart/providers/anthropic-token.js +163 -0
  53. package/dist/heart/providers/anthropic.js +195 -34
  54. package/dist/heart/providers/azure.js +115 -9
  55. package/dist/heart/providers/github-copilot.js +157 -0
  56. package/dist/heart/providers/minimax.js +33 -3
  57. package/dist/heart/providers/openai-codex.js +49 -14
  58. package/dist/heart/safe-workspace.js +381 -0
  59. package/dist/heart/session-activity.js +173 -0
  60. package/dist/heart/session-recall.js +216 -0
  61. package/dist/heart/streaming.js +108 -24
  62. package/dist/heart/target-resolution.js +123 -0
  63. package/dist/heart/tool-loop.js +194 -0
  64. package/dist/heart/turn-coordinator.js +28 -0
  65. package/dist/mind/associative-recall.js +14 -2
  66. package/dist/mind/bundle-manifest.js +12 -0
  67. package/dist/mind/context.js +60 -14
  68. package/dist/mind/first-impressions.js +16 -2
  69. package/dist/mind/friends/channel.js +35 -0
  70. package/dist/mind/friends/group-context.js +144 -0
  71. package/dist/mind/friends/store-file.js +19 -0
  72. package/dist/mind/friends/trust-explanation.js +74 -0
  73. package/dist/mind/friends/types.js +8 -0
  74. package/dist/mind/memory.js +27 -26
  75. package/dist/mind/obligation-steering.js +221 -0
  76. package/dist/mind/pending.js +76 -9
  77. package/dist/mind/phrases.js +1 -0
  78. package/dist/mind/prompt.js +456 -77
  79. package/dist/mind/token-estimate.js +8 -12
  80. package/dist/nerves/cli-logging.js +15 -2
  81. package/dist/nerves/coverage/run-artifacts.js +1 -1
  82. package/dist/nerves/index.js +12 -0
  83. package/dist/nerves/runtime.js +5 -1
  84. package/dist/repertoire/ado-client.js +4 -2
  85. package/dist/repertoire/coding/context-pack.js +254 -0
  86. package/dist/repertoire/coding/feedback.js +301 -0
  87. package/dist/repertoire/coding/index.js +4 -1
  88. package/dist/repertoire/coding/manager.js +210 -4
  89. package/dist/repertoire/coding/spawner.js +39 -9
  90. package/dist/repertoire/coding/tools.js +171 -4
  91. package/dist/repertoire/data/ado-endpoints.json +188 -0
  92. package/dist/repertoire/guardrails.js +290 -0
  93. package/dist/repertoire/mcp-client.js +254 -0
  94. package/dist/repertoire/mcp-manager.js +198 -0
  95. package/dist/repertoire/skills.js +3 -26
  96. package/dist/repertoire/tasks/board.js +12 -0
  97. package/dist/repertoire/tasks/index.js +23 -9
  98. package/dist/repertoire/tasks/transitions.js +1 -2
  99. package/dist/repertoire/tools-base.js +925 -250
  100. package/dist/repertoire/tools-bluebubbles.js +93 -0
  101. package/dist/repertoire/tools-teams.js +58 -25
  102. package/dist/repertoire/tools.js +106 -53
  103. package/dist/senses/bluebubbles-client.js +210 -5
  104. package/dist/senses/bluebubbles-entry.js +2 -0
  105. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  106. package/dist/senses/bluebubbles-media.js +339 -0
  107. package/dist/senses/bluebubbles-model.js +12 -4
  108. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  109. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  110. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  111. package/dist/senses/bluebubbles.js +915 -45
  112. package/dist/senses/cli-layout.js +187 -0
  113. package/dist/senses/cli.js +374 -131
  114. package/dist/senses/continuity.js +94 -0
  115. package/dist/senses/debug-activity.js +154 -0
  116. package/dist/senses/inner-dialog-worker.js +47 -18
  117. package/dist/senses/inner-dialog.js +388 -83
  118. package/dist/senses/pipeline.js +444 -0
  119. package/dist/senses/teams.js +607 -129
  120. package/dist/senses/trust-gate.js +112 -2
  121. package/package.json +9 -3
  122. package/subagents/README.md +4 -70
  123. package/dist/heart/daemon/subagent-installer.js +0 -134
  124. package/subagents/work-doer.md +0 -233
  125. package/subagents/work-merger.md +0 -624
  126. package/subagents/work-planner.md +0 -373
@@ -33,19 +33,298 @@ 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.finalAnswerTool = exports.noResponseTool = exports.goInwardTool = 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 safe_workspace_1 = require("../heart/safe-workspace");
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");
54
+ const coding_1 = require("./coding");
47
55
  const memory_1 = require("../mind/memory");
48
- const postIt = (msg) => `post-it from past you:\n${msg}`;
56
+ const tasks_1 = require("./tasks");
57
+ const pending_1 = require("../mind/pending");
58
+ const progress_story_1 = require("../heart/progress-story");
59
+ const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
60
+ const obligations_1 = require("../heart/obligations");
61
+ // Tracks which file paths have been read via read_file in this session.
62
+ // edit_file requires a file to be read first (must-read-first guard).
63
+ exports.editFileReadTracker = new Set();
64
+ function buildContextDiff(lines, changeStart, changeEnd, contextSize = 3) {
65
+ const start = Math.max(0, changeStart - contextSize);
66
+ const end = Math.min(lines.length, changeEnd + contextSize);
67
+ const result = [];
68
+ for (let i = start; i < end; i++) {
69
+ const lineNum = i + 1;
70
+ const prefix = (i >= changeStart && i < changeEnd) ? ">" : " ";
71
+ result.push(`${prefix} ${lineNum} | ${lines[i]}`);
72
+ }
73
+ return result.join("\n");
74
+ }
75
+ function resolveLocalToolPath(targetPath) {
76
+ return (0, safe_workspace_1.resolveSafeRepoPath)({ requestedPath: targetPath }).resolvedPath;
77
+ }
78
+ const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
79
+ const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
80
+ function findDelegatingBridgeId(ctx) {
81
+ const currentSession = ctx?.currentSession;
82
+ if (!currentSession)
83
+ return undefined;
84
+ return ctx?.activeBridges?.find((bridge) => bridge.lifecycle === "active"
85
+ && bridge.attachedSessions.some((session) => session.friendId === currentSession.friendId
86
+ && session.channel === currentSession.channel
87
+ && session.key === currentSession.key))?.id;
88
+ }
89
+ async function recallSessionSafely(options) {
90
+ try {
91
+ return await (0, session_recall_1.recallSession)(options);
92
+ }
93
+ catch (error) {
94
+ if (options.summarize) {
95
+ (0, runtime_1.emitNervesEvent)({
96
+ component: "daemon",
97
+ event: "daemon.session_recall_summary_fallback",
98
+ message: "session recall summarization failed; using raw transcript",
99
+ meta: {
100
+ friendId: options.friendId,
101
+ channel: options.channel,
102
+ key: options.key,
103
+ error: error instanceof Error ? error.message : String(error),
104
+ },
105
+ });
106
+ try {
107
+ return await (0, session_recall_1.recallSession)({
108
+ ...options,
109
+ summarize: undefined,
110
+ });
111
+ }
112
+ catch {
113
+ return { kind: "missing" };
114
+ }
115
+ }
116
+ return { kind: "missing" };
117
+ }
118
+ }
119
+ async function searchSessionSafely(options) {
120
+ try {
121
+ return await (0, session_recall_1.searchSessionTranscript)(options);
122
+ }
123
+ catch {
124
+ return { kind: "missing" };
125
+ }
126
+ }
127
+ function normalizeProgressOutcome(text) {
128
+ const trimmed = text.trim();
129
+ /* v8 ignore next -- defensive: normalizeProgressOutcome null branch @preserve */
130
+ if (!trimmed || trimmed === "nothing yet" || trimmed === "nothing recent") {
131
+ return null;
132
+ }
133
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) {
134
+ return trimmed.slice(1, -1);
135
+ }
136
+ return trimmed;
137
+ }
138
+ function writePendingEnvelope(queueDir, message) {
139
+ fs.mkdirSync(queueDir, { recursive: true });
140
+ const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
141
+ const filePath = path.join(queueDir, fileName);
142
+ fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
143
+ }
144
+ function renderCrossChatDeliveryStatus(target, result) {
145
+ const phase = result.status === "delivered_now"
146
+ ? "completed"
147
+ : result.status === "queued_for_later"
148
+ ? "queued"
149
+ : result.status === "blocked"
150
+ ? "blocked"
151
+ : "errored";
152
+ const lead = result.status === "delivered_now"
153
+ ? "delivered now"
154
+ : result.status === "queued_for_later"
155
+ ? "queued for later"
156
+ : result.status === "blocked"
157
+ ? "blocked"
158
+ : "failed";
159
+ return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
160
+ scope: "shared-work",
161
+ phase,
162
+ objective: `message to ${target}`,
163
+ outcomeText: `${lead}\n${result.detail}`,
164
+ }));
165
+ }
166
+ function emptyTaskBoard() {
167
+ return {
168
+ compact: "",
169
+ full: "",
170
+ byStatus: {
171
+ drafting: [],
172
+ processing: [],
173
+ validating: [],
174
+ collaborating: [],
175
+ paused: [],
176
+ blocked: [],
177
+ done: [],
178
+ },
179
+ actionRequired: [],
180
+ unresolvedDependencies: [],
181
+ activeSessions: [],
182
+ activeBridges: [],
183
+ };
184
+ }
185
+ function isLiveCodingSessionStatus(status) {
186
+ return status === "spawning"
187
+ || status === "running"
188
+ || status === "waiting_input"
189
+ || status === "stalled";
190
+ }
191
+ function readActiveWorkInnerState() {
192
+ const defaultJob = {
193
+ status: "idle",
194
+ content: null,
195
+ origin: null,
196
+ mode: "reflect",
197
+ obligationStatus: null,
198
+ surfacedResult: null,
199
+ queuedAt: null,
200
+ startedAt: null,
201
+ surfacedAt: null,
202
+ };
203
+ try {
204
+ const agentRoot = (0, identity_1.getAgentRoot)();
205
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
206
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
207
+ const { pendingMessages, turns, runtimeState } = (0, thoughts_1.readInnerDialogRawData)(sessionPath, pendingDir);
208
+ const dialogStatus = (0, thoughts_1.deriveInnerDialogStatus)(pendingMessages, turns, runtimeState);
209
+ const job = (0, thoughts_1.deriveInnerJob)(pendingMessages, turns, runtimeState);
210
+ const storeObligationPending = (0, obligations_1.readPendingObligations)(agentRoot).length > 0;
211
+ return {
212
+ status: dialogStatus.processing === "started" ? "running" : "idle",
213
+ hasPending: dialogStatus.queue !== "clear",
214
+ origin: dialogStatus.origin,
215
+ contentSnippet: dialogStatus.contentSnippet,
216
+ obligationPending: dialogStatus.obligationPending || storeObligationPending,
217
+ job,
218
+ };
219
+ }
220
+ catch {
221
+ return {
222
+ status: "idle",
223
+ hasPending: false,
224
+ job: defaultJob,
225
+ };
226
+ }
227
+ }
228
+ async function buildToolActiveWorkFrame(ctx) {
229
+ const currentSession = ctx?.currentSession
230
+ ? {
231
+ friendId: ctx.currentSession.friendId,
232
+ channel: ctx.currentSession.channel,
233
+ key: ctx.currentSession.key,
234
+ sessionPath: (0, config_1.resolveSessionPath)(ctx.currentSession.friendId, ctx.currentSession.channel, ctx.currentSession.key),
235
+ }
236
+ : null;
237
+ const agentRoot = (0, identity_1.getAgentRoot)();
238
+ const bridges = currentSession
239
+ ? (0, manager_1.createBridgeManager)().findBridgesForSession({
240
+ friendId: currentSession.friendId,
241
+ channel: currentSession.channel,
242
+ key: currentSession.key,
243
+ })
244
+ : [];
245
+ let friendActivity = [];
246
+ try {
247
+ friendActivity = (0, session_activity_1.listSessionActivity)({
248
+ sessionsDir: `${agentRoot}/state/sessions`,
249
+ friendsDir: `${agentRoot}/friends`,
250
+ agentName: (0, identity_1.getAgentName)(),
251
+ currentSession,
252
+ });
253
+ }
254
+ catch {
255
+ friendActivity = [];
256
+ }
257
+ const pendingObligations = (() => {
258
+ try {
259
+ return (0, obligations_1.readPendingObligations)(agentRoot);
260
+ }
261
+ catch {
262
+ return [];
263
+ }
264
+ })();
265
+ let codingSessions = [];
266
+ let otherCodingSessions = [];
267
+ try {
268
+ const liveCodingSessions = (0, coding_1.getCodingSessionManager)()
269
+ .listSessions()
270
+ .filter((session) => isLiveCodingSessionStatus(session.status) && Boolean(session.originSession));
271
+ if (currentSession) {
272
+ codingSessions = liveCodingSessions.filter((session) => session.originSession?.friendId === currentSession.friendId
273
+ && session.originSession.channel === currentSession.channel
274
+ && session.originSession.key === currentSession.key);
275
+ otherCodingSessions = liveCodingSessions.filter((session) => !(session.originSession?.friendId === currentSession.friendId
276
+ && session.originSession.channel === currentSession.channel
277
+ && session.originSession.key === currentSession.key));
278
+ }
279
+ else {
280
+ codingSessions = [];
281
+ otherCodingSessions = liveCodingSessions;
282
+ }
283
+ }
284
+ catch {
285
+ codingSessions = [];
286
+ otherCodingSessions = [];
287
+ }
288
+ const currentObligation = currentSession
289
+ ? pendingObligations.find((obligation) => obligation.status !== "fulfilled"
290
+ && obligation.origin.friendId === currentSession.friendId
291
+ && obligation.origin.channel === currentSession.channel
292
+ && obligation.origin.key === currentSession.key)?.content ?? null
293
+ : null;
294
+ return (0, active_work_1.buildActiveWorkFrame)({
295
+ currentSession,
296
+ currentObligation,
297
+ mustResolveBeforeHandoff: false,
298
+ inner: readActiveWorkInnerState(),
299
+ bridges,
300
+ codingSessions,
301
+ otherCodingSessions,
302
+ pendingObligations,
303
+ taskBoard: (() => {
304
+ try {
305
+ return (0, tasks_1.getTaskModule)().getBoard();
306
+ }
307
+ catch {
308
+ return emptyTaskBoard();
309
+ }
310
+ })(),
311
+ friendActivity,
312
+ targetCandidates: [],
313
+ });
314
+ }
315
+ function renderInnerProgressStatus(status) {
316
+ if (status.processing === "pending") {
317
+ return "i've queued this thought for private attention. it'll come up when my inner dialog is free.";
318
+ }
319
+ if (status.processing === "started") {
320
+ return "i'm working through this privately right now.";
321
+ }
322
+ // processed / completed
323
+ if (status.surfaced && status.surfaced !== "nothing recent" && status.surfaced !== "no outward result") {
324
+ return `i thought about this privately and came to something: ${status.surfaced}`;
325
+ }
326
+ return "i thought about this privately. i'll bring it back when the time is right.";
327
+ }
49
328
  exports.baseToolDefinitions = [
50
329
  {
51
330
  tool: {
@@ -55,12 +334,28 @@ exports.baseToolDefinitions = [
55
334
  description: "read file contents",
56
335
  parameters: {
57
336
  type: "object",
58
- properties: { path: { type: "string" } },
337
+ properties: {
338
+ path: { type: "string" },
339
+ offset: { type: "number", description: "1-based line number to start reading from" },
340
+ limit: { type: "number", description: "maximum number of lines to return" },
341
+ },
59
342
  required: ["path"],
60
343
  },
61
344
  },
62
345
  },
63
- handler: (a) => fs.readFileSync(a.path, "utf-8"),
346
+ handler: (a) => {
347
+ const resolvedPath = resolveLocalToolPath(a.path);
348
+ const content = fs.readFileSync(resolvedPath, "utf-8");
349
+ exports.editFileReadTracker.add(resolvedPath);
350
+ const offset = a.offset ? parseInt(a.offset, 10) : undefined;
351
+ const limit = a.limit ? parseInt(a.limit, 10) : undefined;
352
+ if (offset === undefined && limit === undefined)
353
+ return content;
354
+ const lines = content.split("\n");
355
+ const start = offset ? offset - 1 : 0;
356
+ const end = limit !== undefined ? start + limit : lines.length;
357
+ return lines.slice(start, end).join("\n");
358
+ },
64
359
  },
65
360
  {
66
361
  tool: {
@@ -75,102 +370,244 @@ exports.baseToolDefinitions = [
75
370
  },
76
371
  },
77
372
  },
78
- handler: (a) => (fs.writeFileSync(a.path, a.content, "utf-8"), "ok"),
373
+ handler: (a) => {
374
+ const resolvedPath = resolveLocalToolPath(a.path);
375
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
376
+ fs.writeFileSync(resolvedPath, a.content, "utf-8");
377
+ return "ok";
378
+ },
79
379
  },
80
380
  {
81
381
  tool: {
82
382
  type: "function",
83
383
  function: {
84
- name: "shell",
85
- description: "run shell command",
384
+ name: "edit_file",
385
+ 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
386
  parameters: {
87
387
  type: "object",
88
- properties: { command: { type: "string" } },
89
- required: ["command"],
388
+ properties: {
389
+ path: { type: "string" },
390
+ old_string: { type: "string" },
391
+ new_string: { type: "string" },
392
+ },
393
+ required: ["path", "old_string", "new_string"],
90
394
  },
91
395
  },
92
396
  },
93
- handler: (a) => (0, child_process_1.execSync)(a.command, { encoding: "utf-8", timeout: 30000 }),
397
+ handler: (a) => {
398
+ const resolvedPath = resolveLocalToolPath(a.path);
399
+ if (!exports.editFileReadTracker.has(resolvedPath)) {
400
+ return `error: you must read the file with read_file before editing it. call read_file on ${a.path} first.`;
401
+ }
402
+ let content;
403
+ try {
404
+ content = fs.readFileSync(resolvedPath, "utf-8");
405
+ }
406
+ catch (e) {
407
+ return `error: could not read file: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
408
+ }
409
+ // Count occurrences
410
+ const occurrences = [];
411
+ let searchFrom = 0;
412
+ while (true) {
413
+ const idx = content.indexOf(a.old_string, searchFrom);
414
+ if (idx === -1)
415
+ break;
416
+ occurrences.push(idx);
417
+ searchFrom = idx + 1;
418
+ }
419
+ if (occurrences.length === 0) {
420
+ return `error: old_string not found in ${a.path}`;
421
+ }
422
+ if (occurrences.length > 1) {
423
+ return `error: old_string is ambiguous -- found ${occurrences.length} matches in ${a.path}. provide more context to make the match unique.`;
424
+ }
425
+ // Single unique match -- replace
426
+ const idx = occurrences[0];
427
+ const updated = content.slice(0, idx) + a.new_string + content.slice(idx + a.old_string.length);
428
+ fs.writeFileSync(resolvedPath, updated, "utf-8");
429
+ // Build contextual diff
430
+ const lines = updated.split("\n");
431
+ const prefixLines = content.slice(0, idx).split("\n");
432
+ const changeStartLine = prefixLines.length - 1;
433
+ const newStringLines = a.new_string.split("\n");
434
+ const changeEndLine = changeStartLine + newStringLines.length;
435
+ return buildContextDiff(lines, changeStartLine, changeEndLine);
436
+ },
94
437
  },
95
438
  {
96
439
  tool: {
97
440
  type: "function",
98
441
  function: {
99
- name: "list_directory",
100
- description: "list directory contents",
442
+ name: "glob",
443
+ description: "find files matching a glob pattern. returns matching paths sorted alphabetically, one per line.",
101
444
  parameters: {
102
445
  type: "object",
103
- properties: { path: { type: "string" } },
104
- required: ["path"],
446
+ properties: {
447
+ pattern: { type: "string", description: "glob pattern (e.g. **/*.ts)" },
448
+ cwd: { type: "string", description: "directory to search from (defaults to process.cwd())" },
449
+ },
450
+ required: ["pattern"],
105
451
  },
106
452
  },
107
453
  },
108
- handler: (a) => fs
109
- .readdirSync(a.path, { withFileTypes: true })
110
- .map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
111
- .join("\n"),
454
+ handler: (a) => {
455
+ const cwd = a.cwd ? resolveLocalToolPath(a.cwd) : process.cwd();
456
+ const matches = fg.globSync(a.pattern, { cwd, dot: true });
457
+ return matches.sort().join("\n");
458
+ },
112
459
  },
113
460
  {
114
461
  tool: {
115
462
  type: "function",
116
463
  function: {
117
- name: "git_commit",
118
- description: "commit changes to git with explicit paths",
464
+ name: "grep",
465
+ 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
466
  parameters: {
120
467
  type: "object",
121
468
  properties: {
122
- message: { type: "string" },
123
- paths: { type: "array", items: { type: "string" } },
469
+ pattern: { type: "string", description: "regex pattern to search for" },
470
+ path: { type: "string", description: "file or directory to search" },
471
+ context_lines: { type: "number", description: "number of surrounding context lines (default 0)" },
472
+ include: { type: "string", description: "glob filter to limit searched files (e.g. *.ts)" },
124
473
  },
125
- required: ["message", "paths"],
474
+ required: ["pattern", "path"],
126
475
  },
127
476
  },
128
477
  },
129
478
  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.");
479
+ const targetPath = resolveLocalToolPath(a.path);
480
+ const regex = new RegExp(a.pattern);
481
+ const contextLines = parseInt(a.context_lines || "0", 10);
482
+ const includeGlob = a.include || undefined;
483
+ function searchFile(filePath) {
484
+ let content;
485
+ try {
486
+ content = fs.readFileSync(filePath, "utf-8");
487
+ }
488
+ catch {
489
+ return [];
133
490
  }
134
- for (const p of a.paths) {
135
- if (!fs.existsSync(p)) {
136
- return postIt(`path does not exist: ${p}`);
491
+ const lines = content.split("\n");
492
+ const matchIndices = new Set();
493
+ for (let i = 0; i < lines.length; i++) {
494
+ if (regex.test(lines[i])) {
495
+ matchIndices.add(i);
137
496
  }
138
- (0, child_process_1.execSync)(`git add ${p}`, { encoding: "utf-8" });
139
497
  }
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.");
498
+ if (matchIndices.size === 0)
499
+ return [];
500
+ const outputIndices = new Set();
501
+ for (const idx of matchIndices) {
502
+ const start = Math.max(0, idx - contextLines);
503
+ const end = Math.min(lines.length - 1, idx + contextLines);
504
+ for (let i = start; i <= end; i++) {
505
+ outputIndices.add(i);
506
+ }
143
507
  }
144
- (0, child_process_1.execSync)(`git commit -m \"${a.message}\"`, { encoding: "utf-8" });
145
- return `${diff}\ncommitted`;
508
+ const sortedIndices = [...outputIndices].sort((a, b) => a - b);
509
+ const results = [];
510
+ for (const idx of sortedIndices) {
511
+ const lineNum = idx + 1;
512
+ if (matchIndices.has(idx)) {
513
+ results.push(`${filePath}:${lineNum}: ${lines[idx]}`);
514
+ }
515
+ else {
516
+ results.push(`-${filePath}:${lineNum}: ${lines[idx]}`);
517
+ }
518
+ }
519
+ return results;
146
520
  }
147
- catch (e) {
148
- return `failed: ${e}`;
521
+ function collectFiles(dirPath) {
522
+ const files = [];
523
+ function walk(dir) {
524
+ let entries;
525
+ try {
526
+ entries = fs.readdirSync(dir, { withFileTypes: true });
527
+ }
528
+ catch {
529
+ return;
530
+ }
531
+ for (const entry of entries) {
532
+ const fullPath = path.join(dir, entry.name);
533
+ if (entry.isDirectory()) {
534
+ walk(fullPath);
535
+ }
536
+ else if (entry.isFile()) {
537
+ files.push(fullPath);
538
+ }
539
+ }
540
+ }
541
+ walk(dirPath);
542
+ return files.sort();
543
+ }
544
+ function matchesGlob(filePath, glob) {
545
+ const escaped = glob
546
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
547
+ .replace(/\*/g, ".*")
548
+ .replace(/\?/g, ".");
549
+ return new RegExp(`(^|/)${escaped}$`).test(filePath);
149
550
  }
551
+ const stat = fs.statSync(targetPath, { throwIfNoEntry: false });
552
+ if (!stat)
553
+ return "";
554
+ if (stat.isFile()) {
555
+ return searchFile(targetPath).join("\n");
556
+ }
557
+ let files = collectFiles(targetPath);
558
+ if (includeGlob) {
559
+ files = files.filter((f) => matchesGlob(f, includeGlob));
560
+ }
561
+ const allResults = [];
562
+ for (const file of files) {
563
+ allResults.push(...searchFile(file));
564
+ }
565
+ return allResults.join("\n");
150
566
  },
151
567
  },
152
568
  {
153
569
  tool: {
154
570
  type: "function",
155
571
  function: {
156
- name: "gh_cli",
157
- description: "execute a GitHub CLI (gh) command. use carefully.",
572
+ name: "safe_workspace",
573
+ description: "acquire or inspect the safe harness repo workspace for local edits. returns the real workspace path, branch, and why it was chosen.",
158
574
  parameters: {
159
575
  type: "object",
160
- properties: {
161
- command: { type: "string" },
162
- },
576
+ properties: {},
577
+ },
578
+ },
579
+ },
580
+ handler: () => {
581
+ const selection = (0, safe_workspace_1.ensureSafeRepoWorkspace)();
582
+ return [
583
+ `workspace: ${selection.workspaceRoot}`,
584
+ `branch: ${selection.workspaceBranch}`,
585
+ `runtime: ${selection.runtimeKind}`,
586
+ `cleanup_after_merge: ${selection.cleanupAfterMerge ? "yes" : "no"}`,
587
+ `note: ${selection.note}`,
588
+ ].join("\n");
589
+ },
590
+ },
591
+ {
592
+ tool: {
593
+ type: "function",
594
+ function: {
595
+ name: "shell",
596
+ description: "run shell command",
597
+ parameters: {
598
+ type: "object",
599
+ properties: { command: { type: "string" } },
163
600
  required: ["command"],
164
601
  },
165
602
  },
166
603
  },
167
604
  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
- }
605
+ const prepared = (0, safe_workspace_1.resolveSafeShellExecution)(a.command);
606
+ return (0, child_process_1.execSync)(prepared.command, {
607
+ encoding: "utf-8",
608
+ timeout: 30000,
609
+ ...(prepared.cwd ? { cwd: prepared.cwd } : {}),
610
+ });
174
611
  },
175
612
  },
176
613
  {
@@ -206,20 +643,6 @@ exports.baseToolDefinitions = [
206
643
  }
207
644
  },
208
645
  },
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
- }),
222
- },
223
646
  {
224
647
  tool: {
225
648
  type: "function",
@@ -235,7 +658,7 @@ exports.baseToolDefinitions = [
235
658
  },
236
659
  handler: (a) => {
237
660
  try {
238
- const result = (0, child_process_1.spawnSync)("claude", ["-p", "--dangerously-skip-permissions", "--add-dir", "."], {
661
+ const result = (0, child_process_1.spawnSync)("claude", ["-p", "--no-session-persistence", "--dangerously-skip-permissions", "--add-dir", "."], {
239
662
  input: a.prompt,
240
663
  encoding: "utf-8",
241
664
  timeout: 60000,
@@ -375,151 +798,6 @@ exports.baseToolDefinitions = [
375
798
  return JSON.stringify(friend, null, 2);
376
799
  },
377
800
  },
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
- },
522
- },
523
801
  {
524
802
  tool: {
525
803
  type: "function",
@@ -604,12 +882,126 @@ exports.baseToolDefinitions = [
604
882
  },
605
883
  },
606
884
  // -- cross-session awareness --
885
+ {
886
+ tool: {
887
+ type: "function",
888
+ function: {
889
+ name: "bridge_manage",
890
+ description: "create and manage shared live-work bridges across already-active sessions.",
891
+ parameters: {
892
+ type: "object",
893
+ properties: {
894
+ action: {
895
+ type: "string",
896
+ enum: ["begin", "attach", "status", "promote_task", "complete", "cancel"],
897
+ },
898
+ bridgeId: { type: "string", description: "bridge id for all actions except begin" },
899
+ objective: { type: "string", description: "objective for begin" },
900
+ summary: { type: "string", description: "optional concise shared-work summary" },
901
+ friendId: { type: "string", description: "target friend id for attach" },
902
+ channel: { type: "string", description: "target channel for attach" },
903
+ key: { type: "string", description: "target session key for attach (defaults to 'session')" },
904
+ title: { type: "string", description: "task title override for promote_task" },
905
+ category: { type: "string", description: "task category override for promote_task" },
906
+ body: { type: "string", description: "task body override for promote_task" },
907
+ },
908
+ required: ["action"],
909
+ },
910
+ },
911
+ },
912
+ handler: async (args, ctx) => {
913
+ const manager = (0, manager_1.createBridgeManager)();
914
+ const action = (args.action || "").trim();
915
+ if (action === "begin") {
916
+ if (!ctx?.currentSession) {
917
+ return "bridge_manage begin requires an active session context.";
918
+ }
919
+ const objective = (args.objective || "").trim();
920
+ if (!objective)
921
+ return "objective is required for bridge begin.";
922
+ return (0, manager_1.formatBridgeStatus)(manager.beginBridge({
923
+ objective,
924
+ summary: (args.summary || objective).trim(),
925
+ session: ctx.currentSession,
926
+ }));
927
+ }
928
+ const bridgeId = (args.bridgeId || "").trim();
929
+ if (!bridgeId) {
930
+ return "bridgeId is required for this bridge action.";
931
+ }
932
+ if (action === "attach") {
933
+ const friendId = (args.friendId || "").trim();
934
+ const channel = (args.channel || "").trim();
935
+ const key = (args.key || "session").trim();
936
+ if (!friendId || !channel) {
937
+ return "friendId and channel are required for bridge attach.";
938
+ }
939
+ const sessionPath = (0, config_1.resolveSessionPath)(friendId, channel, key);
940
+ const recall = await recallSessionSafely({
941
+ sessionPath,
942
+ friendId,
943
+ channel,
944
+ key,
945
+ messageCount: 20,
946
+ trustLevel: ctx?.context?.friend?.trustLevel,
947
+ summarize: ctx?.summarize,
948
+ });
949
+ if (recall.kind === "missing") {
950
+ return NO_SESSION_FOUND_MESSAGE;
951
+ }
952
+ return (0, manager_1.formatBridgeStatus)(manager.attachSession(bridgeId, {
953
+ friendId,
954
+ channel,
955
+ key,
956
+ sessionPath,
957
+ snapshot: recall.kind === "ok" ? recall.snapshot : EMPTY_SESSION_MESSAGE,
958
+ }));
959
+ }
960
+ if (action === "status") {
961
+ const bridge = manager.getBridge(bridgeId);
962
+ if (!bridge)
963
+ return `bridge not found: ${bridgeId}`;
964
+ return (0, manager_1.formatBridgeStatus)(bridge);
965
+ }
966
+ if (action === "promote_task") {
967
+ return (0, manager_1.formatBridgeStatus)(manager.promoteBridgeToTask(bridgeId, {
968
+ title: args.title,
969
+ category: args.category,
970
+ body: args.body,
971
+ }));
972
+ }
973
+ if (action === "complete") {
974
+ return (0, manager_1.formatBridgeStatus)(manager.completeBridge(bridgeId));
975
+ }
976
+ if (action === "cancel") {
977
+ return (0, manager_1.formatBridgeStatus)(manager.cancelBridge(bridgeId));
978
+ }
979
+ return `unknown bridge action: ${action}`;
980
+ },
981
+ },
982
+ {
983
+ tool: {
984
+ type: "function",
985
+ function: {
986
+ name: "query_active_work",
987
+ 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.",
988
+ parameters: {
989
+ type: "object",
990
+ properties: {},
991
+ },
992
+ },
993
+ },
994
+ handler: async (_args, ctx) => {
995
+ const frame = await buildToolActiveWorkFrame(ctx);
996
+ 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)}`;
997
+ },
998
+ },
607
999
  {
608
1000
  tool: {
609
1001
  type: "function",
610
1002
  function: {
611
1003
  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.",
1004
+ description: "inspect another session. use transcript for recent context, status for self/inner progress, or search to find older history by query.",
613
1005
  parameters: {
614
1006
  type: "object",
615
1007
  properties: {
@@ -617,40 +1009,75 @@ exports.baseToolDefinitions = [
617
1009
  channel: { type: "string", description: "the channel: cli, teams, or inner" },
618
1010
  key: { type: "string", description: "session key (defaults to 'session')" },
619
1011
  messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
1012
+ mode: {
1013
+ type: "string",
1014
+ enum: ["transcript", "status", "search"],
1015
+ description: "transcript (default), lightweight status for self/inner checks, or search for older history",
1016
+ },
1017
+ query: { type: "string", description: "required when mode=search; search term for older session history" },
620
1018
  },
621
1019
  required: ["friendId", "channel"],
622
1020
  },
623
1021
  },
624
1022
  },
625
1023
  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);
1024
+ const friendId = args.friendId;
1025
+ const channel = args.channel;
1026
+ const key = args.key || "session";
1027
+ const count = parseInt(args.messageCount || "20", 10);
1028
+ const mode = args.mode || "transcript";
1029
+ if (mode === "status") {
1030
+ if (friendId !== "self" || channel !== "inner") {
1031
+ return "status mode is only available for self/inner dialog.";
648
1032
  }
649
- return transcript;
1033
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)((0, identity_1.getAgentRoot)());
1034
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
1035
+ return renderInnerProgressStatus((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
650
1036
  }
651
- catch {
652
- return "no session found for that friend/channel/key combination.";
1037
+ if (mode === "search") {
1038
+ const query = (args.query || "").trim();
1039
+ if (!query) {
1040
+ return "search mode requires a non-empty query.";
1041
+ }
1042
+ const search = await searchSessionSafely({
1043
+ sessionPath: (0, config_1.resolveSessionPath)(friendId, channel, key),
1044
+ friendId,
1045
+ channel,
1046
+ key,
1047
+ query,
1048
+ });
1049
+ if (search.kind === "missing") {
1050
+ return NO_SESSION_FOUND_MESSAGE;
1051
+ }
1052
+ if (search.kind === "empty") {
1053
+ return EMPTY_SESSION_MESSAGE;
1054
+ }
1055
+ if (search.kind === "no_match") {
1056
+ return `no matches for "${search.query}" in that session.\n\n${search.snapshot}`;
1057
+ }
1058
+ return [
1059
+ `history search: "${search.query}"`,
1060
+ search.snapshot,
1061
+ ...search.matches.map((match, index) => `match ${index + 1}\n${match}`),
1062
+ ].join("\n\n");
1063
+ }
1064
+ const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
1065
+ const recall = await recallSessionSafely({
1066
+ sessionPath: sessFile,
1067
+ friendId,
1068
+ channel,
1069
+ key,
1070
+ messageCount: count,
1071
+ trustLevel: ctx?.context?.friend?.trustLevel,
1072
+ summarize: ctx?.summarize,
1073
+ });
1074
+ if (recall.kind === "missing") {
1075
+ return NO_SESSION_FOUND_MESSAGE;
1076
+ }
1077
+ if (recall.kind === "empty") {
1078
+ return EMPTY_SESSION_MESSAGE;
653
1079
  }
1080
+ return recall.summary;
654
1081
  },
655
1082
  },
656
1083
  {
@@ -658,7 +1085,7 @@ exports.baseToolDefinitions = [
658
1085
  type: "function",
659
1086
  function: {
660
1087
  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.",
1088
+ 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
1089
  parameters: {
663
1090
  type: "object",
664
1091
  properties: {
@@ -671,35 +1098,280 @@ exports.baseToolDefinitions = [
671
1098
  },
672
1099
  },
673
1100
  },
674
- handler: async (args) => {
1101
+ handler: async (args, ctx) => {
675
1102
  const friendId = args.friendId;
676
1103
  const channel = args.channel;
677
1104
  const key = args.key || "session";
678
1105
  const content = args.content;
679
1106
  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);
1107
+ const agentName = (0, identity_1.getAgentName)();
1108
+ // Self-routing: messages to "self" always go to inner dialog pending dir,
1109
+ // regardless of the channel or key the agent specified.
1110
+ const isSelf = friendId === "self";
1111
+ const pendingDir = isSelf
1112
+ ? (0, pending_1.getInnerDialogPendingDir)(agentName)
1113
+ : (0, pending_1.getPendingDir)(agentName, friendId, channel, key);
1114
+ const delegatingBridgeId = findDelegatingBridgeId(ctx);
1115
+ const delegatedFrom = isSelf
1116
+ && ctx?.currentSession
1117
+ && !(ctx.currentSession.friendId === "self" && ctx.currentSession.channel === "inner")
1118
+ ? {
1119
+ friendId: ctx.currentSession.friendId,
1120
+ channel: ctx.currentSession.channel,
1121
+ key: ctx.currentSession.key,
1122
+ ...(delegatingBridgeId ? { bridgeId: delegatingBridgeId } : {}),
1123
+ }
1124
+ : undefined;
684
1125
  const envelope = {
685
- from: (0, identity_1.getAgentName)(),
1126
+ from: agentName,
686
1127
  friendId,
687
1128
  channel,
688
1129
  key,
689
1130
  content,
690
1131
  timestamp: now,
1132
+ ...(delegatedFrom ? { delegatedFrom, obligationStatus: "pending" } : {}),
691
1133
  };
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.`;
1134
+ if (isSelf) {
1135
+ writePendingEnvelope(pendingDir, envelope);
1136
+ if (delegatedFrom) {
1137
+ try {
1138
+ (0, obligations_1.createObligation)((0, identity_1.getAgentRoot)(), {
1139
+ origin: {
1140
+ friendId: delegatedFrom.friendId,
1141
+ channel: delegatedFrom.channel,
1142
+ key: delegatedFrom.key,
1143
+ },
1144
+ ...(delegatedFrom.bridgeId ? { bridgeId: delegatedFrom.bridgeId } : {}),
1145
+ content,
1146
+ });
1147
+ }
1148
+ catch {
1149
+ /* v8 ignore next -- defensive: obligation store write failure should not break send_message @preserve */
1150
+ }
1151
+ (0, runtime_1.emitNervesEvent)({
1152
+ event: "repertoire.obligation_created",
1153
+ component: "repertoire",
1154
+ message: "obligation created for inner dialog delegation",
1155
+ meta: {
1156
+ friendId: delegatedFrom.friendId,
1157
+ channel: delegatedFrom.channel,
1158
+ key: delegatedFrom.key,
1159
+ },
1160
+ });
1161
+ }
1162
+ let wakeResponse = null;
1163
+ try {
1164
+ wakeResponse = await (0, socket_client_1.requestInnerWake)(agentName);
1165
+ }
1166
+ catch {
1167
+ wakeResponse = null;
1168
+ }
1169
+ if (!wakeResponse?.ok) {
1170
+ const { runInnerDialogTurn } = await Promise.resolve().then(() => __importStar(require("../senses/inner-dialog")));
1171
+ if (ctx?.context?.channel.channel === "inner") {
1172
+ queueMicrotask(() => {
1173
+ void runInnerDialogTurn({ reason: "instinct" });
1174
+ });
1175
+ return renderInnerProgressStatus({
1176
+ queue: "queued to inner/dialog",
1177
+ wake: "inline scheduled",
1178
+ processing: "pending",
1179
+ surfaced: "nothing yet",
1180
+ });
1181
+ }
1182
+ else {
1183
+ const turnResult = await runInnerDialogTurn({ reason: "instinct" });
1184
+ const surfacedPreview = normalizeProgressOutcome((0, thoughts_1.formatSurfacedValue)((0, thoughts_1.extractThoughtResponseFromMessages)(turnResult?.messages ?? [])));
1185
+ return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
1186
+ scope: "inner-delegation",
1187
+ phase: "completed",
1188
+ objective: "queued to inner/dialog",
1189
+ outcomeText: `wake: inline fallback\n${surfacedPreview}`,
1190
+ }));
1191
+ }
1192
+ }
1193
+ return renderInnerProgressStatus({
1194
+ queue: "queued to inner/dialog",
1195
+ wake: "daemon requested",
1196
+ processing: "pending",
1197
+ surfaced: "nothing yet",
1198
+ });
1199
+ }
1200
+ const deliveryResult = await (0, cross_chat_delivery_1.deliverCrossChatMessage)({
1201
+ friendId,
1202
+ channel,
1203
+ key,
1204
+ content,
1205
+ intent: ctx?.currentSession && ctx.currentSession.friendId !== "self"
1206
+ ? "explicit_cross_chat"
1207
+ : "generic_outreach",
1208
+ ...(ctx?.currentSession && ctx.currentSession.friendId !== "self"
1209
+ ? {
1210
+ authorizingSession: {
1211
+ friendId: ctx.currentSession.friendId,
1212
+ channel: ctx.currentSession.channel,
1213
+ key: ctx.currentSession.key,
1214
+ trustLevel: ctx?.context?.friend?.trustLevel,
1215
+ },
1216
+ }
1217
+ : {}),
1218
+ }, {
1219
+ agentName,
1220
+ queuePending: (message) => writePendingEnvelope(pendingDir, message),
1221
+ deliverers: {
1222
+ bluebubbles: async (request) => {
1223
+ const { sendProactiveBlueBubblesMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/bluebubbles")));
1224
+ const result = await sendProactiveBlueBubblesMessageToSession({
1225
+ friendId: request.friendId,
1226
+ sessionKey: request.key,
1227
+ text: request.content,
1228
+ intent: request.intent,
1229
+ authorizingSession: request.authorizingSession,
1230
+ });
1231
+ if (result.delivered) {
1232
+ return {
1233
+ status: "delivered_now",
1234
+ detail: "sent to the active bluebubbles chat now",
1235
+ };
1236
+ }
1237
+ if (result.reason === "missing_target") {
1238
+ return {
1239
+ status: "blocked",
1240
+ detail: "bluebubbles could not resolve a routable target for that session",
1241
+ };
1242
+ }
1243
+ if (result.reason === "send_error") {
1244
+ return {
1245
+ status: "failed",
1246
+ detail: "bluebubbles send failed",
1247
+ };
1248
+ }
1249
+ return {
1250
+ status: "unavailable",
1251
+ detail: "live delivery unavailable right now; queued for the next active turn",
1252
+ };
1253
+ },
1254
+ teams: async (request) => {
1255
+ if (!ctx?.botApi) {
1256
+ return {
1257
+ status: "unavailable",
1258
+ detail: "live delivery unavailable right now; queued for the next active turn",
1259
+ };
1260
+ }
1261
+ const { sendProactiveTeamsMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/teams")));
1262
+ const result = await sendProactiveTeamsMessageToSession({
1263
+ friendId: request.friendId,
1264
+ sessionKey: request.key,
1265
+ text: request.content,
1266
+ intent: request.intent,
1267
+ authorizingSession: request.authorizingSession,
1268
+ }, {
1269
+ botApi: ctx.botApi,
1270
+ });
1271
+ if (result.delivered) {
1272
+ return {
1273
+ status: "delivered_now",
1274
+ detail: "sent to the active teams chat now",
1275
+ };
1276
+ }
1277
+ if (result.reason === "missing_target") {
1278
+ return {
1279
+ status: "blocked",
1280
+ detail: "teams could not resolve a routable target for that session",
1281
+ };
1282
+ }
1283
+ if (result.reason === "send_error") {
1284
+ return {
1285
+ status: "failed",
1286
+ detail: "teams send failed",
1287
+ };
1288
+ }
1289
+ return {
1290
+ status: "unavailable",
1291
+ detail: "live delivery unavailable right now; queued for the next active turn",
1292
+ };
1293
+ },
1294
+ },
1295
+ });
1296
+ return renderCrossChatDeliveryStatus(`${friendId} on ${channel}/${key}`, deliveryResult);
695
1297
  },
696
1298
  },
1299
+ {
1300
+ tool: {
1301
+ type: "function",
1302
+ function: {
1303
+ name: "set_reasoning_effort",
1304
+ description: "adjust your own reasoning depth for subsequent turns. use higher effort for complex analysis, lower for simple tasks.",
1305
+ parameters: {
1306
+ type: "object",
1307
+ properties: {
1308
+ level: { type: "string", description: "the reasoning effort level to set" },
1309
+ },
1310
+ required: ["level"],
1311
+ },
1312
+ },
1313
+ },
1314
+ handler: (args, ctx) => {
1315
+ if (!ctx?.supportedReasoningEfforts || !ctx.setReasoningEffort) {
1316
+ return "reasoning effort adjustment is not available in this context.";
1317
+ }
1318
+ const level = (args.level || "").trim();
1319
+ if (!ctx.supportedReasoningEfforts.includes(level)) {
1320
+ return `invalid reasoning effort level "${level}". accepted levels: ${ctx.supportedReasoningEfforts.join(", ")}`;
1321
+ }
1322
+ ctx.setReasoningEffort(level);
1323
+ (0, runtime_1.emitNervesEvent)({
1324
+ component: "repertoire",
1325
+ event: "repertoire.reasoning_effort_changed",
1326
+ message: `reasoning effort set to ${level}`,
1327
+ meta: { level },
1328
+ });
1329
+ return `reasoning effort set to "${level}".`;
1330
+ },
1331
+ requiredCapability: "reasoning-effort",
1332
+ },
697
1333
  ...tools_1.codingToolDefinitions,
698
1334
  ];
699
- // Backward-compat: extract just the OpenAI tool schemas
700
1335
  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]));
1336
+ exports.goInwardTool = {
1337
+ type: "function",
1338
+ function: {
1339
+ name: "go_inward",
1340
+ description: "i need to think about this privately. this takes the current thread inward -- i'll sit with it, work through it, or carry it to where it needs to go. must be the only tool call in the turn.",
1341
+ parameters: {
1342
+ type: "object",
1343
+ properties: {
1344
+ content: {
1345
+ type: "string",
1346
+ description: "what i need to think about -- the question, the thread, the thing that needs private attention",
1347
+ },
1348
+ answer: {
1349
+ type: "string",
1350
+ description: "if i want to say something outward before going inward -- an acknowledgment, a 'let me think about that', whatever feels right",
1351
+ },
1352
+ mode: {
1353
+ type: "string",
1354
+ enum: ["reflect", "plan", "relay"],
1355
+ description: "reflect: something to sit with. plan: something to work through. relay: something to carry across.",
1356
+ },
1357
+ },
1358
+ required: ["content"],
1359
+ },
1360
+ },
1361
+ };
1362
+ exports.noResponseTool = {
1363
+ type: "function",
1364
+ function: {
1365
+ name: "no_response",
1366
+ description: "stay silent in this group chat — the moment doesn't call for a response. must be the only tool call in the turn.",
1367
+ parameters: {
1368
+ type: "object",
1369
+ properties: {
1370
+ reason: { type: "string", description: "brief reason for staying silent (for logging)" },
1371
+ },
1372
+ },
1373
+ },
1374
+ };
703
1375
  exports.finalAnswerTool = {
704
1376
  type: "function",
705
1377
  function: {
@@ -707,7 +1379,10 @@ exports.finalAnswerTool = {
707
1379
  description: "respond to the user with your message. call this tool when you are ready to deliver your response.",
708
1380
  parameters: {
709
1381
  type: "object",
710
- properties: { answer: { type: "string" } },
1382
+ properties: {
1383
+ answer: { type: "string" },
1384
+ intent: { type: "string", enum: ["complete", "blocked", "direct_reply"] },
1385
+ },
711
1386
  required: ["answer"],
712
1387
  },
713
1388
  },