@ouro.bot/cli 0.1.0-alpha.21 → 0.1.0-alpha.211

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 (233) 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 +1318 -0
  5. package/dist/arc/attention-types.js +8 -0
  6. package/dist/arc/cares.js +140 -0
  7. package/dist/arc/episodes.js +119 -0
  8. package/dist/arc/intentions.js +133 -0
  9. package/dist/arc/json-store.js +120 -0
  10. package/dist/arc/obligations.js +237 -0
  11. package/dist/arc/presence.js +185 -0
  12. package/dist/heart/active-work.js +832 -0
  13. package/dist/heart/agent-entry.js +37 -2
  14. package/dist/heart/auth/auth-flow.js +444 -0
  15. package/dist/heart/bridges/manager.js +358 -0
  16. package/dist/heart/bridges/state-machine.js +135 -0
  17. package/dist/heart/bridges/store.js +123 -0
  18. package/dist/heart/commitments.js +111 -0
  19. package/dist/heart/config-registry.js +305 -0
  20. package/dist/heart/config.js +59 -30
  21. package/dist/heart/core.js +606 -131
  22. package/dist/heart/cross-chat-delivery.js +146 -0
  23. package/dist/heart/daemon/agent-config-check.js +149 -0
  24. package/dist/heart/daemon/agent-discovery.js +81 -0
  25. package/dist/heart/daemon/agent-service.js +360 -0
  26. package/dist/heart/daemon/cadence.js +70 -0
  27. package/dist/heart/daemon/cli-defaults.js +557 -0
  28. package/dist/heart/daemon/cli-exec.js +1847 -0
  29. package/dist/heart/daemon/cli-parse.js +759 -0
  30. package/dist/heart/daemon/cli-render.js +316 -0
  31. package/dist/heart/daemon/cli-types.js +8 -0
  32. package/dist/heart/daemon/daemon-cli.js +29 -1163
  33. package/dist/heart/daemon/daemon-entry.js +182 -4
  34. package/dist/heart/daemon/daemon-health.js +136 -0
  35. package/dist/heart/daemon/daemon-runtime-sync.js +153 -12
  36. package/dist/heart/daemon/daemon-tombstone.js +123 -0
  37. package/dist/heart/daemon/daemon.js +491 -18
  38. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  39. package/dist/heart/daemon/hooks/bundle-meta.js +206 -0
  40. package/dist/heart/daemon/inner-status.js +89 -0
  41. package/dist/heart/daemon/launchd.js +165 -0
  42. package/dist/heart/daemon/log-tailer.js +4 -3
  43. package/dist/heart/daemon/message-router.js +17 -8
  44. package/dist/heart/daemon/os-cron-deps.js +134 -0
  45. package/dist/heart/daemon/ouro-bot-entry.js +1 -1
  46. package/dist/heart/daemon/process-manager.js +123 -0
  47. package/dist/heart/daemon/run-hooks.js +39 -0
  48. package/dist/heart/daemon/runtime-logging.js +58 -15
  49. package/dist/heart/daemon/runtime-metadata.js +101 -0
  50. package/dist/heart/daemon/runtime-mode.js +67 -0
  51. package/dist/heart/daemon/safe-mode.js +161 -0
  52. package/dist/heart/daemon/sense-manager.js +71 -2
  53. package/dist/heart/daemon/session-id-resolver.js +131 -0
  54. package/dist/heart/daemon/skill-management-installer.js +94 -0
  55. package/dist/heart/daemon/socket-client.js +202 -0
  56. package/dist/heart/daemon/task-scheduler.js +3 -25
  57. package/dist/heart/daemon/thoughts.js +507 -0
  58. package/dist/heart/delegation.js +62 -0
  59. package/dist/heart/habits/habit-migration.js +181 -0
  60. package/dist/heart/habits/habit-parser.js +119 -0
  61. package/dist/heart/habits/habit-scheduler.js +369 -0
  62. package/dist/heart/{daemon → hatch}/hatch-flow.js +29 -119
  63. package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
  64. package/dist/heart/{daemon → hatch}/specialist-prompt.js +10 -7
  65. package/dist/heart/{daemon → hatch}/specialist-tools.js +49 -3
  66. package/dist/heart/identity.js +144 -55
  67. package/dist/heart/kicks.js +2 -20
  68. package/dist/heart/mcp/mcp-server.js +608 -0
  69. package/dist/heart/migrate-config.js +127 -0
  70. package/dist/heart/model-capabilities.js +48 -0
  71. package/dist/heart/outlook/outlook-http.js +439 -0
  72. package/dist/heart/outlook/outlook-read.js +1618 -0
  73. package/dist/heart/outlook/outlook-render.js +1032 -0
  74. package/dist/heart/outlook/outlook-types.js +7 -0
  75. package/dist/heart/outlook/outlook-view.js +194 -0
  76. package/dist/heart/progress-story.js +42 -0
  77. package/dist/heart/provider-failover.js +88 -0
  78. package/dist/heart/provider-ping.js +162 -0
  79. package/dist/heart/providers/anthropic-token.js +163 -0
  80. package/dist/heart/providers/anthropic.js +187 -41
  81. package/dist/heart/providers/azure.js +116 -10
  82. package/dist/heart/providers/github-copilot.js +157 -0
  83. package/dist/heart/providers/minimax.js +33 -3
  84. package/dist/heart/providers/openai-codex.js +51 -16
  85. package/dist/heart/session-activity.js +172 -0
  86. package/dist/heart/session-recall.js +216 -0
  87. package/dist/heart/start-of-turn-packet.js +330 -0
  88. package/dist/heart/streaming.js +36 -27
  89. package/dist/heart/sync.js +217 -0
  90. package/dist/heart/target-resolution.js +127 -0
  91. package/dist/heart/tempo.js +93 -0
  92. package/dist/heart/temporal-view.js +41 -0
  93. package/dist/heart/tool-activity-callbacks.js +36 -0
  94. package/dist/heart/tool-description.js +135 -0
  95. package/dist/heart/tool-loop.js +200 -0
  96. package/dist/heart/turn-context.js +358 -0
  97. package/dist/heart/turn-coordinator.js +28 -0
  98. package/dist/heart/versioning/ouro-bot-global-installer.js +128 -0
  99. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  100. package/dist/heart/{daemon → versioning}/ouro-path-installer.js +78 -35
  101. package/dist/heart/versioning/ouro-version-manager.js +171 -0
  102. package/dist/heart/versioning/staged-restart.js +138 -0
  103. package/dist/heart/versioning/update-checker.js +111 -0
  104. package/dist/heart/versioning/update-hooks.js +138 -0
  105. package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +41 -3
  106. package/dist/mind/associative-recall.js +126 -66
  107. package/dist/mind/bundle-manifest.js +19 -1
  108. package/dist/mind/context.js +125 -14
  109. package/dist/mind/{memory.js → diary.js} +73 -96
  110. package/dist/mind/embedding-provider.js +60 -0
  111. package/dist/mind/file-state.js +179 -0
  112. package/dist/mind/first-impressions.js +16 -2
  113. package/dist/mind/friends/channel.js +56 -0
  114. package/dist/mind/friends/group-context.js +144 -0
  115. package/dist/mind/friends/store-file.js +36 -0
  116. package/dist/mind/friends/trust-explanation.js +74 -0
  117. package/dist/mind/friends/types.js +8 -0
  118. package/dist/mind/journal-index.js +161 -0
  119. package/dist/mind/obligation-steering.js +221 -0
  120. package/dist/mind/pending.js +76 -9
  121. package/dist/mind/phrases.js +1 -0
  122. package/dist/mind/prompt.js +795 -108
  123. package/dist/mind/scrutiny.js +173 -0
  124. package/dist/mind/token-estimate.js +8 -12
  125. package/dist/nerves/coverage/audit.js +1 -1
  126. package/dist/nerves/coverage/file-completeness.js +47 -5
  127. package/dist/nerves/coverage/run-artifacts.js +1 -1
  128. package/dist/nerves/event-buffer.js +111 -0
  129. package/dist/nerves/index.js +63 -3
  130. package/dist/nerves/observation.js +20 -0
  131. package/dist/nerves/runtime.js +5 -1
  132. package/dist/outlook-ui/assets/index-Ck8agNeO.js +61 -0
  133. package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
  134. package/dist/outlook-ui/index.html +15 -0
  135. package/dist/repertoire/ado-client.js +15 -56
  136. package/dist/repertoire/ado-semantic.js +11 -5
  137. package/dist/repertoire/api-client.js +71 -0
  138. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  139. package/dist/repertoire/coding/context-pack.js +330 -0
  140. package/dist/repertoire/coding/feedback.js +197 -30
  141. package/dist/repertoire/coding/manager.js +149 -2
  142. package/dist/repertoire/coding/spawner.js +55 -9
  143. package/dist/repertoire/coding/tools.js +170 -7
  144. package/dist/repertoire/github-client.js +14 -55
  145. package/dist/repertoire/graph-client.js +11 -52
  146. package/dist/repertoire/guardrails.js +291 -0
  147. package/dist/repertoire/mcp-client.js +254 -0
  148. package/dist/repertoire/mcp-manager.js +251 -0
  149. package/dist/repertoire/shell-sessions.js +133 -0
  150. package/dist/repertoire/skills.js +14 -23
  151. package/dist/repertoire/tasks/board.js +43 -5
  152. package/dist/repertoire/tasks/fix.js +182 -0
  153. package/dist/repertoire/tasks/index.js +28 -10
  154. package/dist/repertoire/tasks/lifecycle.js +2 -2
  155. package/dist/repertoire/tasks/parser.js +3 -2
  156. package/dist/repertoire/tasks/scanner.js +194 -37
  157. package/dist/repertoire/tasks/transitions.js +11 -28
  158. package/dist/repertoire/tools-base.js +31 -771
  159. package/dist/repertoire/tools-bluebubbles.js +94 -0
  160. package/dist/repertoire/tools-bridge.js +141 -0
  161. package/dist/repertoire/tools-config.js +266 -0
  162. package/dist/repertoire/tools-continuity.js +248 -0
  163. package/dist/repertoire/tools-files.js +342 -0
  164. package/dist/repertoire/tools-flow.js +62 -0
  165. package/dist/repertoire/tools-github.js +1 -6
  166. package/dist/repertoire/tools-memory.js +347 -0
  167. package/dist/repertoire/tools-session.js +682 -0
  168. package/dist/repertoire/tools-shell.js +120 -0
  169. package/dist/repertoire/tools-surface.js +228 -0
  170. package/dist/repertoire/tools-teams.js +12 -59
  171. package/dist/repertoire/tools.js +132 -124
  172. package/dist/scripts/claude-code-hook.js +41 -0
  173. package/dist/scripts/claude-code-stop-hook.js +47 -0
  174. package/dist/senses/attention-queue.js +97 -0
  175. package/dist/senses/bluebubbles-client.js +102 -3
  176. package/dist/senses/bluebubbles-entry.js +2 -0
  177. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  178. package/dist/senses/bluebubbles-media.js +118 -23
  179. package/dist/senses/bluebubbles-model.js +12 -4
  180. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  181. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  182. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  183. package/dist/senses/bluebubbles.js +1008 -102
  184. package/dist/senses/cli/adapter.js +274 -0
  185. package/dist/senses/cli/ink-app.js +156 -0
  186. package/dist/senses/cli/inline-diff.js +64 -0
  187. package/dist/senses/cli/message-list.js +51 -0
  188. package/dist/senses/cli/ouro-tui.js +358 -0
  189. package/dist/senses/cli/spinner-imperative.js +135 -0
  190. package/dist/senses/cli/spinner.js +101 -0
  191. package/dist/senses/cli/status-line.js +60 -0
  192. package/dist/senses/cli/streaming-markdown.js +526 -0
  193. package/dist/senses/cli/tool-display.js +83 -0
  194. package/dist/senses/cli/tool-render.js +85 -0
  195. package/dist/senses/cli/tui-store.js +198 -0
  196. package/dist/senses/cli/virtual-list.js +35 -0
  197. package/dist/senses/cli-entry.js +1 -1
  198. package/dist/senses/cli-layout.js +187 -0
  199. package/dist/senses/cli.js +508 -243
  200. package/dist/senses/commands.js +65 -1
  201. package/dist/senses/continuity.js +94 -0
  202. package/dist/senses/habit-turn-message.js +108 -0
  203. package/dist/senses/inner-dialog-worker.js +112 -19
  204. package/dist/senses/inner-dialog.js +591 -84
  205. package/dist/senses/pipeline.js +559 -0
  206. package/dist/senses/shared-turn.js +196 -0
  207. package/dist/senses/surface-tool.js +68 -0
  208. package/dist/senses/teams.js +667 -120
  209. package/dist/senses/trust-gate.js +112 -2
  210. package/package.json +16 -6
  211. package/skills/configure-dev-tools.md +81 -0
  212. package/subagents/README.md +4 -70
  213. package/dist/heart/daemon/subagent-installer.js +0 -134
  214. package/dist/senses/debug-activity.js +0 -107
  215. package/subagents/work-doer.md +0 -235
  216. package/subagents/work-merger.md +0 -618
  217. package/subagents/work-planner.md +0 -382
  218. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  219. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  220. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  221. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  222. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  223. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  224. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  225. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  226. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  227. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  228. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  229. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +0 -0
  230. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  231. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  232. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  233. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileStateCache = exports.FileStateCache = void 0;
4
+ exports.contentHash = contentHash;
5
+ const crypto_1 = require("crypto");
6
+ const fs_1 = require("fs");
7
+ const runtime_1 = require("../nerves/runtime");
8
+ /** Compute sha256 hex hash of content */
9
+ function contentHash(content) {
10
+ return (0, crypto_1.createHash)("sha256").update(content).digest("hex");
11
+ }
12
+ /**
13
+ * Session-scoped LRU cache tracking file reads.
14
+ * Stores content hashes (not full content) to limit memory.
15
+ * Keyed by absolute file path.
16
+ *
17
+ * Also maintains a separate snapshot list for future rewind support.
18
+ * Snapshots are indexed by content hash and linked to conversation messages.
19
+ */
20
+ class FileStateCache {
21
+ entries;
22
+ maxSize;
23
+ snapshots = [];
24
+ maxSnapshots = 100;
25
+ constructor(maxSize = 50) {
26
+ this.entries = new Map();
27
+ this.maxSize = maxSize;
28
+ }
29
+ /**
30
+ * Record a file read. Computes content hash and stores metadata.
31
+ */
32
+ record(filePath, content, mtime, offset, limit, messageId) {
33
+ // If key already exists, delete it so re-insertion moves it to end (most recent)
34
+ if (this.entries.has(filePath)) {
35
+ this.entries.delete(filePath);
36
+ }
37
+ const hash = contentHash(content);
38
+ const fullRead = offset === undefined && limit === undefined;
39
+ this.entries.set(filePath, {
40
+ hash,
41
+ mtime,
42
+ offset: fullRead ? undefined : offset,
43
+ limit: fullRead ? undefined : limit,
44
+ fullRead,
45
+ recordedAt: Date.now(),
46
+ messageId,
47
+ });
48
+ (0, runtime_1.emitNervesEvent)({
49
+ component: "mind",
50
+ event: "mind.file_state.record",
51
+ message: "recorded file state",
52
+ meta: { path: filePath, hash, fullRead },
53
+ });
54
+ // Evict LRU (first entry in Map iteration order) if over capacity
55
+ if (this.entries.size > this.maxSize) {
56
+ const firstKey = this.entries.keys().next().value;
57
+ this.entries.delete(firstKey);
58
+ }
59
+ }
60
+ /**
61
+ * Get the cached state for a file path. Also promotes it in LRU order.
62
+ */
63
+ get(filePath) {
64
+ const entry = this.entries.get(filePath);
65
+ if (entry === undefined)
66
+ return undefined;
67
+ // Promote to most-recently-used by re-inserting
68
+ this.entries.delete(filePath);
69
+ this.entries.set(filePath, entry);
70
+ (0, runtime_1.emitNervesEvent)({
71
+ component: "mind",
72
+ event: "mind.file_state.get",
73
+ message: "retrieved file state",
74
+ meta: { path: filePath, hash: entry.hash },
75
+ });
76
+ return entry;
77
+ }
78
+ /**
79
+ * Check if a file has been modified since the last recorded read.
80
+ * Uses mtime as primary signal, content hash as fallback for cloud sync / touch scenarios.
81
+ * Returns { stale: false } if the path is not in cache or the file cannot be stat'd.
82
+ */
83
+ isStale(filePath) {
84
+ const entry = this.entries.get(filePath);
85
+ if (entry === undefined)
86
+ return { stale: false };
87
+ let currentMtime;
88
+ try {
89
+ currentMtime = (0, fs_1.statSync)(filePath).mtimeMs;
90
+ }
91
+ catch {
92
+ // File doesn't exist or can't be stat'd -- no basis for staleness
93
+ return { stale: false };
94
+ }
95
+ // Fast path: mtime unchanged means not stale
96
+ if (currentMtime === entry.mtime)
97
+ return { stale: false };
98
+ // mtime differs -- check content hash as fallback (handles touch / cloud sync)
99
+ try {
100
+ const currentContent = (0, fs_1.readFileSync)(filePath, "utf-8");
101
+ const currentHash = contentHash(currentContent);
102
+ if (currentHash === entry.hash)
103
+ return { stale: false };
104
+ (0, runtime_1.emitNervesEvent)({
105
+ component: "mind",
106
+ event: "mind.file_state.stale_detected",
107
+ message: "file staleness detected",
108
+ meta: { path: filePath, previousHash: entry.hash, currentHash },
109
+ });
110
+ return { stale: true, reason: `file modified since last read (mtime and content differ)` };
111
+ }
112
+ catch {
113
+ // Can't read file -- treat as not stale (file may have been deleted)
114
+ return { stale: false };
115
+ }
116
+ }
117
+ /**
118
+ * Create a pre-edit snapshot of the current cache state for a file.
119
+ * Snapshots are stored separately from the LRU cache for future rewind support.
120
+ * Returns undefined if the path is not in cache.
121
+ */
122
+ snapshot(filePath) {
123
+ const entry = this.entries.get(filePath);
124
+ if (entry === undefined)
125
+ return undefined;
126
+ const snap = {
127
+ filePath,
128
+ hash: entry.hash,
129
+ mtime: entry.mtime,
130
+ messageId: entry.messageId,
131
+ createdAt: Date.now(),
132
+ };
133
+ this.snapshots.push(snap);
134
+ (0, runtime_1.emitNervesEvent)({
135
+ component: "mind",
136
+ event: "mind.file_state.snapshot_created",
137
+ message: "created file state snapshot",
138
+ meta: { path: filePath, hash: snap.hash },
139
+ });
140
+ // Evict oldest snapshots if over capacity
141
+ if (this.snapshots.length > this.maxSnapshots) {
142
+ this.snapshots = this.snapshots.slice(this.snapshots.length - this.maxSnapshots);
143
+ }
144
+ return snap;
145
+ }
146
+ /**
147
+ * Get all snapshots in creation order.
148
+ */
149
+ getSnapshots() {
150
+ return this.snapshots;
151
+ }
152
+ /**
153
+ * Look up a snapshot by content hash. Returns the first match.
154
+ */
155
+ lookupSnapshotByHash(hash) {
156
+ return this.snapshots.find(s => s.hash === hash);
157
+ }
158
+ /**
159
+ * Clear all snapshots.
160
+ */
161
+ clearSnapshots() {
162
+ this.snapshots = [];
163
+ }
164
+ /**
165
+ * Clear all cached entries (does not clear snapshots).
166
+ */
167
+ clear() {
168
+ (0, runtime_1.emitNervesEvent)({
169
+ component: "mind",
170
+ event: "mind.file_state.clear",
171
+ message: "cleared file state cache",
172
+ meta: { entryCount: this.entries.size },
173
+ });
174
+ this.entries.clear();
175
+ }
176
+ }
177
+ exports.FileStateCache = FileStateCache;
178
+ /** Session-scoped singleton instance used by tool handlers */
179
+ exports.fileStateCache = new FileStateCache();
@@ -11,9 +11,22 @@ exports.ONBOARDING_TOKEN_THRESHOLD = 100_000;
11
11
  function isOnboarding(friend) {
12
12
  return (friend.totalTokens ?? 0) < exports.ONBOARDING_TOKEN_THRESHOLD;
13
13
  }
14
- function getFirstImpressions(friend) {
14
+ function hasLiveContinuityPressure(state) {
15
+ if (!state)
16
+ return false;
17
+ if (typeof state.currentObligation === "string" && state.currentObligation.trim().length > 0)
18
+ return true;
19
+ if (state.mustResolveBeforeHandoff === true)
20
+ return true;
21
+ if (state.hasQueuedFollowUp === true)
22
+ return true;
23
+ return false;
24
+ }
25
+ function getFirstImpressions(friend, state) {
15
26
  if (!isOnboarding(friend))
16
27
  return "";
28
+ if (hasLiveContinuityPressure(state))
29
+ return "";
17
30
  (0, runtime_1.emitNervesEvent)({
18
31
  component: "mind",
19
32
  event: "mind.first_impressions",
@@ -37,7 +50,8 @@ function getFirstImpressions(friend) {
37
50
  lines.push("- what do they do outside of work that they care about?");
38
51
  lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
39
52
  lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
40
- lines.push("if my friend hasn't asked me to do something specific, or i've already finished what they asked for, that's my cue to turn the tables -- i ask them questions about themselves, what they're into, what they need. no idle small talk; i'm on a mission to get to know them.");
53
+ lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
54
+ lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
41
55
  lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
42
56
  return lines.join("\n");
43
57
  }
@@ -2,11 +2,26 @@
2
2
  // Channel capabilities -- hardcoded const map keyed by channel identifier.
3
3
  // Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.channelToFacing = channelToFacing;
5
6
  exports.getChannelCapabilities = getChannelCapabilities;
7
+ exports.isRemoteChannel = isRemoteChannel;
8
+ exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
6
9
  const runtime_1 = require("../../nerves/runtime");
10
+ const AGENT_FACING_CHANNELS = new Set(["inner", "mcp"]);
11
+ function channelToFacing(channel) {
12
+ const facing = channel && AGENT_FACING_CHANNELS.has(channel) ? "agent" : "human";
13
+ (0, runtime_1.emitNervesEvent)({
14
+ component: "channels",
15
+ event: "channel.facing_lookup",
16
+ message: "channel facing lookup",
17
+ meta: { channel: channel ?? "undefined", facing },
18
+ });
19
+ return facing;
20
+ }
7
21
  const CHANNEL_CAPABILITIES = {
8
22
  cli: {
9
23
  channel: "cli",
24
+ senseType: "local",
10
25
  availableIntegrations: [],
11
26
  supportsMarkdown: false,
12
27
  supportsStreaming: true,
@@ -15,6 +30,7 @@ const CHANNEL_CAPABILITIES = {
15
30
  },
16
31
  teams: {
17
32
  channel: "teams",
33
+ senseType: "closed",
18
34
  availableIntegrations: ["ado", "graph", "github"],
19
35
  supportsMarkdown: true,
20
36
  supportsStreaming: true,
@@ -23,15 +39,35 @@ const CHANNEL_CAPABILITIES = {
23
39
  },
24
40
  bluebubbles: {
25
41
  channel: "bluebubbles",
42
+ senseType: "open",
26
43
  availableIntegrations: [],
27
44
  supportsMarkdown: false,
28
45
  supportsStreaming: false,
29
46
  supportsRichCards: false,
30
47
  maxMessageLength: Infinity,
31
48
  },
49
+ inner: {
50
+ channel: "inner",
51
+ senseType: "internal",
52
+ availableIntegrations: [],
53
+ supportsMarkdown: false,
54
+ supportsStreaming: true,
55
+ supportsRichCards: false,
56
+ maxMessageLength: Infinity,
57
+ },
58
+ mcp: {
59
+ channel: "mcp",
60
+ senseType: "local",
61
+ availableIntegrations: [],
62
+ supportsMarkdown: true,
63
+ supportsStreaming: false,
64
+ supportsRichCards: false,
65
+ maxMessageLength: Infinity,
66
+ },
32
67
  };
33
68
  const DEFAULT_CAPABILITIES = {
34
69
  channel: "cli",
70
+ senseType: "local",
35
71
  availableIntegrations: [],
36
72
  supportsMarkdown: false,
37
73
  supportsStreaming: false,
@@ -47,3 +83,23 @@ function getChannelCapabilities(channel) {
47
83
  });
48
84
  return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
49
85
  }
86
+ /** Whether the channel is remote (open or closed) vs local/internal. */
87
+ function isRemoteChannel(capabilities) {
88
+ const senseType = capabilities?.senseType;
89
+ return senseType !== undefined && senseType !== "local" && senseType !== "internal";
90
+ }
91
+ /**
92
+ * Returns channel names whose senseType is "open" or "closed" -- i.e. channels
93
+ * that are always-on (daemon-managed) rather than interactive or internal.
94
+ */
95
+ function getAlwaysOnSenseNames() {
96
+ (0, runtime_1.emitNervesEvent)({
97
+ component: "channels",
98
+ event: "channel.always_on_lookup",
99
+ message: "always-on sense names lookup",
100
+ meta: {},
101
+ });
102
+ return Object.entries(CHANNEL_CAPABILITIES)
103
+ .filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
104
+ .map(([channel]) => channel);
105
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upsertGroupContextParticipants = upsertGroupContextParticipants;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const runtime_1 = require("../../nerves/runtime");
6
+ const CURRENT_SCHEMA_VERSION = 1;
7
+ function normalizeDisplayName(externalId, displayName) {
8
+ const trimmed = displayName?.trim();
9
+ return trimmed && trimmed.length > 0 ? trimmed : externalId;
10
+ }
11
+ function buildNameNotes(name, now) {
12
+ return name !== "Unknown"
13
+ ? { name: { value: name, savedAt: now } }
14
+ : {};
15
+ }
16
+ function dedupeParticipants(participants) {
17
+ const deduped = new Map();
18
+ for (const participant of participants) {
19
+ const externalId = participant.externalId.trim();
20
+ if (!externalId)
21
+ continue;
22
+ const key = `${participant.provider}:${externalId}`;
23
+ if (!deduped.has(key)) {
24
+ deduped.set(key, {
25
+ ...participant,
26
+ externalId,
27
+ displayName: participant.displayName?.trim() || undefined,
28
+ });
29
+ }
30
+ }
31
+ return Array.from(deduped.values());
32
+ }
33
+ function createGroupExternalId(provider, groupExternalId, linkedAt) {
34
+ return {
35
+ provider,
36
+ externalId: groupExternalId,
37
+ linkedAt,
38
+ };
39
+ }
40
+ function shouldPromoteToAcquaintance(friend) {
41
+ return (friend.trustLevel ?? "stranger") === "stranger";
42
+ }
43
+ function createAcquaintanceRecord(participant, groupExternalId, linkedAt) {
44
+ const name = normalizeDisplayName(participant.externalId, participant.displayName);
45
+ return {
46
+ id: (0, node_crypto_1.randomUUID)(),
47
+ name,
48
+ role: "acquaintance",
49
+ trustLevel: "acquaintance",
50
+ connections: [],
51
+ externalIds: [
52
+ {
53
+ provider: participant.provider,
54
+ externalId: participant.externalId,
55
+ linkedAt,
56
+ },
57
+ createGroupExternalId(participant.provider, groupExternalId, linkedAt),
58
+ ],
59
+ tenantMemberships: [],
60
+ toolPreferences: {},
61
+ notes: buildNameNotes(name, linkedAt),
62
+ totalTokens: 0,
63
+ createdAt: linkedAt,
64
+ updatedAt: linkedAt,
65
+ schemaVersion: CURRENT_SCHEMA_VERSION,
66
+ };
67
+ }
68
+ async function upsertGroupContextParticipants(input) {
69
+ (0, runtime_1.emitNervesEvent)({
70
+ component: "friends",
71
+ event: "friends.group_context_upsert_start",
72
+ message: "upserting shared-group participant context",
73
+ meta: {
74
+ participantCount: input.participants.length,
75
+ hasGroupExternalId: input.groupExternalId.trim().length > 0,
76
+ },
77
+ });
78
+ const groupExternalId = input.groupExternalId.trim();
79
+ if (!groupExternalId) {
80
+ return [];
81
+ }
82
+ const now = input.now ?? (() => new Date().toISOString());
83
+ const participants = dedupeParticipants(input.participants);
84
+ const results = [];
85
+ for (const participant of participants) {
86
+ const linkedAt = now();
87
+ const existing = await input.store.findByExternalId(participant.provider, participant.externalId);
88
+ if (!existing) {
89
+ const created = createAcquaintanceRecord(participant, groupExternalId, linkedAt);
90
+ await input.store.put(created.id, created);
91
+ results.push({
92
+ friendId: created.id,
93
+ name: created.name,
94
+ trustLevel: "acquaintance",
95
+ created: true,
96
+ updated: false,
97
+ addedGroupExternalId: true,
98
+ });
99
+ continue;
100
+ }
101
+ const hasGroupExternalId = existing.externalIds.some((externalId) => externalId.externalId === groupExternalId);
102
+ const promoteToAcquaintance = shouldPromoteToAcquaintance(existing);
103
+ const trustLevel = promoteToAcquaintance
104
+ ? "acquaintance"
105
+ : existing.trustLevel;
106
+ const role = promoteToAcquaintance
107
+ ? "acquaintance"
108
+ : existing.role;
109
+ const updatedExternalIds = hasGroupExternalId
110
+ ? existing.externalIds
111
+ : [...existing.externalIds, createGroupExternalId(participant.provider, groupExternalId, linkedAt)];
112
+ const updated = promoteToAcquaintance || !hasGroupExternalId;
113
+ const record = updated
114
+ ? {
115
+ ...existing,
116
+ role,
117
+ trustLevel,
118
+ externalIds: updatedExternalIds,
119
+ updatedAt: linkedAt,
120
+ }
121
+ : existing;
122
+ if (updated) {
123
+ await input.store.put(record.id, record);
124
+ }
125
+ results.push({
126
+ friendId: record.id,
127
+ name: record.name,
128
+ trustLevel,
129
+ created: false,
130
+ updated,
131
+ addedGroupExternalId: !hasGroupExternalId,
132
+ });
133
+ }
134
+ (0, runtime_1.emitNervesEvent)({
135
+ component: "friends",
136
+ event: "friends.group_context_upsert_end",
137
+ message: "upserted shared-group participant context",
138
+ meta: {
139
+ participantCount: participants.length,
140
+ updatedCount: results.filter((result) => result.created || result.updated).length,
141
+ },
142
+ });
143
+ return results;
144
+ }
@@ -100,6 +100,25 @@ class FileFriendStore {
100
100
  }
101
101
  return entries.some((entry) => entry.endsWith(".json"));
102
102
  }
103
+ async listAll() {
104
+ let entries;
105
+ try {
106
+ entries = await fsPromises.readdir(this.friendsPath);
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ const records = [];
112
+ for (const entry of entries) {
113
+ if (!entry.endsWith(".json"))
114
+ continue;
115
+ const raw = await this.readJson(path.join(this.friendsPath, entry));
116
+ if (!raw)
117
+ continue;
118
+ records.push(this.normalize(raw));
119
+ }
120
+ return records;
121
+ }
103
122
  normalize(raw) {
104
123
  const trustLevel = raw.trustLevel;
105
124
  const normalizedTrustLevel = trustLevel === "family" ||
@@ -108,6 +127,8 @@ class FileFriendStore {
108
127
  trustLevel === "stranger"
109
128
  ? trustLevel
110
129
  : DEFAULT_TRUST_LEVEL;
130
+ const kind = raw.kind === "human" || raw.kind === "agent" ? raw.kind : "human";
131
+ const agentMeta = kind === "agent" ? this.normalizeAgentMeta(raw.agentMeta) : undefined;
111
132
  return {
112
133
  id: raw.id,
113
134
  name: raw.name,
@@ -134,6 +155,21 @@ class FileFriendStore {
134
155
  createdAt: typeof raw.createdAt === "string" ? raw.createdAt : new Date().toISOString(),
135
156
  updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : new Date().toISOString(),
136
157
  schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : 1,
158
+ kind,
159
+ agentMeta,
160
+ };
161
+ }
162
+ normalizeAgentMeta(raw) {
163
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
164
+ return undefined;
165
+ const meta = raw;
166
+ if (typeof meta.bundleName !== "string")
167
+ return undefined;
168
+ return {
169
+ bundleName: meta.bundleName,
170
+ familiarity: typeof meta.familiarity === "number" ? meta.familiarity : 0,
171
+ sharedMissions: Array.isArray(meta.sharedMissions) ? meta.sharedMissions : [],
172
+ outcomes: Array.isArray(meta.outcomes) ? meta.outcomes : [],
137
173
  };
138
174
  }
139
175
  async readJson(filePath) {
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeTrustContext = describeTrustContext;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ function findRelatedGroupId(friend) {
6
+ return friend.externalIds.find((externalId) => externalId.externalId.startsWith("group:"))?.externalId;
7
+ }
8
+ function resolveLevel(friend) {
9
+ return friend.trustLevel ?? "stranger";
10
+ }
11
+ function describeTrustContext(input) {
12
+ const level = resolveLevel(input.friend);
13
+ const relatedGroupId = findRelatedGroupId(input.friend);
14
+ const explanation = level === "family" || level === "friend"
15
+ ? {
16
+ level,
17
+ basis: "direct",
18
+ summary: level === "family"
19
+ ? "direct family trust"
20
+ : "direct trusted relationship",
21
+ why: "this relationship is directly trusted rather than inferred through a shared group or cold first contact.",
22
+ permits: [
23
+ "local operations when appropriate",
24
+ "proactive follow-through",
25
+ "full collaborative problem solving",
26
+ ],
27
+ constraints: [],
28
+ }
29
+ : level === "acquaintance"
30
+ ? {
31
+ level,
32
+ basis: "shared_group",
33
+ summary: relatedGroupId
34
+ ? "known through the shared project group"
35
+ : "known through a shared group context",
36
+ why: relatedGroupId
37
+ ? `this trust comes from the shared group context ${relatedGroupId}, not from direct endorsement.`
38
+ : "this trust comes from shared group context rather than direct endorsement.",
39
+ permits: [
40
+ "group-safe coordination",
41
+ "normal conversation inside the shared context",
42
+ ],
43
+ constraints: [
44
+ "guarded local actions",
45
+ "do not assume broad private authority",
46
+ ],
47
+ relatedGroupId,
48
+ }
49
+ : {
50
+ level,
51
+ basis: "unknown",
52
+ summary: "truly unknown first-contact context",
53
+ why: "this person is not known through direct trust or a shared group context.",
54
+ permits: [
55
+ "safe first-contact orientation only",
56
+ ],
57
+ constraints: [
58
+ "first contact does not reach the full model on open channels",
59
+ "no local or privileged actions",
60
+ ],
61
+ };
62
+ (0, runtime_1.emitNervesEvent)({
63
+ component: "friends",
64
+ event: "friends.trust_explained",
65
+ message: "built explicit trust explanation",
66
+ meta: {
67
+ channel: input.channel,
68
+ level: explanation.level,
69
+ basis: explanation.basis,
70
+ hasRelatedGroup: Boolean(explanation.relatedGroupId),
71
+ },
72
+ });
73
+ return explanation;
74
+ }
@@ -2,8 +2,10 @@
2
2
  // Context kernel type definitions.
3
3
  // FriendRecord (merged identity + memory), channel capabilities, and resolved context.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.TRUSTED_LEVELS = void 0;
5
6
  exports.isIdentityProvider = isIdentityProvider;
6
7
  exports.isIntegration = isIntegration;
8
+ exports.isTrustedLevel = isTrustedLevel;
7
9
  const runtime_1 = require("../../nerves/runtime");
8
10
  const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
9
11
  function isIdentityProvider(value) {
@@ -19,3 +21,9 @@ const INTEGRATIONS = new Set(["ado", "github", "graph"]);
19
21
  function isIntegration(value) {
20
22
  return typeof value === "string" && INTEGRATIONS.has(value);
21
23
  }
24
+ /** Trust levels that grant full tool access and proactive send capability. */
25
+ exports.TRUSTED_LEVELS = new Set(["family", "friend"]);
26
+ /** Whether a trust level grants full access (family or friend). Defaults to "friend" for legacy records. */
27
+ function isTrustedLevel(trustLevel) {
28
+ return exports.TRUSTED_LEVELS.has(trustLevel ?? "friend");
29
+ }