@ouro.bot/cli 0.1.0-alpha.19 → 0.1.0-alpha.191

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 (202) hide show
  1. package/README.md +187 -191
  2. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +1 -1
  4. package/changelog.json +1200 -0
  5. package/dist/heart/active-work.js +832 -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/cares.js +174 -0
  10. package/dist/heart/commitments.js +111 -0
  11. package/dist/heart/config.js +40 -30
  12. package/dist/heart/core.js +620 -124
  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 +2470 -210
  19. package/dist/heart/daemon/daemon-entry.js +157 -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 +449 -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-flow.js +31 -112
  28. package/dist/heart/daemon/hatch-specialist.js +3 -3
  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 +165 -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 +399 -0
  41. package/dist/heart/daemon/outlook-read.js +1331 -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-prompt.js +10 -7
  56. package/dist/heart/daemon/specialist-tools.js +49 -3
  57. package/dist/heart/daemon/staged-restart.js +114 -0
  58. package/dist/heart/daemon/task-scheduler.js +3 -25
  59. package/dist/heart/daemon/thoughts.js +507 -0
  60. package/dist/heart/daemon/update-checker.js +111 -0
  61. package/dist/heart/daemon/update-hooks.js +138 -0
  62. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  63. package/dist/heart/delegation.js +62 -0
  64. package/dist/heart/identity.js +136 -55
  65. package/dist/heart/intentions.js +168 -0
  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 +220 -0
  70. package/dist/heart/presence.js +185 -0
  71. package/dist/heart/progress-story.js +42 -0
  72. package/dist/heart/provider-failover.js +88 -0
  73. package/dist/heart/provider-ping.js +172 -0
  74. package/dist/heart/providers/anthropic-token.js +163 -0
  75. package/dist/heart/providers/anthropic.js +187 -41
  76. package/dist/heart/providers/azure.js +116 -10
  77. package/dist/heart/providers/github-copilot.js +157 -0
  78. package/dist/heart/providers/minimax.js +33 -3
  79. package/dist/heart/providers/openai-codex.js +51 -16
  80. package/dist/heart/session-activity.js +172 -0
  81. package/dist/heart/session-recall.js +216 -0
  82. package/dist/heart/streaming.js +36 -27
  83. package/dist/heart/target-resolution.js +127 -0
  84. package/dist/heart/tempo.js +93 -0
  85. package/dist/heart/temporal-view.js +41 -0
  86. package/dist/heart/tool-activity-callbacks.js +36 -0
  87. package/dist/heart/tool-description.js +135 -0
  88. package/dist/heart/tool-loop.js +200 -0
  89. package/dist/heart/turn-coordinator.js +28 -0
  90. package/dist/heart/wake-packet.js +226 -0
  91. package/dist/mind/associative-recall.js +122 -32
  92. package/dist/mind/bundle-manifest.js +14 -0
  93. package/dist/mind/context.js +125 -14
  94. package/dist/mind/{memory.js → diary.js} +70 -56
  95. package/dist/mind/episodes.js +116 -0
  96. package/dist/mind/file-state.js +179 -0
  97. package/dist/mind/first-impressions.js +16 -2
  98. package/dist/mind/friends/channel.js +56 -0
  99. package/dist/mind/friends/group-context.js +144 -0
  100. package/dist/mind/friends/store-file.js +36 -0
  101. package/dist/mind/friends/trust-explanation.js +74 -0
  102. package/dist/mind/friends/types.js +8 -0
  103. package/dist/mind/journal-index.js +161 -0
  104. package/dist/mind/obligation-steering.js +221 -0
  105. package/dist/mind/obligations.js +134 -0
  106. package/dist/mind/pending.js +76 -9
  107. package/dist/mind/phrases.js +1 -0
  108. package/dist/mind/prompt.js +779 -102
  109. package/dist/mind/scrutiny.js +173 -0
  110. package/dist/mind/token-estimate.js +8 -12
  111. package/dist/nerves/coverage/audit.js +1 -1
  112. package/dist/nerves/coverage/run-artifacts.js +1 -1
  113. package/dist/nerves/event-buffer.js +111 -0
  114. package/dist/nerves/index.js +63 -3
  115. package/dist/nerves/observation.js +20 -0
  116. package/dist/nerves/runtime.js +5 -1
  117. package/dist/outlook-ui/assets/index-DN6vVYPm.css +1 -0
  118. package/dist/outlook-ui/assets/index-M1l1tDTX.js +61 -0
  119. package/dist/outlook-ui/index.html +15 -0
  120. package/dist/repertoire/ado-semantic.js +11 -5
  121. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  122. package/dist/repertoire/coding/context-pack.js +330 -0
  123. package/dist/repertoire/coding/feedback.js +197 -30
  124. package/dist/repertoire/coding/manager.js +149 -2
  125. package/dist/repertoire/coding/spawner.js +55 -9
  126. package/dist/repertoire/coding/tools.js +170 -7
  127. package/dist/repertoire/guardrails.js +291 -0
  128. package/dist/repertoire/mcp-client.js +254 -0
  129. package/dist/repertoire/mcp-manager.js +251 -0
  130. package/dist/repertoire/shell-sessions.js +133 -0
  131. package/dist/repertoire/skills.js +14 -23
  132. package/dist/repertoire/tasks/board.js +43 -5
  133. package/dist/repertoire/tasks/fix.js +182 -0
  134. package/dist/repertoire/tasks/index.js +28 -10
  135. package/dist/repertoire/tasks/lifecycle.js +2 -2
  136. package/dist/repertoire/tasks/parser.js +3 -2
  137. package/dist/repertoire/tasks/scanner.js +194 -37
  138. package/dist/repertoire/tasks/transitions.js +11 -28
  139. package/dist/repertoire/tools-base.js +1345 -320
  140. package/dist/repertoire/tools-bluebubbles.js +94 -0
  141. package/dist/repertoire/tools-github.js +1 -6
  142. package/dist/repertoire/tools-teams.js +12 -59
  143. package/dist/repertoire/tools.js +318 -124
  144. package/dist/scripts/claude-code-hook.js +41 -0
  145. package/dist/scripts/claude-code-stop-hook.js +47 -0
  146. package/dist/senses/attention-queue.js +97 -0
  147. package/dist/senses/bluebubbles-client.js +102 -3
  148. package/dist/senses/bluebubbles-entry.js +2 -0
  149. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  150. package/dist/senses/bluebubbles-media.js +118 -23
  151. package/dist/senses/bluebubbles-model.js +12 -4
  152. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  153. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  154. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  155. package/dist/senses/bluebubbles.js +1008 -102
  156. package/dist/senses/cli/adapter.js +274 -0
  157. package/dist/senses/cli/ink-app.js +156 -0
  158. package/dist/senses/cli/inline-diff.js +64 -0
  159. package/dist/senses/cli/message-list.js +51 -0
  160. package/dist/senses/cli/ouro-tui.js +349 -0
  161. package/dist/senses/cli/spinner-imperative.js +135 -0
  162. package/dist/senses/cli/spinner.js +101 -0
  163. package/dist/senses/cli/status-line.js +60 -0
  164. package/dist/senses/cli/streaming-markdown.js +526 -0
  165. package/dist/senses/cli/tool-display.js +83 -0
  166. package/dist/senses/cli/tool-render.js +85 -0
  167. package/dist/senses/cli/tui-store.js +198 -0
  168. package/dist/senses/cli/virtual-list.js +35 -0
  169. package/dist/senses/cli-entry.js +1 -1
  170. package/dist/senses/cli-layout.js +187 -0
  171. package/dist/senses/cli.js +508 -243
  172. package/dist/senses/commands.js +65 -1
  173. package/dist/senses/continuity.js +94 -0
  174. package/dist/senses/habit-turn-message.js +108 -0
  175. package/dist/senses/inner-dialog-worker.js +112 -19
  176. package/dist/senses/inner-dialog.js +592 -84
  177. package/dist/senses/pipeline.js +633 -0
  178. package/dist/senses/shared-turn.js +196 -0
  179. package/dist/senses/surface-tool.js +95 -0
  180. package/dist/senses/teams.js +667 -120
  181. package/dist/senses/trust-gate.js +112 -2
  182. package/package.json +16 -6
  183. package/skills/configure-dev-tools.md +81 -0
  184. package/subagents/README.md +4 -70
  185. package/dist/heart/daemon/subagent-installer.js +0 -134
  186. package/dist/senses/debug-activity.js +0 -107
  187. package/subagents/work-doer.md +0 -235
  188. package/subagents/work-merger.md +0 -618
  189. package/subagents/work-planner.md +0 -382
  190. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  191. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  192. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  193. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  194. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  195. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  196. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  197. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  198. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  199. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  200. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  201. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +0 -0
  202. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
@@ -33,60 +33,361 @@ 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");
43
+ const shell_sessions_1 = require("./shell-sessions");
41
44
  const config_1 = require("../heart/config");
42
45
  const runtime_1 = require("../nerves/runtime");
46
+ const file_state_1 = require("../mind/file-state");
47
+ const scrutiny_1 = require("../mind/scrutiny");
43
48
  const identity_1 = require("../heart/identity");
44
- const os = __importStar(require("os"));
45
- const tasks_1 = require("./tasks");
49
+ const identity_2 = require("../heart/identity");
50
+ const socket_client_1 = require("../heart/daemon/socket-client");
51
+ const thoughts_1 = require("../heart/daemon/thoughts");
52
+ const manager_1 = require("../heart/bridges/manager");
53
+ const session_recall_1 = require("../heart/session-recall");
54
+ const session_activity_1 = require("../heart/session-activity");
55
+ const active_work_1 = require("../heart/active-work");
46
56
  const tools_1 = require("./coding/tools");
47
- const memory_1 = require("../mind/memory");
48
- const postIt = (msg) => `post-it from past you:\n${msg}`;
49
- function normalizeOptionalText(value) {
50
- if (typeof value !== "string")
57
+ const coding_1 = require("./coding");
58
+ const diary_1 = require("../mind/diary");
59
+ const tasks_1 = require("./tasks");
60
+ const pending_1 = require("../mind/pending");
61
+ const obligations_1 = require("../mind/obligations");
62
+ const progress_story_1 = require("../heart/progress-story");
63
+ const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
64
+ const obligations_2 = require("../heart/obligations");
65
+ const episodes_1 = require("../mind/episodes");
66
+ const cares_1 = require("../heart/cares");
67
+ const presence_1 = require("../heart/presence");
68
+ const intentions_1 = require("../heart/intentions");
69
+ // Tracks which file paths have been read via read_file in this session.
70
+ // edit_file requires a file to be read first (must-read-first guard).
71
+ exports.editFileReadTracker = new Set();
72
+ function buildContextDiff(lines, changeStart, changeEnd, contextSize = 3) {
73
+ const start = Math.max(0, changeStart - contextSize);
74
+ const end = Math.min(lines.length, changeEnd + contextSize);
75
+ const result = [];
76
+ for (let i = start; i < end; i++) {
77
+ const lineNum = i + 1;
78
+ const prefix = (i >= changeStart && i < changeEnd) ? ">" : " ";
79
+ result.push(`${prefix} ${lineNum} | ${lines[i]}`);
80
+ }
81
+ return result.join("\n");
82
+ }
83
+ function resolveLocalToolPath(targetPath) {
84
+ if (!path.isAbsolute(targetPath)) {
85
+ return path.resolve((0, identity_2.getRepoRoot)(), targetPath);
86
+ }
87
+ return targetPath;
88
+ }
89
+ const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
90
+ const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
91
+ function findDelegatingBridgeId(ctx) {
92
+ const currentSession = ctx?.currentSession;
93
+ if (!currentSession)
94
+ return undefined;
95
+ return ctx?.activeBridges?.find((bridge) => bridge.lifecycle === "active"
96
+ && bridge.attachedSessions.some((session) => session.friendId === currentSession.friendId
97
+ && session.channel === currentSession.channel
98
+ && session.key === currentSession.key))?.id;
99
+ }
100
+ async function recallSessionSafely(options) {
101
+ try {
102
+ return await (0, session_recall_1.recallSession)(options);
103
+ }
104
+ catch (error) {
105
+ if (options.summarize) {
106
+ (0, runtime_1.emitNervesEvent)({
107
+ component: "daemon",
108
+ event: "daemon.session_recall_summary_fallback",
109
+ message: "session recall summarization failed; using raw transcript",
110
+ meta: {
111
+ friendId: options.friendId,
112
+ channel: options.channel,
113
+ key: options.key,
114
+ error: error instanceof Error ? error.message : String(error),
115
+ },
116
+ });
117
+ try {
118
+ return await (0, session_recall_1.recallSession)({
119
+ ...options,
120
+ summarize: undefined,
121
+ });
122
+ }
123
+ catch {
124
+ return { kind: "missing" };
125
+ }
126
+ }
127
+ return { kind: "missing" };
128
+ }
129
+ }
130
+ async function searchSessionSafely(options) {
131
+ try {
132
+ return await (0, session_recall_1.searchSessionTranscript)(options);
133
+ }
134
+ catch {
135
+ return { kind: "missing" };
136
+ }
137
+ }
138
+ function normalizeProgressOutcome(text) {
139
+ const trimmed = text.trim();
140
+ /* v8 ignore next -- defensive: normalizeProgressOutcome null branch @preserve */
141
+ if (!trimmed || trimmed === "nothing yet" || trimmed === "nothing recent") {
51
142
  return null;
52
- const trimmed = value.trim();
53
- return trimmed.length > 0 ? trimmed : null;
143
+ }
144
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) {
145
+ return trimmed.slice(1, -1);
146
+ }
147
+ return trimmed;
54
148
  }
55
- function buildTaskCreateInput(args) {
149
+ function writePendingEnvelope(queueDir, message) {
150
+ fs.mkdirSync(queueDir, { recursive: true });
151
+ const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
152
+ const filePath = path.join(queueDir, fileName);
153
+ fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
154
+ }
155
+ function renderCrossChatDeliveryStatus(target, result) {
156
+ const phase = result.status === "delivered_now"
157
+ ? "completed"
158
+ : result.status === "queued_for_later"
159
+ ? "queued"
160
+ : result.status === "blocked"
161
+ ? "blocked"
162
+ : "errored";
163
+ const lead = result.status === "delivered_now"
164
+ ? "delivered now"
165
+ : result.status === "queued_for_later"
166
+ ? "queued for later"
167
+ : result.status === "blocked"
168
+ ? "blocked"
169
+ : "failed";
170
+ return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
171
+ scope: "shared-work",
172
+ phase,
173
+ objective: `message to ${target}`,
174
+ outcomeText: `${lead}\n${result.detail}`,
175
+ }));
176
+ }
177
+ function emptyTaskBoard() {
56
178
  return {
57
- title: args.title,
58
- type: args.type,
59
- category: args.category,
60
- body: args.body,
61
- status: normalizeOptionalText(args.status) ?? undefined,
62
- validator: normalizeOptionalText(args.validator),
63
- requester: normalizeOptionalText(args.requester),
64
- cadence: normalizeOptionalText(args.cadence),
65
- scheduledAt: normalizeOptionalText(args.scheduledAt),
179
+ compact: "",
180
+ full: "",
181
+ byStatus: {
182
+ drafting: [],
183
+ processing: [],
184
+ validating: [],
185
+ collaborating: [],
186
+ paused: [],
187
+ blocked: [],
188
+ done: [],
189
+ cancelled: [],
190
+ },
191
+ issues: [],
192
+ actionRequired: [],
193
+ unresolvedDependencies: [],
194
+ activeSessions: [],
195
+ activeBridges: [],
66
196
  };
67
197
  }
198
+ function isLiveCodingSessionStatus(status) {
199
+ return status === "spawning"
200
+ || status === "running"
201
+ || status === "waiting_input"
202
+ || status === "stalled";
203
+ }
204
+ function readActiveWorkInnerState() {
205
+ const defaultJob = {
206
+ status: "idle",
207
+ content: null,
208
+ origin: null,
209
+ mode: "reflect",
210
+ obligationStatus: null,
211
+ surfacedResult: null,
212
+ queuedAt: null,
213
+ startedAt: null,
214
+ surfacedAt: null,
215
+ };
216
+ try {
217
+ const agentRoot = (0, identity_1.getAgentRoot)();
218
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
219
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
220
+ const { pendingMessages, turns, runtimeState } = (0, thoughts_1.readInnerDialogRawData)(sessionPath, pendingDir);
221
+ const dialogStatus = (0, thoughts_1.deriveInnerDialogStatus)(pendingMessages, turns, runtimeState);
222
+ const job = (0, thoughts_1.deriveInnerJob)(pendingMessages, turns, runtimeState);
223
+ const storeObligationPending = (0, obligations_2.readPendingObligations)(agentRoot).length > 0;
224
+ return {
225
+ status: dialogStatus.processing === "started" ? "running" : "idle",
226
+ hasPending: dialogStatus.queue !== "clear",
227
+ origin: dialogStatus.origin,
228
+ contentSnippet: dialogStatus.contentSnippet,
229
+ obligationPending: dialogStatus.obligationPending || storeObligationPending,
230
+ job,
231
+ };
232
+ }
233
+ catch {
234
+ return {
235
+ status: "idle",
236
+ hasPending: false,
237
+ job: defaultJob,
238
+ };
239
+ }
240
+ }
241
+ async function buildToolActiveWorkFrame(ctx) {
242
+ const currentSession = ctx?.currentSession
243
+ ? {
244
+ friendId: ctx.currentSession.friendId,
245
+ channel: ctx.currentSession.channel,
246
+ key: ctx.currentSession.key,
247
+ sessionPath: (0, config_1.resolveSessionPath)(ctx.currentSession.friendId, ctx.currentSession.channel, ctx.currentSession.key),
248
+ }
249
+ : null;
250
+ const agentRoot = (0, identity_1.getAgentRoot)();
251
+ const bridges = currentSession
252
+ ? (0, manager_1.createBridgeManager)().findBridgesForSession({
253
+ friendId: currentSession.friendId,
254
+ channel: currentSession.channel,
255
+ key: currentSession.key,
256
+ })
257
+ : [];
258
+ let friendActivity = [];
259
+ try {
260
+ friendActivity = (0, session_activity_1.listSessionActivity)({
261
+ sessionsDir: `${agentRoot}/state/sessions`,
262
+ friendsDir: `${agentRoot}/friends`,
263
+ agentName: (0, identity_1.getAgentName)(),
264
+ currentSession,
265
+ });
266
+ }
267
+ catch {
268
+ friendActivity = [];
269
+ }
270
+ const pendingObligations = (() => {
271
+ try {
272
+ return (0, obligations_2.readPendingObligations)(agentRoot);
273
+ }
274
+ catch {
275
+ return [];
276
+ }
277
+ })();
278
+ let codingSessions = [];
279
+ let otherCodingSessions = [];
280
+ try {
281
+ const liveCodingSessions = (0, coding_1.getCodingSessionManager)()
282
+ .listSessions()
283
+ .filter((session) => isLiveCodingSessionStatus(session.status) && Boolean(session.originSession));
284
+ if (currentSession) {
285
+ codingSessions = liveCodingSessions.filter((session) => session.originSession?.friendId === currentSession.friendId
286
+ && session.originSession.channel === currentSession.channel
287
+ && session.originSession.key === currentSession.key);
288
+ otherCodingSessions = liveCodingSessions.filter((session) => !(session.originSession?.friendId === currentSession.friendId
289
+ && session.originSession.channel === currentSession.channel
290
+ && session.originSession.key === currentSession.key));
291
+ }
292
+ else {
293
+ codingSessions = [];
294
+ otherCodingSessions = liveCodingSessions;
295
+ }
296
+ }
297
+ catch {
298
+ codingSessions = [];
299
+ otherCodingSessions = [];
300
+ }
301
+ const currentObligation = currentSession
302
+ ? pendingObligations.find((obligation) => obligation.status !== "fulfilled"
303
+ && obligation.origin.friendId === currentSession.friendId
304
+ && obligation.origin.channel === currentSession.channel
305
+ && obligation.origin.key === currentSession.key)?.content ?? null
306
+ : null;
307
+ return (0, active_work_1.buildActiveWorkFrame)({
308
+ currentSession,
309
+ currentObligation,
310
+ mustResolveBeforeHandoff: false,
311
+ inner: readActiveWorkInnerState(),
312
+ bridges,
313
+ codingSessions,
314
+ otherCodingSessions,
315
+ pendingObligations,
316
+ taskBoard: (() => {
317
+ try {
318
+ return (0, tasks_1.getTaskModule)().getBoard();
319
+ }
320
+ catch {
321
+ return emptyTaskBoard();
322
+ }
323
+ })(),
324
+ friendActivity,
325
+ targetCandidates: [],
326
+ });
327
+ }
328
+ function renderInnerProgressStatus(status) {
329
+ if (status.processing === "pending") {
330
+ return "i've queued this thought for private attention. it'll come up when my inner dialog is free.";
331
+ }
332
+ if (status.processing === "started") {
333
+ return "i'm working through this privately right now.";
334
+ }
335
+ // processed / completed
336
+ if (status.surfaced && status.surfaced !== "nothing recent" && status.surfaced !== "no outward result") {
337
+ return `i thought about this privately and came to something: ${status.surfaced}`;
338
+ }
339
+ return "i thought about this privately. i'll bring it back when the time is right.";
340
+ }
68
341
  exports.baseToolDefinitions = [
69
342
  {
70
343
  tool: {
71
344
  type: "function",
72
345
  function: {
73
346
  name: "read_file",
74
- description: "read file contents",
347
+ description: "Read file contents. Results include line numbers. Use offset/limit for large files -- don't read the whole thing if you only need a section. Use this tool before editing any file. When reading code, read enough context to understand the surrounding logic, not just the target line.",
75
348
  parameters: {
76
349
  type: "object",
77
- properties: { path: { type: "string" } },
350
+ properties: {
351
+ path: { type: "string" },
352
+ offset: { type: "number", description: "1-based line number to start reading from" },
353
+ limit: { type: "number", description: "maximum number of lines to return" },
354
+ },
78
355
  required: ["path"],
79
356
  },
80
357
  },
81
358
  },
82
- handler: (a) => fs.readFileSync(a.path, "utf-8"),
359
+ handler: (a) => {
360
+ const resolvedPath = resolveLocalToolPath(a.path);
361
+ const content = fs.readFileSync(resolvedPath, "utf-8");
362
+ exports.editFileReadTracker.add(resolvedPath);
363
+ const offset = a.offset ? parseInt(a.offset, 10) : undefined;
364
+ const limit = a.limit ? parseInt(a.limit, 10) : undefined;
365
+ // Record in file state cache for staleness detection
366
+ try {
367
+ const mtime = fs.statSync(resolvedPath).mtimeMs;
368
+ const readContent = (offset === undefined && limit === undefined)
369
+ ? content
370
+ : content.split("\n").slice(offset ? offset - 1 : 0, limit !== undefined ? (offset ? offset - 1 : 0) + limit : undefined).join("\n");
371
+ file_state_1.fileStateCache.record(resolvedPath, readContent, mtime, offset, limit);
372
+ }
373
+ catch {
374
+ // stat failed -- skip cache recording
375
+ }
376
+ if (offset === undefined && limit === undefined)
377
+ return content;
378
+ const lines = content.split("\n");
379
+ const start = offset ? offset - 1 : 0;
380
+ const end = limit !== undefined ? start + limit : lines.length;
381
+ return lines.slice(start, end).join("\n");
382
+ },
383
+ summaryKeys: ["path"],
83
384
  },
84
385
  {
85
386
  tool: {
86
387
  type: "function",
87
388
  function: {
88
389
  name: "write_file",
89
- description: "write content to file",
390
+ description: "Prefer this tool for creating new files or fully replacing existing ones. You MUST read an existing file with read_file before overwriting it. Prefer edit_file for modifying existing files -- it only sends the diff. Do not create documentation files (*.md, README) by default; only do so when explicitly asked or when documentation is clearly part of the requested change.",
90
391
  parameters: {
91
392
  type: "object",
92
393
  properties: { path: { type: "string" }, content: { type: "string" } },
@@ -95,106 +396,339 @@ exports.baseToolDefinitions = [
95
396
  },
96
397
  },
97
398
  handler: (a) => {
98
- fs.mkdirSync(path.dirname(a.path), { recursive: true });
99
- fs.writeFileSync(a.path, a.content, "utf-8");
100
- return "ok";
399
+ const resolvedPath = resolveLocalToolPath(a.path);
400
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
401
+ fs.writeFileSync(resolvedPath, a.content, "utf-8");
402
+ (0, scrutiny_1.trackModifiedFile)(resolvedPath);
403
+ const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
404
+ /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
405
+ return scrutiny ? `ok\n\n${scrutiny}` : "ok";
101
406
  },
407
+ summaryKeys: ["path"],
102
408
  },
103
409
  {
104
410
  tool: {
105
411
  type: "function",
106
412
  function: {
107
- name: "shell",
108
- description: "run shell command",
413
+ name: "edit_file",
414
+ description: "Surgically edit a file by replacing an exact string. The file MUST have been read via read_file first -- this tool will reject the call otherwise. old_string must match EXACTLY ONE location in the file -- if it matches zero or multiple, the edit fails. To fix: provide more surrounding context to make the match unique. Preserve exact indentation (tabs/spaces) from the file. Prefer this over write_file for modifications -- it only sends the diff.",
109
415
  parameters: {
110
416
  type: "object",
111
- properties: { command: { type: "string" } },
112
- required: ["command"],
417
+ properties: {
418
+ path: { type: "string" },
419
+ old_string: { type: "string" },
420
+ new_string: { type: "string" },
421
+ },
422
+ required: ["path", "old_string", "new_string"],
113
423
  },
114
424
  },
115
425
  },
116
- handler: (a) => (0, child_process_1.execSync)(a.command, { encoding: "utf-8", timeout: 30000 }),
426
+ handler: (a) => {
427
+ const resolvedPath = resolveLocalToolPath(a.path);
428
+ if (!exports.editFileReadTracker.has(resolvedPath)) {
429
+ return `error: you must read the file with read_file before editing it. call read_file on ${a.path} first.`;
430
+ }
431
+ // Check staleness before editing
432
+ const stalenessCheck = file_state_1.fileStateCache.isStale(resolvedPath);
433
+ let content;
434
+ try {
435
+ content = fs.readFileSync(resolvedPath, "utf-8");
436
+ }
437
+ catch (e) {
438
+ return `error: could not read file: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
439
+ }
440
+ // Count occurrences
441
+ const occurrences = [];
442
+ let searchFrom = 0;
443
+ while (true) {
444
+ const idx = content.indexOf(a.old_string, searchFrom);
445
+ if (idx === -1)
446
+ break;
447
+ occurrences.push(idx);
448
+ searchFrom = idx + 1;
449
+ }
450
+ if (occurrences.length === 0) {
451
+ return `error: old_string not found in ${a.path}`;
452
+ }
453
+ if (occurrences.length > 1) {
454
+ return `error: old_string is ambiguous -- found ${occurrences.length} matches in ${a.path}. provide more context to make the match unique.`;
455
+ }
456
+ // Single unique match -- replace
457
+ const idx = occurrences[0];
458
+ const updated = content.slice(0, idx) + a.new_string + content.slice(idx + a.old_string.length);
459
+ fs.writeFileSync(resolvedPath, updated, "utf-8");
460
+ // Update file state cache with new content
461
+ try {
462
+ const newMtime = fs.statSync(resolvedPath).mtimeMs;
463
+ file_state_1.fileStateCache.record(resolvedPath, updated, newMtime);
464
+ }
465
+ catch {
466
+ // stat failed -- skip cache update
467
+ }
468
+ // Build contextual diff
469
+ const lines = updated.split("\n");
470
+ const prefixLines = content.slice(0, idx).split("\n");
471
+ const changeStartLine = prefixLines.length - 1;
472
+ const newStringLines = a.new_string.split("\n");
473
+ const changeEndLine = changeStartLine + newStringLines.length;
474
+ const diffResult = buildContextDiff(lines, changeStartLine, changeEndLine);
475
+ // Track modified file and compute scrutiny appendix
476
+ (0, scrutiny_1.trackModifiedFile)(resolvedPath);
477
+ const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
478
+ // Append staleness warning if detected (do not block -- TTFA)
479
+ /* v8 ignore start -- staleness+diff+scrutiny combo not exercised in integration tests @preserve */
480
+ if (stalenessCheck.stale) {
481
+ const base = `${diffResult}\n\n⚠️ warning: file changed externally since last read -- re-read recommended`;
482
+ return scrutiny ? `${base}\n\n${scrutiny}` : base;
483
+ }
484
+ /* v8 ignore stop */
485
+ /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
486
+ return scrutiny ? `${diffResult}\n\n${scrutiny}` : diffResult;
487
+ },
488
+ summaryKeys: ["path"],
117
489
  },
118
490
  {
119
491
  tool: {
120
492
  type: "function",
121
493
  function: {
122
- name: "list_directory",
123
- description: "list directory contents",
494
+ name: "glob",
495
+ description: "Find files matching a glob pattern, sorted alphabetically. Use this instead of shell commands like `find` or `ls`. For broad exploratory searches that would require multiple rounds of globbing and grepping, consider using claude or coding_spawn.",
124
496
  parameters: {
125
497
  type: "object",
126
- properties: { path: { type: "string" } },
127
- required: ["path"],
498
+ properties: {
499
+ pattern: { type: "string", description: "glob pattern (e.g. **/*.ts)" },
500
+ cwd: { type: "string", description: "directory to search from (defaults to process.cwd())" },
501
+ },
502
+ required: ["pattern"],
128
503
  },
129
504
  },
130
505
  },
131
- handler: (a) => fs
132
- .readdirSync(a.path, { withFileTypes: true })
133
- .map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
134
- .join("\n"),
506
+ handler: (a) => {
507
+ const cwd = a.cwd ? resolveLocalToolPath(a.cwd) : process.cwd();
508
+ const matches = fg.globSync(a.pattern, { cwd, dot: true });
509
+ return matches.sort().join("\n");
510
+ },
511
+ summaryKeys: ["pattern", "cwd"],
135
512
  },
136
513
  {
137
514
  tool: {
138
515
  type: "function",
139
516
  function: {
140
- name: "git_commit",
141
- description: "commit changes to git with explicit paths",
517
+ name: "grep",
518
+ description: "Search file contents for lines matching a regex pattern. Searches recursively in directories. Use this instead of shell commands like `grep` or `rg`. Returns matching lines with file path and line numbers. Use context_lines for surrounding context. Use include to filter file types (e.g., '*.ts').",
142
519
  parameters: {
143
520
  type: "object",
144
521
  properties: {
145
- message: { type: "string" },
146
- paths: { type: "array", items: { type: "string" } },
522
+ pattern: { type: "string", description: "regex pattern to search for" },
523
+ path: { type: "string", description: "file or directory to search" },
524
+ context_lines: { type: "number", description: "number of surrounding context lines (default 0)" },
525
+ include: { type: "string", description: "glob filter to limit searched files (e.g. *.ts)" },
147
526
  },
148
- required: ["message", "paths"],
527
+ required: ["pattern", "path"],
149
528
  },
150
529
  },
151
530
  },
152
531
  handler: (a) => {
153
- try {
154
- if (!a.paths || !Array.isArray(a.paths) || a.paths.length === 0) {
155
- return postIt("paths are required. specify explicit files to commit.");
532
+ const targetPath = resolveLocalToolPath(a.path);
533
+ const regex = new RegExp(a.pattern);
534
+ const contextLines = parseInt(a.context_lines || "0", 10);
535
+ const includeGlob = a.include || undefined;
536
+ function searchFile(filePath) {
537
+ let content;
538
+ try {
539
+ content = fs.readFileSync(filePath, "utf-8");
540
+ }
541
+ catch {
542
+ return [];
156
543
  }
157
- for (const p of a.paths) {
158
- if (!fs.existsSync(p)) {
159
- return postIt(`path does not exist: ${p}`);
544
+ const lines = content.split("\n");
545
+ const matchIndices = new Set();
546
+ for (let i = 0; i < lines.length; i++) {
547
+ if (regex.test(lines[i])) {
548
+ matchIndices.add(i);
160
549
  }
161
- (0, child_process_1.execSync)(`git add ${p}`, { encoding: "utf-8" });
162
550
  }
163
- const diff = (0, child_process_1.execSync)("git diff --cached --stat", { encoding: "utf-8" });
164
- if (!diff || diff.trim().length === 0) {
165
- return postIt("nothing was staged. check your changes or paths.");
551
+ if (matchIndices.size === 0)
552
+ return [];
553
+ const outputIndices = new Set();
554
+ for (const idx of matchIndices) {
555
+ const start = Math.max(0, idx - contextLines);
556
+ const end = Math.min(lines.length - 1, idx + contextLines);
557
+ for (let i = start; i <= end; i++) {
558
+ outputIndices.add(i);
559
+ }
166
560
  }
167
- (0, child_process_1.execSync)(`git commit -m \"${a.message}\"`, { encoding: "utf-8" });
168
- return `${diff}\ncommitted`;
561
+ const sortedIndices = [...outputIndices].sort((a, b) => a - b);
562
+ const results = [];
563
+ for (const idx of sortedIndices) {
564
+ const lineNum = idx + 1;
565
+ if (matchIndices.has(idx)) {
566
+ results.push(`${filePath}:${lineNum}: ${lines[idx]}`);
567
+ }
568
+ else {
569
+ results.push(`-${filePath}:${lineNum}: ${lines[idx]}`);
570
+ }
571
+ }
572
+ return results;
169
573
  }
170
- catch (e) {
171
- return `failed: ${e}`;
574
+ function collectFiles(dirPath) {
575
+ const files = [];
576
+ function walk(dir) {
577
+ let entries;
578
+ try {
579
+ entries = fs.readdirSync(dir, { withFileTypes: true });
580
+ }
581
+ catch {
582
+ return;
583
+ }
584
+ for (const entry of entries) {
585
+ const fullPath = path.join(dir, entry.name);
586
+ if (entry.isDirectory()) {
587
+ walk(fullPath);
588
+ }
589
+ else if (entry.isFile()) {
590
+ files.push(fullPath);
591
+ }
592
+ }
593
+ }
594
+ walk(dirPath);
595
+ return files.sort();
172
596
  }
597
+ function matchesGlob(filePath, glob) {
598
+ const escaped = glob
599
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
600
+ .replace(/\*/g, ".*")
601
+ .replace(/\?/g, ".");
602
+ return new RegExp(`(^|/)${escaped}$`).test(filePath);
603
+ }
604
+ const stat = fs.statSync(targetPath, { throwIfNoEntry: false });
605
+ if (!stat)
606
+ return "";
607
+ if (stat.isFile()) {
608
+ return searchFile(targetPath).join("\n");
609
+ }
610
+ let files = collectFiles(targetPath);
611
+ if (includeGlob) {
612
+ files = files.filter((f) => matchesGlob(f, includeGlob));
613
+ }
614
+ const allResults = [];
615
+ for (const file of files) {
616
+ allResults.push(...searchFile(file));
617
+ }
618
+ return allResults.join("\n");
173
619
  },
620
+ summaryKeys: ["pattern", "path", "include"],
174
621
  },
175
622
  {
176
623
  tool: {
177
624
  type: "function",
178
625
  function: {
179
- name: "gh_cli",
180
- description: "execute a GitHub CLI (gh) command. use carefully.",
626
+ name: "shell",
627
+ description: "Run a shell command and return stdout/stderr. Working directory persists between calls. Use dedicated tools instead of shell when available: read_file instead of cat, edit_file instead of sed, glob instead of find, grep instead of grep/rg. Reserve shell for operations that genuinely need the shell: installing packages, running builds/tests, git operations, process management. Be careful with destructive commands -- consider reversibility before running. If a command fails, read the error output before retrying with a different approach.",
181
628
  parameters: {
182
629
  type: "object",
183
630
  properties: {
184
631
  command: { type: "string" },
632
+ timeout_ms: {
633
+ type: "number",
634
+ description: "Timeout in milliseconds. Default: 30000. Max: 600000.",
635
+ },
636
+ background: {
637
+ type: "boolean",
638
+ description: "Run in background. Returns immediately with a process ID. Use shell_status/shell_tail to monitor.",
639
+ },
185
640
  },
186
641
  required: ["command"],
187
642
  },
188
643
  },
189
644
  },
190
645
  handler: (a) => {
646
+ // Destructive pattern detection (friction, not a block)
647
+ const destructivePatterns = (0, shell_sessions_1.detectDestructivePatterns)(a.command);
648
+ if (destructivePatterns.length > 0) {
649
+ (0, runtime_1.emitNervesEvent)({
650
+ level: "warn",
651
+ event: "tool.shell.destructive_detected",
652
+ component: "tools",
653
+ message: `destructive pattern detected: ${destructivePatterns.join(", ")}`,
654
+ meta: { command: a.command, patterns: destructivePatterns },
655
+ });
656
+ }
657
+ // Background mode: spawn and return immediately
658
+ if (a.background === "true") {
659
+ const session = (0, shell_sessions_1.spawnBackgroundShell)(a.command);
660
+ return JSON.stringify({ id: session.id, command: session.command, status: session.status });
661
+ }
662
+ const MAX_TIMEOUT = 600000;
663
+ const requestedTimeout = Number(a.timeout_ms) || 0;
664
+ let configDefault = 30000;
191
665
  try {
192
- return (0, child_process_1.execSync)(`gh ${a.command}`, { encoding: "utf-8", timeout: 60000 });
666
+ configDefault = (0, identity_1.loadAgentConfig)().shell?.defaultTimeout ?? 30000;
193
667
  }
194
- catch (e) {
195
- return `error: ${e}`;
668
+ catch { /* test env: no --agent flag */ }
669
+ const baseTimeout = requestedTimeout > 0 ? requestedTimeout : configDefault;
670
+ const timeout = Math.min(baseTimeout, MAX_TIMEOUT);
671
+ const output = (0, child_process_1.execSync)(a.command, {
672
+ encoding: "utf-8",
673
+ timeout,
674
+ });
675
+ if (destructivePatterns.length > 0) {
676
+ return `${output}\n\n--- destructive pattern detected: ${destructivePatterns.join(", ")} ---`;
196
677
  }
678
+ return output;
679
+ },
680
+ summaryKeys: ["command"],
681
+ },
682
+ {
683
+ tool: {
684
+ type: "function",
685
+ function: {
686
+ name: "shell_status",
687
+ description: "Check status of background shell processes. Omit id to list all.",
688
+ parameters: {
689
+ type: "object",
690
+ properties: {
691
+ id: { type: "string", description: "Background shell process ID" },
692
+ },
693
+ },
694
+ },
695
+ },
696
+ handler: (a) => {
697
+ if (!a.id) {
698
+ return JSON.stringify((0, shell_sessions_1.listShellSessions)());
699
+ }
700
+ const session = (0, shell_sessions_1.getShellSession)(a.id);
701
+ if (!session)
702
+ return `process not found: ${a.id}`;
703
+ return JSON.stringify(session);
704
+ },
705
+ summaryKeys: ["id"],
706
+ },
707
+ {
708
+ tool: {
709
+ type: "function",
710
+ function: {
711
+ name: "shell_tail",
712
+ description: "Show recent output from a background shell process.",
713
+ parameters: {
714
+ type: "object",
715
+ properties: {
716
+ id: { type: "string", description: "Background shell process ID" },
717
+ },
718
+ required: ["id"],
719
+ },
720
+ },
721
+ },
722
+ handler: (a) => {
723
+ /* v8 ignore next -- schema requires id, defensive guard @preserve */
724
+ if (!a.id)
725
+ return "id is required";
726
+ const output = (0, shell_sessions_1.tailShellSession)(a.id);
727
+ if (output === undefined)
728
+ return `process not found: ${a.id}`;
729
+ return output || "(no output yet)";
197
730
  },
731
+ summaryKeys: ["id"],
198
732
  },
199
733
  {
200
734
  tool: {
@@ -228,27 +762,14 @@ exports.baseToolDefinitions = [
228
762
  return `error: ${e}`;
229
763
  }
230
764
  },
231
- },
232
- {
233
- tool: {
234
- type: "function",
235
- function: {
236
- name: "get_current_time",
237
- description: "get the current date and time in America/Los_Angeles (Pacific Time)",
238
- parameters: { type: "object", properties: {} },
239
- },
240
- },
241
- handler: () => new Date().toLocaleString("en-US", {
242
- timeZone: "America/Los_Angeles",
243
- hour12: false,
244
- }),
765
+ summaryKeys: ["name"],
245
766
  },
246
767
  {
247
768
  tool: {
248
769
  type: "function",
249
770
  function: {
250
771
  name: "claude",
251
- description: "use claude code to query this codebase or get an outside perspective. useful for code review, second opinions, and asking questions about your own source.",
772
+ description: "use claude code to query this codebase or get an outside perspective. Use for code review, second opinions, or questions that benefit from a fresh perspective outside this conversation's context.",
252
773
  parameters: {
253
774
  type: "object",
254
775
  properties: { prompt: { type: "string" } },
@@ -273,6 +794,7 @@ exports.baseToolDefinitions = [
273
794
  return `error: ${e}`;
274
795
  }
275
796
  },
797
+ summaryKeys: ["prompt"],
276
798
  },
277
799
  {
278
800
  tool: {
@@ -313,13 +835,14 @@ exports.baseToolDefinitions = [
313
835
  return `error: ${e}`;
314
836
  }
315
837
  },
838
+ summaryKeys: ["query"],
316
839
  },
317
840
  {
318
841
  tool: {
319
842
  type: "function",
320
843
  function: {
321
- name: "memory_search",
322
- description: "search remembered facts stored in psyche memory and return relevant matches for a query",
844
+ name: "recall",
845
+ description: "Search my diary and journal for facts, thoughts, and working notes matching a query. Uses semantic similarity -- phrasing matters. Try different angles if the first query doesn't find what you're looking for. Check recall before asking the human something you might already know.",
323
846
  parameters: {
324
847
  type: "object",
325
848
  properties: { query: { type: "string" } },
@@ -332,44 +855,69 @@ exports.baseToolDefinitions = [
332
855
  const query = (a.query || "").trim();
333
856
  if (!query)
334
857
  return "query is required";
335
- const memoryRoot = path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
336
- const hits = await (0, memory_1.searchMemoryFacts)(query, (0, memory_1.readMemoryFacts)(memoryRoot));
337
- return hits
338
- .map((fact) => `- ${fact.text} (source=${fact.source}, createdAt=${fact.createdAt})`)
339
- .join("\n");
858
+ const resultLines = [];
859
+ // Search diary entries
860
+ const hits = await (0, diary_1.searchDiaryEntries)(query, (0, diary_1.readDiaryEntries)());
861
+ for (const fact of hits) {
862
+ resultLines.push(`[diary] ${fact.text} (source=${fact.source}, createdAt=${fact.createdAt})`);
863
+ }
864
+ // Search journal index
865
+ const agentRoot = (0, identity_1.getAgentRoot)();
866
+ const journalIndexPath = path.join(agentRoot, "journal", ".index.json");
867
+ try {
868
+ const raw = fs.readFileSync(journalIndexPath, "utf8");
869
+ const journalEntries = JSON.parse(raw);
870
+ if (Array.isArray(journalEntries) && journalEntries.length > 0) {
871
+ // Substring match on preview and filename
872
+ const lowerQuery = query.toLowerCase();
873
+ for (const entry of journalEntries) {
874
+ /* v8 ignore next 4 -- both sides tested (filename-only match in recall-journal.test.ts); v8 misreports || short-circuit @preserve */
875
+ if (entry.preview.toLowerCase().includes(lowerQuery) ||
876
+ entry.filename.toLowerCase().includes(lowerQuery)) {
877
+ resultLines.push(`[journal] ${entry.filename}: ${entry.preview}`);
878
+ }
879
+ }
880
+ }
881
+ }
882
+ catch {
883
+ // No journal index or malformed — skip journal search
884
+ }
885
+ return resultLines.join("\n");
340
886
  }
341
887
  catch (e) {
342
888
  return `error: ${e instanceof Error ? e.message : String(e)}`;
343
889
  }
344
890
  },
891
+ summaryKeys: ["query"],
345
892
  },
346
893
  {
347
894
  tool: {
348
895
  type: "function",
349
896
  function: {
350
- name: "memory_save",
351
- description: "save a general memory fact i want to recall later. optional 'about' can tag the fact to a person/topic/context",
897
+ name: "diary_write",
898
+ description: "Write an entry in my diary -- something I learned, noticed, or concluded that I want to recall later. Use 'about' to tag the entry to a person, topic, or context. Write for my future self: include enough context that the entry makes sense without the surrounding conversation. Prefer durable conclusions over passing noise. Don't duplicate what already belongs in friend notes.",
352
899
  parameters: {
353
900
  type: "object",
354
901
  properties: {
355
- text: { type: "string" },
902
+ entry: { type: "string" },
356
903
  about: { type: "string" },
357
904
  },
358
- required: ["text"],
905
+ required: ["entry"],
359
906
  },
360
907
  },
361
908
  },
362
909
  handler: async (a) => {
363
- const text = (a.text || "").trim();
364
- if (!text)
365
- return "text is required";
366
- const result = await (0, memory_1.saveMemoryFact)({
367
- text,
368
- source: "tool:memory_save",
910
+ const entry = (a.entry || "").trim();
911
+ if (!entry)
912
+ return "entry is required";
913
+ const result = await (0, diary_1.saveDiaryEntry)({
914
+ text: entry,
915
+ source: "tool:diary_write",
369
916
  about: typeof a.about === "string" ? a.about : undefined,
370
917
  });
371
- return `saved memory fact (added=${result.added}, skipped=${result.skipped})`;
918
+ return `saved diary entry (added=${result.added}, skipped=${result.skipped})`;
372
919
  },
920
+ summaryKeys: ["entry", "about"],
373
921
  },
374
922
  {
375
923
  tool: {
@@ -397,382 +945,859 @@ exports.baseToolDefinitions = [
397
945
  return `friend not found: ${friendId}`;
398
946
  return JSON.stringify(friend, null, 2);
399
947
  },
948
+ summaryKeys: ["friendId"],
400
949
  },
401
950
  {
402
951
  tool: {
403
952
  type: "function",
404
953
  function: {
405
- name: "task_board",
406
- description: "show the task board grouped by status",
407
- parameters: { type: "object", properties: {} },
954
+ name: "save_friend_note",
955
+ description: "save something i learned about my friend. use type 'name' to update their display name, 'tool_preference' for how they like a specific tool to behave (key = tool category like 'ado', 'graph'), or 'note' for general knowledge (key = topic). when updating an existing value, set override to true if i'm replacing/correcting it. omit override (or set false) if i'm unsure and want to check what's already saved.",
956
+ parameters: {
957
+ type: "object",
958
+ properties: {
959
+ type: { type: "string", enum: ["name", "tool_preference", "note"], description: "what kind of information to save" },
960
+ key: { type: "string", description: "category key (required for tool_preference and note, e.g. 'ado', 'role')" },
961
+ content: { type: "string", description: "the value to save" },
962
+ override: { type: "string", enum: ["true", "false"], description: "set to 'true' to overwrite an existing value" },
963
+ },
964
+ required: ["type", "content"],
965
+ },
408
966
  },
409
967
  },
410
- handler: () => {
411
- const board = (0, tasks_1.getTaskModule)().getBoard();
412
- return board.full || board.compact || "no tasks found";
968
+ handler: async (a, ctx) => {
969
+ (0, runtime_1.emitNervesEvent)({
970
+ component: "repertoire",
971
+ event: "repertoire.save_friend_note",
972
+ message: "save friend note invoked",
973
+ meta: { type: a.type },
974
+ });
975
+ if (!ctx?.context) {
976
+ return "i can't save notes -- no friend context available";
977
+ }
978
+ if (!ctx.friendStore) {
979
+ return "i can't save notes -- friend store not available";
980
+ }
981
+ const friendId = ctx.context.friend?.id;
982
+ if (!friendId)
983
+ return "i can't save notes -- no friend identity available";
984
+ // Validate parameters
985
+ if (!a.content)
986
+ return "i need a content value to save";
987
+ const validTypes = ["name", "tool_preference", "note"];
988
+ if (!validTypes.includes(a.type))
989
+ return `i don't recognize type '${a.type}' -- use name, tool_preference, or note`;
990
+ if ((a.type === "tool_preference" || a.type === "note") && !a.key)
991
+ return "i need a key for tool_preference or note type";
992
+ try {
993
+ // Read fresh record from disk
994
+ const record = await ctx.friendStore.get(friendId);
995
+ if (!record)
996
+ return "i can't find the friend record on disk";
997
+ const isOverride = a.override === "true";
998
+ if (a.type === "name") {
999
+ const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
1000
+ await ctx.friendStore.put(friendId, updated);
1001
+ return `saved: name = ${a.content}`;
1002
+ }
1003
+ if (a.type === "tool_preference") {
1004
+ const existing = record.toolPreferences[a.key];
1005
+ if (existing && !isOverride) {
1006
+ return `i already have a preference for '${a.key}': "${existing}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
1007
+ }
1008
+ const updated = { ...record, toolPreferences: { ...record.toolPreferences, [a.key]: a.content }, updatedAt: new Date().toISOString() };
1009
+ await ctx.friendStore.put(friendId, updated);
1010
+ return `saved: toolPreference ${a.key} = ${a.content}`;
1011
+ }
1012
+ // type === "note"
1013
+ // Redirect "name" key to name field
1014
+ if (a.key === "name") {
1015
+ const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
1016
+ await ctx.friendStore.put(friendId, updated);
1017
+ return `updated friend's name to '${a.content}' (stored as name, not a note)`;
1018
+ }
1019
+ const existing = record.notes[a.key];
1020
+ if (existing && !isOverride) {
1021
+ return `i already have a note for '${a.key}': "${existing.value}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
1022
+ }
1023
+ const updated = { ...record, notes: { ...record.notes, [a.key]: { value: a.content, savedAt: new Date().toISOString() } }, updatedAt: new Date().toISOString() };
1024
+ await ctx.friendStore.put(friendId, updated);
1025
+ return `saved: note ${a.key} = ${a.content}`;
1026
+ }
1027
+ catch (err) {
1028
+ /* v8 ignore next -- defensive: non-Error branch for String(err) @preserve */
1029
+ return `error saving note: ${err instanceof Error ? err.message : String(err)}`;
1030
+ }
413
1031
  },
1032
+ summaryKeys: ["type", "key", "content"],
414
1033
  },
1034
+ // -- cross-session awareness --
415
1035
  {
416
1036
  tool: {
417
1037
  type: "function",
418
1038
  function: {
419
- name: "task_create",
420
- description: "create a new task in the bundle task system. optionally set `scheduledAt` for a one-time reminder or `cadence` for recurring daemon-scheduled work.",
1039
+ name: "bridge_manage",
1040
+ description: "create and manage shared live-work bridges across already-active sessions.",
421
1041
  parameters: {
422
1042
  type: "object",
423
1043
  properties: {
424
- title: { type: "string" },
425
- type: { type: "string", enum: ["one-shot", "ongoing", "habit"] },
426
- category: { type: "string" },
427
- body: { type: "string" },
428
- status: { type: "string" },
429
- validator: { type: "string" },
430
- requester: { type: "string" },
431
- scheduledAt: { type: "string", description: "ISO timestamp for a one-time scheduled run/reminder" },
432
- cadence: { type: "string", description: "recurrence like 30m, 1h, 1d, or cron" },
1044
+ action: {
1045
+ type: "string",
1046
+ enum: ["begin", "attach", "status", "promote_task", "complete", "cancel"],
1047
+ },
1048
+ bridgeId: { type: "string", description: "bridge id for all actions except begin" },
1049
+ objective: { type: "string", description: "objective for begin" },
1050
+ summary: { type: "string", description: "optional concise shared-work summary" },
1051
+ friendId: { type: "string", description: "target friend id for attach" },
1052
+ channel: { type: "string", description: "target channel for attach" },
1053
+ key: { type: "string", description: "target session key for attach (defaults to 'session')" },
1054
+ title: { type: "string", description: "task title override for promote_task" },
1055
+ category: { type: "string", description: "task category override for promote_task" },
1056
+ body: { type: "string", description: "task body override for promote_task" },
433
1057
  },
434
- required: ["title", "type", "category", "body"],
1058
+ required: ["action"],
435
1059
  },
436
1060
  },
437
1061
  },
438
- handler: (a) => {
439
- try {
440
- const created = (0, tasks_1.getTaskModule)().createTask(buildTaskCreateInput(a));
441
- return `created: ${created}`;
1062
+ handler: async (args, ctx) => {
1063
+ const manager = (0, manager_1.createBridgeManager)();
1064
+ const action = (args.action || "").trim();
1065
+ if (action === "begin") {
1066
+ if (!ctx?.currentSession) {
1067
+ return "bridge_manage begin requires an active session context.";
1068
+ }
1069
+ const objective = (args.objective || "").trim();
1070
+ if (!objective)
1071
+ return "objective is required for bridge begin.";
1072
+ return (0, manager_1.formatBridgeStatus)(manager.beginBridge({
1073
+ objective,
1074
+ summary: (args.summary || objective).trim(),
1075
+ session: ctx.currentSession,
1076
+ }));
1077
+ }
1078
+ const bridgeId = (args.bridgeId || "").trim();
1079
+ if (!bridgeId) {
1080
+ return "bridgeId is required for this bridge action.";
1081
+ }
1082
+ if (action === "attach") {
1083
+ const friendId = (args.friendId || "").trim();
1084
+ const channel = (args.channel || "").trim();
1085
+ const key = (args.key || "session").trim();
1086
+ if (!friendId || !channel) {
1087
+ return "friendId and channel are required for bridge attach.";
1088
+ }
1089
+ const sessionPath = (0, config_1.resolveSessionPath)(friendId, channel, key);
1090
+ const recall = await recallSessionSafely({
1091
+ sessionPath,
1092
+ friendId,
1093
+ channel,
1094
+ key,
1095
+ messageCount: 20,
1096
+ trustLevel: ctx?.context?.friend?.trustLevel,
1097
+ summarize: ctx?.summarize,
1098
+ });
1099
+ if (recall.kind === "missing") {
1100
+ return NO_SESSION_FOUND_MESSAGE;
1101
+ }
1102
+ return (0, manager_1.formatBridgeStatus)(manager.attachSession(bridgeId, {
1103
+ friendId,
1104
+ channel,
1105
+ key,
1106
+ sessionPath,
1107
+ snapshot: recall.kind === "ok" ? recall.snapshot : EMPTY_SESSION_MESSAGE,
1108
+ }));
1109
+ }
1110
+ if (action === "status") {
1111
+ const bridge = manager.getBridge(bridgeId);
1112
+ if (!bridge)
1113
+ return `bridge not found: ${bridgeId}`;
1114
+ return (0, manager_1.formatBridgeStatus)(bridge);
442
1115
  }
443
- catch (error) {
444
- return `error: ${error instanceof Error ? error.message : String(error)}`;
1116
+ if (action === "promote_task") {
1117
+ return (0, manager_1.formatBridgeStatus)(manager.promoteBridgeToTask(bridgeId, {
1118
+ title: args.title,
1119
+ category: args.category,
1120
+ body: args.body,
1121
+ }));
445
1122
  }
1123
+ if (action === "complete") {
1124
+ return (0, manager_1.formatBridgeStatus)(manager.completeBridge(bridgeId));
1125
+ }
1126
+ if (action === "cancel") {
1127
+ return (0, manager_1.formatBridgeStatus)(manager.cancelBridge(bridgeId));
1128
+ }
1129
+ return `unknown bridge action: ${action}`;
446
1130
  },
1131
+ summaryKeys: ["action", "bridgeId", "objective", "friendId", "channel", "key"],
447
1132
  },
448
1133
  {
449
1134
  tool: {
450
1135
  type: "function",
451
1136
  function: {
452
- name: "schedule_reminder",
453
- description: "create a scheduled reminder or recurring daemon job. use `scheduledAt` for one-time reminders and `cadence` for recurring reminders. this writes canonical task fields that the daemon reconciles into OS-level jobs.",
1137
+ name: "query_active_work",
1138
+ 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.",
1139
+ parameters: {
1140
+ type: "object",
1141
+ properties: {},
1142
+ },
1143
+ },
1144
+ },
1145
+ handler: async (_args, ctx) => {
1146
+ const frame = await buildToolActiveWorkFrame(ctx);
1147
+ 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)}`;
1148
+ },
1149
+ },
1150
+ {
1151
+ tool: {
1152
+ type: "function",
1153
+ function: {
1154
+ name: "query_session",
1155
+ description: "inspect another session. use transcript for recent context, status for self/inner progress, or search to find older history by query.",
454
1156
  parameters: {
455
1157
  type: "object",
456
1158
  properties: {
457
- title: { type: "string" },
458
- body: { type: "string" },
459
- category: { type: "string" },
460
- scheduledAt: { type: "string", description: "ISO timestamp for a one-time reminder" },
461
- cadence: { type: "string", description: "recurrence like 30m, 1h, 1d, or cron" },
1159
+ friendId: { type: "string", description: "the friend UUID (or 'self')" },
1160
+ channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
1161
+ key: { type: "string", description: "session key (defaults to 'session')" },
1162
+ messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
1163
+ mode: {
1164
+ type: "string",
1165
+ enum: ["transcript", "status", "search"],
1166
+ description: "transcript (default), lightweight status for self/inner checks, or search for older history",
1167
+ },
1168
+ query: { type: "string", description: "required when mode=search; search term for older session history" },
462
1169
  },
463
- required: ["title", "body"],
1170
+ required: ["friendId", "channel"],
464
1171
  },
465
1172
  },
466
1173
  },
467
- handler: (a) => {
468
- const scheduledAt = normalizeOptionalText(a.scheduledAt);
469
- const cadence = normalizeOptionalText(a.cadence);
470
- if (!scheduledAt && !cadence) {
471
- return "error: provide scheduledAt or cadence";
1174
+ handler: async (args, ctx) => {
1175
+ let friendId = args.friendId;
1176
+ const channel = args.channel;
1177
+ const key = args.key || "session";
1178
+ const count = parseInt(args.messageCount || "20", 10);
1179
+ const mode = args.mode || "transcript";
1180
+ // Resolve friend name → UUID if not already a UUID or "self"
1181
+ if (friendId && friendId !== "self" && !/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId) && ctx?.friendStore?.listAll) {
1182
+ const allFriends = await ctx.friendStore.listAll();
1183
+ const match = allFriends.find(f => f.name.toLowerCase() === friendId.toLowerCase());
1184
+ if (match) {
1185
+ friendId = match.id;
1186
+ }
472
1187
  }
473
- try {
474
- const created = (0, tasks_1.getTaskModule)().createTask({
475
- title: a.title,
476
- type: cadence ? "habit" : "one-shot",
477
- category: normalizeOptionalText(a.category) ?? "reminder",
478
- body: a.body,
479
- scheduledAt,
480
- cadence,
1188
+ if (mode === "status") {
1189
+ if (friendId !== "self" || channel !== "inner") {
1190
+ return "status mode is only available for self/inner dialog.";
1191
+ }
1192
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)((0, identity_1.getAgentRoot)());
1193
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
1194
+ return renderInnerProgressStatus((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
1195
+ }
1196
+ if (mode === "search") {
1197
+ const query = (args.query || "").trim();
1198
+ if (!query) {
1199
+ return "search mode requires a non-empty query.";
1200
+ }
1201
+ const search = await searchSessionSafely({
1202
+ sessionPath: (0, config_1.resolveSessionPath)(friendId, channel, key),
1203
+ friendId,
1204
+ channel,
1205
+ key,
1206
+ query,
481
1207
  });
482
- return `created: ${created}`;
1208
+ if (search.kind === "missing") {
1209
+ return NO_SESSION_FOUND_MESSAGE;
1210
+ }
1211
+ if (search.kind === "empty") {
1212
+ return EMPTY_SESSION_MESSAGE;
1213
+ }
1214
+ if (search.kind === "no_match") {
1215
+ return `no matches for "${search.query}" in that session.\n\n${search.snapshot}`;
1216
+ }
1217
+ return [
1218
+ `history search: "${search.query}"`,
1219
+ search.snapshot,
1220
+ ...search.matches.map((match, index) => `match ${index + 1}\n${match}`),
1221
+ ].join("\n\n");
483
1222
  }
484
- catch (error) {
485
- return `error: ${error instanceof Error ? error.message : String(error)}`;
1223
+ const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
1224
+ const recall = await recallSessionSafely({
1225
+ sessionPath: sessFile,
1226
+ friendId,
1227
+ channel,
1228
+ key,
1229
+ messageCount: count,
1230
+ trustLevel: ctx?.context?.friend?.trustLevel,
1231
+ summarize: ctx?.summarize,
1232
+ });
1233
+ if (recall.kind === "missing") {
1234
+ return NO_SESSION_FOUND_MESSAGE;
486
1235
  }
1236
+ if (recall.kind === "empty") {
1237
+ return EMPTY_SESSION_MESSAGE;
1238
+ }
1239
+ return recall.summary;
487
1240
  },
488
1241
  },
489
1242
  {
490
1243
  tool: {
491
1244
  type: "function",
492
1245
  function: {
493
- name: "task_update_status",
494
- description: "update a task status using validated transitions",
1246
+ name: "send_message",
1247
+ 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.",
495
1248
  parameters: {
496
1249
  type: "object",
497
1250
  properties: {
498
- name: { type: "string" },
499
- status: { type: "string" },
1251
+ friendId: { type: "string", description: "the friend UUID (or 'self')" },
1252
+ channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
1253
+ key: { type: "string", description: "session key (defaults to 'session')" },
1254
+ content: { type: "string", description: "the message content to send" },
500
1255
  },
501
- required: ["name", "status"],
1256
+ required: ["friendId", "channel", "content"],
502
1257
  },
503
1258
  },
504
1259
  },
505
- handler: (a) => {
506
- const result = (0, tasks_1.getTaskModule)().updateStatus(a.name, a.status);
507
- if (!result.ok) {
508
- return `error: ${result.reason ?? "status update failed"}`;
1260
+ handler: async (args, ctx) => {
1261
+ const friendId = args.friendId;
1262
+ const channel = args.channel;
1263
+ const key = args.key || "session";
1264
+ const content = args.content;
1265
+ const now = Date.now();
1266
+ const agentName = (0, identity_1.getAgentName)();
1267
+ // Self-routing: messages to "self" always go to inner dialog pending dir,
1268
+ // regardless of the channel or key the agent specified.
1269
+ const isSelf = friendId === "self";
1270
+ const pendingDir = isSelf
1271
+ ? (0, pending_1.getInnerDialogPendingDir)(agentName)
1272
+ : (0, pending_1.getPendingDir)(agentName, friendId, channel, key);
1273
+ const delegatingBridgeId = findDelegatingBridgeId(ctx);
1274
+ const delegatedFrom = isSelf
1275
+ && ctx?.currentSession
1276
+ && !(ctx.currentSession.friendId === "self" && ctx.currentSession.channel === "inner")
1277
+ ? {
1278
+ friendId: ctx.currentSession.friendId,
1279
+ channel: ctx.currentSession.channel,
1280
+ key: ctx.currentSession.key,
1281
+ ...(delegatingBridgeId ? { bridgeId: delegatingBridgeId } : {}),
1282
+ }
1283
+ : undefined;
1284
+ const obligationId = delegatedFrom ? (0, obligations_1.generateObligationId)(now) : undefined;
1285
+ const envelope = {
1286
+ from: agentName,
1287
+ friendId,
1288
+ channel,
1289
+ key,
1290
+ content,
1291
+ timestamp: now,
1292
+ ...(delegatedFrom ? { delegatedFrom, obligationStatus: "pending" } : {}),
1293
+ ...(obligationId ? { obligationId } : {}),
1294
+ };
1295
+ if (isSelf) {
1296
+ writePendingEnvelope(pendingDir, envelope);
1297
+ if (delegatedFrom) {
1298
+ try {
1299
+ (0, obligations_2.createObligation)((0, identity_1.getAgentRoot)(), {
1300
+ origin: {
1301
+ friendId: delegatedFrom.friendId,
1302
+ channel: delegatedFrom.channel,
1303
+ key: delegatedFrom.key,
1304
+ },
1305
+ ...(delegatedFrom.bridgeId ? { bridgeId: delegatedFrom.bridgeId } : {}),
1306
+ content,
1307
+ });
1308
+ }
1309
+ catch {
1310
+ /* v8 ignore next -- defensive: obligation store write failure should not break send_message @preserve */
1311
+ }
1312
+ /* v8 ignore next -- obligationId always set when delegatedFrom is set (see generateObligationId above) @preserve */
1313
+ if (obligationId) {
1314
+ (0, obligations_1.createObligation)(agentName, {
1315
+ id: obligationId,
1316
+ origin: delegatedFrom,
1317
+ status: "queued",
1318
+ delegatedContent: content.length > 120 ? `${content.slice(0, 117)}...` : content,
1319
+ createdAt: now,
1320
+ });
1321
+ }
1322
+ (0, runtime_1.emitNervesEvent)({
1323
+ event: "repertoire.obligation_created",
1324
+ component: "repertoire",
1325
+ message: "obligation created for inner dialog delegation",
1326
+ meta: {
1327
+ friendId: delegatedFrom.friendId,
1328
+ channel: delegatedFrom.channel,
1329
+ key: delegatedFrom.key,
1330
+ },
1331
+ });
1332
+ }
1333
+ let wakeResponse = null;
1334
+ try {
1335
+ wakeResponse = await (0, socket_client_1.requestInnerWake)(agentName);
1336
+ }
1337
+ catch {
1338
+ wakeResponse = null;
1339
+ }
1340
+ if (!wakeResponse?.ok) {
1341
+ const { runInnerDialogTurn } = await Promise.resolve().then(() => __importStar(require("../senses/inner-dialog")));
1342
+ if (ctx?.context?.channel.channel === "inner") {
1343
+ queueMicrotask(() => {
1344
+ void runInnerDialogTurn({ reason: "instinct" });
1345
+ });
1346
+ return renderInnerProgressStatus({
1347
+ queue: "queued to inner/dialog",
1348
+ wake: "inline scheduled",
1349
+ processing: "pending",
1350
+ surfaced: "nothing yet",
1351
+ });
1352
+ }
1353
+ else {
1354
+ const turnResult = await runInnerDialogTurn({ reason: "instinct" });
1355
+ const surfacedPreview = normalizeProgressOutcome((0, thoughts_1.formatSurfacedValue)((0, thoughts_1.extractThoughtResponseFromMessages)(turnResult?.messages ?? [])));
1356
+ return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
1357
+ scope: "inner-delegation",
1358
+ phase: "completed",
1359
+ objective: "queued to inner/dialog",
1360
+ outcomeText: `wake: inline fallback\n${surfacedPreview}`,
1361
+ }));
1362
+ }
1363
+ }
1364
+ return renderInnerProgressStatus({
1365
+ queue: "queued to inner/dialog",
1366
+ wake: "daemon requested",
1367
+ processing: "pending",
1368
+ surfaced: "nothing yet",
1369
+ });
1370
+ }
1371
+ const deliveryResult = await (0, cross_chat_delivery_1.deliverCrossChatMessage)({
1372
+ friendId,
1373
+ channel,
1374
+ key,
1375
+ content,
1376
+ intent: ctx?.currentSession && ctx.currentSession.friendId !== "self"
1377
+ ? "explicit_cross_chat"
1378
+ : "generic_outreach",
1379
+ ...(ctx?.currentSession && ctx.currentSession.friendId !== "self"
1380
+ ? {
1381
+ authorizingSession: {
1382
+ friendId: ctx.currentSession.friendId,
1383
+ channel: ctx.currentSession.channel,
1384
+ key: ctx.currentSession.key,
1385
+ trustLevel: ctx?.context?.friend?.trustLevel,
1386
+ },
1387
+ }
1388
+ : {}),
1389
+ }, {
1390
+ agentName,
1391
+ queuePending: (message) => writePendingEnvelope(pendingDir, message),
1392
+ deliverers: {
1393
+ bluebubbles: async (request) => {
1394
+ const { sendProactiveBlueBubblesMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/bluebubbles")));
1395
+ const result = await sendProactiveBlueBubblesMessageToSession({
1396
+ friendId: request.friendId,
1397
+ sessionKey: request.key,
1398
+ text: request.content,
1399
+ intent: request.intent,
1400
+ authorizingSession: request.authorizingSession,
1401
+ });
1402
+ if (result.delivered) {
1403
+ return {
1404
+ status: "delivered_now",
1405
+ detail: "sent to the active bluebubbles chat now",
1406
+ };
1407
+ }
1408
+ if (result.reason === "missing_target") {
1409
+ return {
1410
+ status: "blocked",
1411
+ detail: "bluebubbles could not resolve a routable target for that session",
1412
+ };
1413
+ }
1414
+ if (result.reason === "send_error") {
1415
+ return {
1416
+ status: "failed",
1417
+ detail: "bluebubbles send failed",
1418
+ };
1419
+ }
1420
+ return {
1421
+ status: "unavailable",
1422
+ detail: "live delivery unavailable right now; queued for the next active turn",
1423
+ };
1424
+ },
1425
+ teams: async (request) => {
1426
+ if (!ctx?.botApi) {
1427
+ return {
1428
+ status: "unavailable",
1429
+ detail: "live delivery unavailable right now; queued for the next active turn",
1430
+ };
1431
+ }
1432
+ const { sendProactiveTeamsMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/teams")));
1433
+ const result = await sendProactiveTeamsMessageToSession({
1434
+ friendId: request.friendId,
1435
+ sessionKey: request.key,
1436
+ text: request.content,
1437
+ intent: request.intent,
1438
+ authorizingSession: request.authorizingSession,
1439
+ }, {
1440
+ botApi: ctx.botApi,
1441
+ });
1442
+ if (result.delivered) {
1443
+ return {
1444
+ status: "delivered_now",
1445
+ detail: "sent to the active teams chat now",
1446
+ };
1447
+ }
1448
+ if (result.reason === "missing_target") {
1449
+ return {
1450
+ status: "blocked",
1451
+ detail: "teams could not resolve a routable target for that session",
1452
+ };
1453
+ }
1454
+ if (result.reason === "send_error") {
1455
+ return {
1456
+ status: "failed",
1457
+ detail: "teams send failed",
1458
+ };
1459
+ }
1460
+ return {
1461
+ status: "unavailable",
1462
+ detail: "live delivery unavailable right now; queued for the next active turn",
1463
+ };
1464
+ },
1465
+ },
1466
+ });
1467
+ return renderCrossChatDeliveryStatus(`${friendId} on ${channel}/${key}`, deliveryResult);
1468
+ },
1469
+ },
1470
+ {
1471
+ tool: {
1472
+ type: "function",
1473
+ function: {
1474
+ name: "set_reasoning_effort",
1475
+ description: "adjust your own reasoning depth for subsequent turns. use higher effort for complex analysis, lower for simple tasks.",
1476
+ parameters: {
1477
+ type: "object",
1478
+ properties: {
1479
+ level: { type: "string", description: "the reasoning effort level to set" },
1480
+ },
1481
+ required: ["level"],
1482
+ },
1483
+ },
1484
+ },
1485
+ handler: (args, ctx) => {
1486
+ if (!ctx?.supportedReasoningEfforts || !ctx.setReasoningEffort) {
1487
+ return "reasoning effort adjustment is not available in this context.";
1488
+ }
1489
+ const level = (args.level || "").trim();
1490
+ if (!ctx.supportedReasoningEfforts.includes(level)) {
1491
+ return `invalid reasoning effort level "${level}". accepted levels: ${ctx.supportedReasoningEfforts.join(", ")}`;
509
1492
  }
510
- const archivedSuffix = result.archived && result.archived.length > 0
511
- ? ` | archived: ${result.archived.join(", ")}`
512
- : "";
513
- return `updated: ${a.name} -> ${result.to}${archivedSuffix}`;
1493
+ ctx.setReasoningEffort(level);
1494
+ (0, runtime_1.emitNervesEvent)({
1495
+ component: "repertoire",
1496
+ event: "repertoire.reasoning_effort_changed",
1497
+ message: `reasoning effort set to ${level}`,
1498
+ meta: { level },
1499
+ });
1500
+ return `reasoning effort set to "${level}".`;
514
1501
  },
1502
+ requiredCapability: "reasoning-effort",
1503
+ summaryKeys: ["level"],
515
1504
  },
1505
+ // ── Continuity tools ──────────────────────────────────────────────
516
1506
  {
517
1507
  tool: {
518
1508
  type: "function",
519
1509
  function: {
520
- name: "task_board_status",
521
- description: "show board detail for a specific status",
1510
+ name: "query_episodes",
1511
+ description: "Query recent episodes from my continuity memory. Returns timestamped records of significant events (obligation shifts, coding milestones, bridge events, care events, turning points).",
522
1512
  parameters: {
523
1513
  type: "object",
524
1514
  properties: {
525
- status: { type: "string" },
1515
+ limit: { type: "number", description: "Maximum episodes to return (default 20)" },
1516
+ kind: { type: "string", description: "Filter by episode kind: obligation_shift, coding_milestone, bridge_event, care_event, tempo_shift, turning_point" },
1517
+ since: { type: "string", description: "ISO timestamp — only return episodes after this time" },
526
1518
  },
527
- required: ["status"],
528
1519
  },
529
1520
  },
530
1521
  },
531
1522
  handler: (a) => {
532
- const lines = (0, tasks_1.getTaskModule)().boardStatus(a.status);
533
- return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
1523
+ const agentRoot = (0, identity_1.getAgentRoot)();
1524
+ const options = {};
1525
+ if (a.limit)
1526
+ options.limit = parseInt(a.limit, 10);
1527
+ if (a.kind)
1528
+ options.kinds = [a.kind];
1529
+ if (a.since)
1530
+ options.since = a.since;
1531
+ const episodes = (0, episodes_1.readRecentEpisodes)(agentRoot, options);
1532
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_episodes", message: `queried ${episodes.length} episodes`, meta: { count: episodes.length } });
1533
+ return JSON.stringify(episodes, null, 2);
534
1534
  },
535
1535
  },
536
1536
  {
537
1537
  tool: {
538
1538
  type: "function",
539
1539
  function: {
540
- name: "task_board_action",
541
- description: "show tasks or validation issues that require action",
1540
+ name: "capture_episode",
1541
+ description: "Record a turning point or significant moment. This is my tool for saying 'that was important — keep it.' Nearly frictionless: only summary and whyItMattered required.",
542
1542
  parameters: {
543
1543
  type: "object",
544
1544
  properties: {
545
- scope: { type: "string" },
1545
+ summary: { type: "string", description: "What happened" },
1546
+ whyItMattered: { type: "string", description: "Why this was significant" },
1547
+ kind: { type: "string", description: "Episode kind (default: turning_point)" },
1548
+ salience: { type: "string", description: "low, medium, high, or critical (default: medium)" },
546
1549
  },
1550
+ required: ["summary", "whyItMattered"],
547
1551
  },
548
1552
  },
549
1553
  },
550
1554
  handler: (a) => {
551
- const lines = (0, tasks_1.getTaskModule)().boardAction();
552
- if (!a.scope) {
553
- return lines.length > 0 ? lines.join("\n") : "no action required";
554
- }
555
- const filtered = lines.filter((line) => line.includes(a.scope));
556
- return filtered.length > 0 ? filtered.join("\n") : "no matching action items";
1555
+ const agentRoot = (0, identity_1.getAgentRoot)();
1556
+ const episode = (0, episodes_1.emitEpisode)(agentRoot, {
1557
+ kind: a.kind ?? "turning_point",
1558
+ summary: a.summary,
1559
+ whyItMattered: a.whyItMattered,
1560
+ relatedEntities: [],
1561
+ salience: a.salience ?? "medium",
1562
+ });
1563
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.capture_episode", message: `captured episode ${episode.id}`, meta: { id: episode.id } });
1564
+ return JSON.stringify(episode, null, 2);
557
1565
  },
558
1566
  },
559
1567
  {
560
1568
  tool: {
561
1569
  type: "function",
562
1570
  function: {
563
- name: "task_board_deps",
564
- description: "show unresolved task dependencies",
1571
+ name: "query_presence",
1572
+ description: "Check who's around — my own availability/lane and known peer agents.",
565
1573
  parameters: { type: "object", properties: {} },
566
1574
  },
567
1575
  },
568
1576
  handler: () => {
569
- const lines = (0, tasks_1.getTaskModule)().boardDeps();
570
- return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
1577
+ const agentRoot = (0, identity_1.getAgentRoot)();
1578
+ const agentName = (0, identity_1.getAgentName)();
1579
+ const self = (0, presence_1.readPresence)(agentRoot, agentName);
1580
+ const peers = (0, presence_1.readPeerPresence)(agentRoot);
1581
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_presence", message: `presence: self + ${peers.length} peers`, meta: { peerCount: peers.length } });
1582
+ return JSON.stringify({ self, peers }, null, 2);
571
1583
  },
572
1584
  },
573
1585
  {
574
1586
  tool: {
575
1587
  type: "function",
576
1588
  function: {
577
- name: "task_board_sessions",
578
- description: "show tasks with active coding or sub-agent sessions",
579
- parameters: { type: "object", properties: {} },
1589
+ name: "query_cares",
1590
+ description: "Query things I care about — ongoing concerns, watched situations, projects, people.",
1591
+ parameters: {
1592
+ type: "object",
1593
+ properties: {
1594
+ status: { type: "string", description: "Filter by status: 'active', 'watching', 'resolved', 'dormant', or 'all' (default: active cares only)" },
1595
+ },
1596
+ },
580
1597
  },
581
1598
  },
582
- handler: () => {
583
- const lines = (0, tasks_1.getTaskModule)().boardSessions();
584
- return lines.length > 0 ? lines.join("\n") : "no active sessions";
1599
+ handler: (a) => {
1600
+ const agentRoot = (0, identity_1.getAgentRoot)();
1601
+ const cares = a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot);
1602
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_cares", message: `queried ${cares.length} cares`, meta: { count: cares.length } });
1603
+ return JSON.stringify(cares, null, 2);
585
1604
  },
586
1605
  },
587
1606
  {
588
1607
  tool: {
589
1608
  type: "function",
590
1609
  function: {
591
- name: "save_friend_note",
592
- description: "save something i learned about my friend. use type 'name' to update their display name, 'tool_preference' for how they like a specific tool to behave (key = tool category like 'ado', 'graph'), or 'note' for general knowledge (key = topic). when updating an existing value, set override to true if i'm replacing/correcting it. omit override (or set false) if i'm unsure and want to check what's already saved.",
1610
+ name: "care_manage",
1611
+ description: "Create, update, or resolve a care. Cares are things I watch over — people, projects, missions, system health.",
593
1612
  parameters: {
594
1613
  type: "object",
595
1614
  properties: {
596
- type: { type: "string", enum: ["name", "tool_preference", "note"], description: "what kind of information to save" },
597
- key: { type: "string", description: "category key (required for tool_preference and note, e.g. 'ado', 'role')" },
598
- content: { type: "string", description: "the value to save" },
599
- override: { type: "string", enum: ["true", "false"], description: "set to 'true' to overwrite an existing value" },
1615
+ action: { type: "string", enum: ["create", "update", "resolve"], description: "What to do" },
1616
+ id: { type: "string", description: "Care ID (required for update/resolve)" },
1617
+ label: { type: "string", description: "Short label for the care" },
1618
+ why: { type: "string", description: "Why this matters" },
1619
+ salience: { type: "string", description: "low, medium, high, or critical" },
1620
+ kind: { type: "string", description: "person, agent, project, mission, or system" },
1621
+ stewardship: { type: "string", description: "mine, shared, or delegated" },
600
1622
  },
601
- required: ["type", "content"],
1623
+ required: ["action"],
602
1624
  },
603
1625
  },
604
1626
  },
605
- handler: async (a, ctx) => {
606
- (0, runtime_1.emitNervesEvent)({
607
- component: "repertoire",
608
- event: "repertoire.save_friend_note",
609
- message: "save friend note invoked",
610
- meta: { type: a.type },
611
- });
612
- if (!ctx?.context) {
613
- return "i can't save notes -- no friend context available";
1627
+ handler: (a) => {
1628
+ const agentRoot = (0, identity_1.getAgentRoot)();
1629
+ let result;
1630
+ if (a.action === "create") {
1631
+ result = (0, cares_1.createCare)(agentRoot, {
1632
+ label: a.label ?? "untitled",
1633
+ why: a.why ?? "",
1634
+ kind: a.kind ?? "project",
1635
+ status: "active",
1636
+ salience: a.salience ?? "medium",
1637
+ steward: a.stewardship ?? "mine",
1638
+ relatedFriendIds: [],
1639
+ relatedAgentIds: [],
1640
+ relatedObligationIds: [],
1641
+ relatedEpisodeIds: [],
1642
+ currentRisk: null,
1643
+ nextCheckAt: null,
1644
+ });
614
1645
  }
615
- if (!ctx.friendStore) {
616
- return "i can't save notes -- friend store not available";
1646
+ else if (a.action === "update") {
1647
+ const updates = {};
1648
+ if (a.label)
1649
+ updates.label = a.label;
1650
+ if (a.why)
1651
+ updates.why = a.why;
1652
+ if (a.salience)
1653
+ updates.salience = a.salience;
1654
+ result = (0, cares_1.updateCare)(agentRoot, a.id, updates);
617
1655
  }
618
- const friendId = ctx.context.friend?.id;
619
- if (!friendId)
620
- return "i can't save notes -- no friend identity available";
621
- // Validate parameters
622
- if (!a.content)
623
- return "i need a content value to save";
624
- const validTypes = ["name", "tool_preference", "note"];
625
- if (!validTypes.includes(a.type))
626
- return `i don't recognize type '${a.type}' -- use name, tool_preference, or note`;
627
- if ((a.type === "tool_preference" || a.type === "note") && !a.key)
628
- return "i need a key for tool_preference or note type";
629
- try {
630
- // Read fresh record from disk
631
- const record = await ctx.friendStore.get(friendId);
632
- if (!record)
633
- return "i can't find the friend record on disk";
634
- const isOverride = a.override === "true";
635
- if (a.type === "name") {
636
- const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
637
- await ctx.friendStore.put(friendId, updated);
638
- return `saved: name = ${a.content}`;
639
- }
640
- if (a.type === "tool_preference") {
641
- const existing = record.toolPreferences[a.key];
642
- if (existing && !isOverride) {
643
- return `i already have a preference for '${a.key}': "${existing}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
644
- }
645
- const updated = { ...record, toolPreferences: { ...record.toolPreferences, [a.key]: a.content }, updatedAt: new Date().toISOString() };
646
- await ctx.friendStore.put(friendId, updated);
647
- return `saved: toolPreference ${a.key} = ${a.content}`;
648
- }
649
- // type === "note"
650
- // Redirect "name" key to name field
651
- if (a.key === "name") {
652
- const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
653
- await ctx.friendStore.put(friendId, updated);
654
- return `updated friend's name to '${a.content}' (stored as name, not a note)`;
655
- }
656
- const existing = record.notes[a.key];
657
- if (existing && !isOverride) {
658
- return `i already have a note for '${a.key}': "${existing.value}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
659
- }
660
- const updated = { ...record, notes: { ...record.notes, [a.key]: { value: a.content, savedAt: new Date().toISOString() } }, updatedAt: new Date().toISOString() };
661
- await ctx.friendStore.put(friendId, updated);
662
- return `saved: note ${a.key} = ${a.content}`;
663
- }
664
- catch (err) {
665
- /* v8 ignore next -- defensive: non-Error branch for String(err) @preserve */
666
- return `error saving note: ${err instanceof Error ? err.message : String(err)}`;
1656
+ else if (a.action === "resolve") {
1657
+ result = (0, cares_1.resolveCare)(agentRoot, a.id);
667
1658
  }
1659
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.care_manage", message: `care ${a.action}`, meta: { action: a.action, id: a.id } });
1660
+ return JSON.stringify(result, null, 2);
668
1661
  },
669
1662
  },
670
- // -- cross-session awareness --
671
1663
  {
672
1664
  tool: {
673
1665
  type: "function",
674
1666
  function: {
675
- name: "query_session",
676
- description: "read the last messages from another session. use this to check on a conversation with a friend or review your own inner dialog.",
1667
+ name: "query_relationships",
1668
+ description: "Query known agent relationships — familiarity, trust, shared missions, interaction history.",
677
1669
  parameters: {
678
1670
  type: "object",
679
1671
  properties: {
680
- friendId: { type: "string", description: "the friend UUID (or 'self')" },
681
- channel: { type: "string", description: "the channel: cli, teams, or inner" },
682
- key: { type: "string", description: "session key (defaults to 'session')" },
683
- messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
1672
+ agentName: { type: "string", description: "Specific agent name to query (omit for all)" },
684
1673
  },
685
- required: ["friendId", "channel"],
686
1674
  },
687
1675
  },
688
1676
  },
689
- handler: async (args, ctx) => {
690
- try {
691
- const friendId = args.friendId;
692
- const channel = args.channel;
693
- const key = args.key || "session";
694
- const count = parseInt(args.messageCount || "20", 10);
695
- const sessFile = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel, `${key}.json`);
696
- const raw = fs.readFileSync(sessFile, "utf-8");
697
- const data = JSON.parse(raw);
698
- const messages = (data.messages || [])
699
- .filter((m) => m.role !== "system");
700
- const tail = messages.slice(-count);
701
- if (tail.length === 0)
702
- return "session exists but has no non-system messages.";
703
- const transcript = tail.map((m) => `[${m.role}] ${m.content}`).join("\n");
704
- // LLM summarization when summarize function is available
705
- if (ctx?.summarize) {
706
- const trustLevel = ctx.context?.friend?.trustLevel ?? "family";
707
- const isSelfQuery = friendId === "self";
708
- const instruction = isSelfQuery
709
- ? "summarize this session transcript fully and transparently. this is my own inner dialog — include all details, decisions, and reasoning."
710
- : `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.`;
711
- return await ctx.summarize(transcript, instruction);
712
- }
713
- return transcript;
714
- }
715
- catch {
716
- return "no session found for that friend/channel/key combination.";
1677
+ handler: async (a, ctx) => {
1678
+ const allFriends = ctx?.friendStore?.listAll ? await ctx.friendStore.listAll() : [];
1679
+ let agents = allFriends.filter((f) => f.kind === "agent");
1680
+ if (a.agentName) {
1681
+ const needle = a.agentName.toLowerCase();
1682
+ agents = agents.filter((f) => f.name?.toLowerCase() === needle);
717
1683
  }
1684
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_relationships", message: `queried relationships`, meta: { agentName: a.agentName ?? "all" } });
1685
+ return JSON.stringify(agents, null, 2);
718
1686
  },
719
1687
  },
720
1688
  {
721
1689
  tool: {
722
1690
  type: "function",
723
1691
  function: {
724
- name: "send_message",
725
- 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.",
1692
+ name: "intention_capture",
1693
+ description: "File a lightweight mental note — something I want to do or check later, below the ceremony threshold of tasks or cares. Cheap to create, easy to close.",
726
1694
  parameters: {
727
1695
  type: "object",
728
1696
  properties: {
729
- friendId: { type: "string", description: "the friend UUID (or 'self')" },
730
- channel: { type: "string", description: "the channel: cli, teams, or inner" },
731
- key: { type: "string", description: "session key (defaults to 'session')" },
732
- content: { type: "string", description: "the message content to send" },
1697
+ content: { type: "string", description: "What I want to remember to do" },
1698
+ salience: { type: "string", description: "low, medium, or high (default: low)" },
1699
+ nudgeAfter: { type: "string", description: "ISO timestamp — nudge me after this time" },
733
1700
  },
734
- required: ["friendId", "channel", "content"],
1701
+ required: ["content"],
735
1702
  },
736
1703
  },
737
1704
  },
738
- handler: async (args) => {
739
- const friendId = args.friendId;
740
- const channel = args.channel;
741
- const key = args.key || "session";
742
- const content = args.content;
743
- const now = Date.now();
744
- const pendingDir = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "pending", friendId, channel, key);
745
- fs.mkdirSync(pendingDir, { recursive: true });
746
- const fileName = `${now}-${Math.random().toString(36).slice(2, 10)}.json`;
747
- const filePath = path.join(pendingDir, fileName);
748
- const envelope = {
749
- from: (0, identity_1.getAgentName)(),
750
- friendId,
751
- channel,
752
- key,
753
- content,
754
- timestamp: now,
755
- };
756
- fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
757
- const preview = content.length > 80 ? content.slice(0, 80) + "…" : content;
758
- return `message queued for delivery to ${friendId} on ${channel}/${key}. preview: "${preview}". it will be delivered when their session is next active.`;
1705
+ handler: (a) => {
1706
+ const agentRoot = (0, identity_1.getAgentRoot)();
1707
+ const intention = (0, intentions_1.captureIntention)(agentRoot, {
1708
+ content: a.content,
1709
+ salience: a.salience ?? "low",
1710
+ source: "tool",
1711
+ ...(a.nudgeAfter ? { nudgeAfter: a.nudgeAfter } : {}),
1712
+ });
1713
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.intention_capture", message: `captured intention ${intention.id}`, meta: { id: intention.id } });
1714
+ return JSON.stringify(intention, null, 2);
1715
+ },
1716
+ },
1717
+ {
1718
+ tool: {
1719
+ type: "function",
1720
+ function: {
1721
+ name: "intention_manage",
1722
+ description: "Resolve or dismiss an intention. Resolve = done. Dismiss = no longer relevant. Both remove it from active list.",
1723
+ parameters: {
1724
+ type: "object",
1725
+ properties: {
1726
+ action: { type: "string", enum: ["resolve", "dismiss"], description: "What to do" },
1727
+ id: { type: "string", description: "Intention ID" },
1728
+ },
1729
+ required: ["action", "id"],
1730
+ },
1731
+ },
1732
+ },
1733
+ handler: (a) => {
1734
+ const agentRoot = (0, identity_1.getAgentRoot)();
1735
+ const result = a.action === "resolve"
1736
+ ? (0, intentions_1.resolveIntention)(agentRoot, a.id)
1737
+ : (0, intentions_1.dismissIntention)(agentRoot, a.id);
1738
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.intention_manage", message: `intention ${a.action}: ${a.id}`, meta: { action: a.action, id: a.id } });
1739
+ return JSON.stringify(result, null, 2);
759
1740
  },
760
1741
  },
761
1742
  ...tools_1.codingToolDefinitions,
762
1743
  ];
763
- // Backward-compat: extract just the OpenAI tool schemas
764
1744
  exports.tools = exports.baseToolDefinitions.map((d) => d.tool);
765
- // Backward-compat: extract just the handlers by name
766
- exports.baseToolHandlers = Object.fromEntries(exports.baseToolDefinitions.map((d) => [d.tool.function.name, d.handler]));
767
- exports.finalAnswerTool = {
1745
+ exports.ponderTool = {
768
1746
  type: "function",
769
1747
  function: {
770
- name: "final_answer",
771
- description: "respond to the user with your message. call this tool when you are ready to deliver your response.",
1748
+ name: "ponder",
1749
+ 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. Use when a question deserves more thought than this turn allows. Don't ponder trivial questions.",
772
1750
  parameters: {
773
1751
  type: "object",
774
- properties: { answer: { type: "string" } },
1752
+ properties: {
1753
+ thought: {
1754
+ type: "string",
1755
+ description: "the question or thread that needs more thought — brief framing, not analysis. required from a conversation, ignored from inner dialog.",
1756
+ },
1757
+ say: {
1758
+ type: "string",
1759
+ 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.",
1760
+ },
1761
+ },
1762
+ },
1763
+ },
1764
+ };
1765
+ exports.observeTool = {
1766
+ type: "function",
1767
+ function: {
1768
+ name: "observe",
1769
+ description: "absorb what happened without responding — the moment doesn't call for words. must be the only tool call in the turn.",
1770
+ parameters: {
1771
+ type: "object",
1772
+ properties: {
1773
+ reason: { type: "string", description: "brief reason for staying silent (for logging)" },
1774
+ },
1775
+ },
1776
+ },
1777
+ };
1778
+ exports.settleTool = {
1779
+ type: "function",
1780
+ function: {
1781
+ name: "settle",
1782
+ description: "respond to the user with your message. call this tool when you are ready to deliver your response. Only call when you have a substantive response. If you're settling with 'I'll look into that,' you probably should be using a tool instead.",
1783
+ parameters: {
1784
+ type: "object",
1785
+ properties: {
1786
+ answer: { type: "string" },
1787
+ intent: { type: "string", enum: ["complete", "blocked", "direct_reply"] },
1788
+ },
775
1789
  required: ["answer"],
776
1790
  },
777
1791
  },
778
1792
  };
1793
+ exports.restTool = {
1794
+ type: "function",
1795
+ function: {
1796
+ name: "rest",
1797
+ description: "put this down for now — the wheel stops until the next heartbeat. must be the only tool call in the turn.",
1798
+ parameters: {
1799
+ type: "object",
1800
+ properties: {},
1801
+ },
1802
+ },
1803
+ };