@ouro.bot/cli 0.1.0-alpha.9 → 0.1.0-alpha.91
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.
- package/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +147 -205
- package/assets/ouroboros.png +0 -0
- package/changelog.json +536 -0
- package/dist/heart/active-work.js +251 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +109 -0
- package/dist/heart/config.js +68 -23
- package/dist/heart/core.js +452 -93
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/auth-flow.js +430 -0
- package/dist/heart/daemon/daemon-cli.js +1738 -269
- package/dist/heart/daemon/daemon-entry.js +55 -6
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +216 -10
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +7 -82
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/ouro-bot-entry.js +0 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-entry.js +0 -0
- package/dist/heart/daemon/ouro-path-installer.js +260 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +14 -1
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +307 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +53 -84
- package/dist/heart/daemon/specialist-prompt.js +63 -11
- package/dist/heart/daemon/specialist-tools.js +211 -60
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +126 -21
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +191 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/providers/anthropic.js +74 -9
- package/dist/heart/providers/azure.js +86 -7
- package/dist/heart/providers/github-copilot.js +149 -0
- package/dist/heart/providers/minimax.js +4 -0
- package/dist/heart/providers/openai-codex.js +12 -3
- package/dist/heart/safe-workspace.js +362 -0
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/session-activity.js +169 -0
- package/dist/heart/session-recall.js +116 -0
- package/dist/heart/streaming.js +100 -22
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +14 -2
- package/dist/mind/bundle-manifest.js +70 -0
- package/dist/mind/context.js +57 -11
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/memory.js +27 -26
- package/dist/mind/obligation-steering.js +31 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +467 -77
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +12 -0
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +180 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +69 -4
- package/dist/repertoire/coding/spawner.js +21 -3
- package/dist/repertoire/coding/tools.js +105 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +195 -0
- package/dist/repertoire/skills.js +3 -26
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +714 -249
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +106 -53
- package/dist/senses/bluebubbles-client.js +210 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +894 -45
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +400 -164
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +377 -83
- package/dist/senses/pipeline.js +307 -0
- package/dist/senses/teams.js +573 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +14 -3
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/specialist-session.js +0 -142
- package/dist/heart/daemon/subagent-installer.js +0 -125
- package/dist/inner-worker-entry.js +0 -4
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.suggestBridgeForActiveWork = suggestBridgeForActiveWork;
|
|
4
|
+
exports.buildActiveWorkFrame = buildActiveWorkFrame;
|
|
5
|
+
exports.formatActiveWorkFrame = formatActiveWorkFrame;
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
const state_machine_1 = require("./bridges/state-machine");
|
|
8
|
+
const obligations_1 = require("./obligations");
|
|
9
|
+
const target_resolution_1 = require("./target-resolution");
|
|
10
|
+
function activityPriority(source) {
|
|
11
|
+
return source === "friend-facing" ? 0 : 1;
|
|
12
|
+
}
|
|
13
|
+
function compareActivity(a, b) {
|
|
14
|
+
const sourceDiff = activityPriority(a.activitySource) - activityPriority(b.activitySource);
|
|
15
|
+
if (sourceDiff !== 0)
|
|
16
|
+
return sourceDiff;
|
|
17
|
+
return b.lastActivityMs - a.lastActivityMs;
|
|
18
|
+
}
|
|
19
|
+
function summarizeLiveTasks(taskBoard) {
|
|
20
|
+
const live = [
|
|
21
|
+
...taskBoard.byStatus.processing,
|
|
22
|
+
...taskBoard.byStatus.validating,
|
|
23
|
+
...taskBoard.byStatus.collaborating,
|
|
24
|
+
];
|
|
25
|
+
return [...new Set(live)];
|
|
26
|
+
}
|
|
27
|
+
function isActiveBridge(bridge) {
|
|
28
|
+
return bridge.lifecycle === "active";
|
|
29
|
+
}
|
|
30
|
+
function hasSharedObligationPressure(input) {
|
|
31
|
+
return (typeof input.currentObligation === "string"
|
|
32
|
+
&& input.currentObligation.trim().length > 0) || input.mustResolveBeforeHandoff
|
|
33
|
+
|| summarizeLiveTasks(input.taskBoard).length > 0;
|
|
34
|
+
}
|
|
35
|
+
function activeObligationCount(obligations) {
|
|
36
|
+
return (obligations ?? []).filter((ob) => (0, obligations_1.isOpenObligationStatus)(ob.status)).length;
|
|
37
|
+
}
|
|
38
|
+
function formatObligationSurface(obligation) {
|
|
39
|
+
if (!obligation.currentSurface?.label)
|
|
40
|
+
return "";
|
|
41
|
+
switch (obligation.status) {
|
|
42
|
+
case "investigating":
|
|
43
|
+
return ` (working in ${obligation.currentSurface.label})`;
|
|
44
|
+
case "waiting_for_merge":
|
|
45
|
+
return ` (waiting at ${obligation.currentSurface.label})`;
|
|
46
|
+
case "updating_runtime":
|
|
47
|
+
return ` (updating via ${obligation.currentSurface.label})`;
|
|
48
|
+
default:
|
|
49
|
+
return ` (${obligation.currentSurface.label})`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function suggestBridgeForActiveWork(input) {
|
|
53
|
+
const targetCandidates = (input.targetCandidates ?? [])
|
|
54
|
+
.filter((candidate) => {
|
|
55
|
+
if (candidate.delivery.mode === "blocked") {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (candidate.activitySource !== "friend-facing" || candidate.channel === "inner") {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (!input.currentSession) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return !(candidate.friendId === input.currentSession.friendId
|
|
65
|
+
&& candidate.channel === input.currentSession.channel
|
|
66
|
+
&& candidate.key === input.currentSession.key);
|
|
67
|
+
})
|
|
68
|
+
.sort((a, b) => {
|
|
69
|
+
return b.lastActivityMs - a.lastActivityMs;
|
|
70
|
+
});
|
|
71
|
+
if (!hasSharedObligationPressure(input) || targetCandidates.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const targetSession = targetCandidates[0];
|
|
75
|
+
const activeBridge = input.bridges.find(isActiveBridge) ?? null;
|
|
76
|
+
if (activeBridge) {
|
|
77
|
+
const alreadyAttached = activeBridge.attachedSessions.some((session) => session.friendId === targetSession.friendId
|
|
78
|
+
&& session.channel === targetSession.channel
|
|
79
|
+
&& session.key === targetSession.key);
|
|
80
|
+
if (alreadyAttached) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
kind: "attach-existing",
|
|
85
|
+
bridgeId: activeBridge.id,
|
|
86
|
+
targetSession,
|
|
87
|
+
reason: "shared-work-candidate",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: "begin-new",
|
|
92
|
+
targetSession,
|
|
93
|
+
objectiveHint: input.currentObligation?.trim() || "keep this shared work aligned",
|
|
94
|
+
reason: "shared-work-candidate",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function formatSessionLabel(session) {
|
|
98
|
+
return `${session.channel}/${session.key}`;
|
|
99
|
+
}
|
|
100
|
+
function buildActiveWorkFrame(input) {
|
|
101
|
+
const friendSessions = input.currentSession
|
|
102
|
+
? input.friendActivity
|
|
103
|
+
.filter((entry) => entry.friendId === input.currentSession?.friendId)
|
|
104
|
+
.sort(compareActivity)
|
|
105
|
+
: [];
|
|
106
|
+
const liveTaskNames = summarizeLiveTasks(input.taskBoard);
|
|
107
|
+
const activeBridgePresent = input.bridges.some(isActiveBridge);
|
|
108
|
+
const openObligations = activeObligationCount(input.pendingObligations);
|
|
109
|
+
const centerOfGravity = activeBridgePresent
|
|
110
|
+
? "shared-work"
|
|
111
|
+
: (input.inner.status === "running" || input.inner.hasPending || input.mustResolveBeforeHandoff || openObligations > 0)
|
|
112
|
+
? "inward-work"
|
|
113
|
+
: "local-turn";
|
|
114
|
+
const frame = {
|
|
115
|
+
currentSession: input.currentSession ?? null,
|
|
116
|
+
currentObligation: input.currentObligation?.trim() || null,
|
|
117
|
+
mustResolveBeforeHandoff: input.mustResolveBeforeHandoff,
|
|
118
|
+
centerOfGravity,
|
|
119
|
+
inner: input.inner,
|
|
120
|
+
bridges: input.bridges,
|
|
121
|
+
taskPressure: {
|
|
122
|
+
compactBoard: input.taskBoard.compact,
|
|
123
|
+
liveTaskNames,
|
|
124
|
+
activeBridges: input.taskBoard.activeBridges,
|
|
125
|
+
},
|
|
126
|
+
friendActivity: {
|
|
127
|
+
freshestForCurrentFriend: friendSessions[0] ?? null,
|
|
128
|
+
otherLiveSessionsForCurrentFriend: friendSessions,
|
|
129
|
+
},
|
|
130
|
+
pendingObligations: input.pendingObligations ?? [],
|
|
131
|
+
targetCandidates: input.targetCandidates ?? [],
|
|
132
|
+
bridgeSuggestion: suggestBridgeForActiveWork({
|
|
133
|
+
currentSession: input.currentSession,
|
|
134
|
+
currentObligation: input.currentObligation,
|
|
135
|
+
mustResolveBeforeHandoff: input.mustResolveBeforeHandoff,
|
|
136
|
+
bridges: input.bridges,
|
|
137
|
+
taskBoard: input.taskBoard,
|
|
138
|
+
targetCandidates: input.targetCandidates,
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
(0, runtime_1.emitNervesEvent)({
|
|
142
|
+
component: "engine",
|
|
143
|
+
event: "engine.active_work_build",
|
|
144
|
+
message: "built shared active-work frame",
|
|
145
|
+
meta: {
|
|
146
|
+
centerOfGravity: frame.centerOfGravity,
|
|
147
|
+
friendId: frame.currentSession?.friendId ?? null,
|
|
148
|
+
bridges: frame.bridges.length,
|
|
149
|
+
liveTasks: frame.taskPressure.liveTaskNames.length,
|
|
150
|
+
liveSessions: frame.friendActivity.otherLiveSessionsForCurrentFriend.length,
|
|
151
|
+
pendingObligations: openObligations,
|
|
152
|
+
hasBridgeSuggestion: frame.bridgeSuggestion !== null,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return frame;
|
|
156
|
+
}
|
|
157
|
+
function formatActiveWorkFrame(frame) {
|
|
158
|
+
const lines = ["## what i'm holding"];
|
|
159
|
+
// Session line
|
|
160
|
+
if (frame.currentSession) {
|
|
161
|
+
let sessionLine = `i'm in a conversation on ${formatSessionLabel(frame.currentSession)}.`;
|
|
162
|
+
if (typeof frame.currentObligation === "string" && frame.currentObligation.trim().length > 0) {
|
|
163
|
+
sessionLine += ` i told them i'd ${frame.currentObligation.trim()}.`;
|
|
164
|
+
}
|
|
165
|
+
else if (frame.mustResolveBeforeHandoff) {
|
|
166
|
+
sessionLine += " i need to finish what i started here before moving on.";
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push(sessionLine);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push("i'm not in a conversation right now.");
|
|
174
|
+
}
|
|
175
|
+
// Inner status block
|
|
176
|
+
const job = frame.inner?.job;
|
|
177
|
+
if (job) {
|
|
178
|
+
if (job.status === "queued") {
|
|
179
|
+
let queuedLine = "i have a thought queued up for private attention.";
|
|
180
|
+
if (frame.inner?.contentSnippet) {
|
|
181
|
+
queuedLine += `\nit's about: "${frame.inner.contentSnippet}"`;
|
|
182
|
+
}
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(queuedLine);
|
|
185
|
+
}
|
|
186
|
+
else if (job.status === "running") {
|
|
187
|
+
const originName = job.origin?.friendName ?? job.origin?.friendId;
|
|
188
|
+
let runningLine = originName
|
|
189
|
+
? `i'm thinking through something privately right now. ${originName} asked about something and i wanted to give it real thought.`
|
|
190
|
+
: "i'm thinking through something privately right now.";
|
|
191
|
+
if (frame.inner?.obligationPending) {
|
|
192
|
+
runningLine += "\ni still owe them an answer.";
|
|
193
|
+
}
|
|
194
|
+
lines.push("");
|
|
195
|
+
lines.push(runningLine);
|
|
196
|
+
}
|
|
197
|
+
else if (job.status === "surfaced") {
|
|
198
|
+
let surfacedLine = "i finished thinking about something privately. i should bring my answer back.";
|
|
199
|
+
if (job.surfacedResult) {
|
|
200
|
+
const truncated = job.surfacedResult.length > 120 ? job.surfacedResult.slice(0, 117) + "..." : job.surfacedResult;
|
|
201
|
+
surfacedLine += `\nwhat i came to: ${truncated}`;
|
|
202
|
+
}
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push(surfacedLine);
|
|
205
|
+
}
|
|
206
|
+
// idle, returned, abandoned: omitted
|
|
207
|
+
}
|
|
208
|
+
// Task pressure
|
|
209
|
+
if ((frame.taskPressure?.liveTaskNames ?? []).length > 0) {
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push(`i'm also tracking: ${frame.taskPressure.liveTaskNames.join(", ")}.`);
|
|
212
|
+
}
|
|
213
|
+
// Bridges
|
|
214
|
+
if ((frame.bridges ?? []).length > 0) {
|
|
215
|
+
const bridgeLabels = frame.bridges.map((bridge) => `${bridge.id} [${(0, state_machine_1.bridgeStateLabel)(bridge)}]`);
|
|
216
|
+
lines.push("");
|
|
217
|
+
lines.push(`i have shared work spanning sessions: ${bridgeLabels.join(", ")}.`);
|
|
218
|
+
}
|
|
219
|
+
// Target candidates (keep factual format)
|
|
220
|
+
const targetCandidatesBlock = frame.targetCandidates && frame.targetCandidates.length > 0
|
|
221
|
+
? (0, target_resolution_1.formatTargetSessionCandidates)(frame.targetCandidates)
|
|
222
|
+
: "";
|
|
223
|
+
if (targetCandidatesBlock) {
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push(targetCandidatesBlock);
|
|
226
|
+
}
|
|
227
|
+
if ((frame.pendingObligations ?? []).length > 0) {
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push("## return obligations");
|
|
230
|
+
for (const obligation of frame.pendingObligations) {
|
|
231
|
+
if (!(0, obligations_1.isOpenObligationStatus)(obligation.status))
|
|
232
|
+
continue;
|
|
233
|
+
let obligationLine = `- [${obligation.status}] ${obligation.origin.friendId}/${obligation.origin.channel}/${obligation.origin.key}: ${obligation.content}${formatObligationSurface(obligation)}`;
|
|
234
|
+
if (obligation.latestNote?.trim()) {
|
|
235
|
+
obligationLine += `\n latest: ${obligation.latestNote.trim()}`;
|
|
236
|
+
}
|
|
237
|
+
lines.push(obligationLine);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Bridge suggestion
|
|
241
|
+
if (frame.bridgeSuggestion) {
|
|
242
|
+
lines.push("");
|
|
243
|
+
if (frame.bridgeSuggestion.kind === "begin-new") {
|
|
244
|
+
lines.push(`this work touches my conversation on ${formatSessionLabel(frame.bridgeSuggestion.targetSession)} too -- i should connect these threads.`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
lines.push(`this work relates to bridge ${frame.bridgeSuggestion.bridgeId} -- i should connect ${formatSessionLabel(frame.bridgeSuggestion.targetSession)} to it.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return lines.join("\n");
|
|
251
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatBridgeStatus = formatBridgeStatus;
|
|
4
|
+
exports.formatBridgeContext = formatBridgeContext;
|
|
5
|
+
exports.createBridgeManager = createBridgeManager;
|
|
6
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
7
|
+
const state_machine_1 = require("./state-machine");
|
|
8
|
+
const store_1 = require("./store");
|
|
9
|
+
const turn_coordinator_1 = require("../turn-coordinator");
|
|
10
|
+
const tasks_1 = require("../../repertoire/tasks");
|
|
11
|
+
function defaultIdFactory() {
|
|
12
|
+
return `bridge-${Date.now().toString(36)}`;
|
|
13
|
+
}
|
|
14
|
+
function sessionIdentityKey(session) {
|
|
15
|
+
return `${session.friendId}/${session.channel}/${session.key}`;
|
|
16
|
+
}
|
|
17
|
+
function assertBridgeMutable(bridge, action) {
|
|
18
|
+
if (bridge.lifecycle === "completed" || bridge.lifecycle === "cancelled") {
|
|
19
|
+
throw new Error(`cannot ${action} a terminal bridge`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function defaultTaskBody(bridge) {
|
|
23
|
+
const lines = [
|
|
24
|
+
"## scope",
|
|
25
|
+
bridge.objective,
|
|
26
|
+
"",
|
|
27
|
+
"## bridge",
|
|
28
|
+
`id: ${bridge.id}`,
|
|
29
|
+
];
|
|
30
|
+
if (bridge.attachedSessions.length > 0) {
|
|
31
|
+
lines.push("sessions:");
|
|
32
|
+
for (const session of bridge.attachedSessions) {
|
|
33
|
+
lines.push(`- ${sessionIdentityKey(session)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function formatBridgeStatus(bridge) {
|
|
39
|
+
const summary = typeof bridge.summary === "string" ? bridge.summary.trim() : "";
|
|
40
|
+
const lines = [
|
|
41
|
+
`bridge: ${bridge.id}`,
|
|
42
|
+
`objective: ${bridge.objective}`,
|
|
43
|
+
`state: ${(0, state_machine_1.bridgeStateLabel)(bridge)}`,
|
|
44
|
+
`sessions: ${bridge.attachedSessions.length}`,
|
|
45
|
+
`task: ${bridge.task?.taskName ?? "none"}`,
|
|
46
|
+
];
|
|
47
|
+
if (summary) {
|
|
48
|
+
lines.push(`summary: ${summary}`);
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
function formatBridgeContext(bridges) {
|
|
53
|
+
if (bridges.length === 0)
|
|
54
|
+
return "";
|
|
55
|
+
const lines = ["## active bridge work"];
|
|
56
|
+
for (const bridge of bridges) {
|
|
57
|
+
const task = bridge.task?.taskName ? ` (task: ${bridge.task.taskName})` : "";
|
|
58
|
+
const label = typeof bridge.summary === "string" && bridge.summary.trim().length > 0 ? bridge.summary.trim() : bridge.objective;
|
|
59
|
+
lines.push(`- ${bridge.id}: ${label} [${(0, state_machine_1.bridgeStateLabel)(bridge)}]${task}`);
|
|
60
|
+
}
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
function ensureRunnable(bridge, now, store) {
|
|
64
|
+
if (bridge.lifecycle === "forming" || bridge.lifecycle === "suspended") {
|
|
65
|
+
const activated = {
|
|
66
|
+
...bridge,
|
|
67
|
+
...(0, state_machine_1.activateBridge)(bridge),
|
|
68
|
+
updatedAt: now(),
|
|
69
|
+
};
|
|
70
|
+
return store.save(activated);
|
|
71
|
+
}
|
|
72
|
+
if (bridge.lifecycle === "completed" || bridge.lifecycle === "cancelled") {
|
|
73
|
+
throw new Error(`bridge is terminal: ${bridge.id}`);
|
|
74
|
+
}
|
|
75
|
+
return bridge;
|
|
76
|
+
}
|
|
77
|
+
function sessionMatches(left, right) {
|
|
78
|
+
return left.friendId === right.friendId && left.channel === right.channel && left.key === right.key;
|
|
79
|
+
}
|
|
80
|
+
function hasAttachedSessionActivity(bridge, sessionActivity) {
|
|
81
|
+
return sessionActivity.some((activity) => activity.channel !== "inner"
|
|
82
|
+
&& bridge.attachedSessions.some((session) => sessionMatches(activity, session)));
|
|
83
|
+
}
|
|
84
|
+
function hasLiveTaskStatus(bridge, taskBoard) {
|
|
85
|
+
const taskName = bridge.task?.taskName;
|
|
86
|
+
if (!taskName)
|
|
87
|
+
return false;
|
|
88
|
+
return (taskBoard.byStatus.processing.includes(taskName)
|
|
89
|
+
|| taskBoard.byStatus.collaborating.includes(taskName)
|
|
90
|
+
|| taskBoard.byStatus.validating.includes(taskName));
|
|
91
|
+
}
|
|
92
|
+
function isCurrentSessionAttached(bridge, currentSession) {
|
|
93
|
+
if (!currentSession)
|
|
94
|
+
return false;
|
|
95
|
+
return bridge.attachedSessions.some((session) => sessionMatches(session, currentSession));
|
|
96
|
+
}
|
|
97
|
+
function createBridgeManager(options = {}) {
|
|
98
|
+
const store = options.store ?? (0, store_1.createBridgeStore)();
|
|
99
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
100
|
+
const idFactory = options.idFactory ?? defaultIdFactory;
|
|
101
|
+
function requireBridge(bridgeId) {
|
|
102
|
+
const bridge = store.get(bridgeId);
|
|
103
|
+
if (!bridge) {
|
|
104
|
+
throw new Error(`bridge not found: ${bridgeId}`);
|
|
105
|
+
}
|
|
106
|
+
return bridge;
|
|
107
|
+
}
|
|
108
|
+
function save(bridge) {
|
|
109
|
+
return store.save(bridge);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
beginBridge(input) {
|
|
113
|
+
const timestamp = now();
|
|
114
|
+
const state = (0, state_machine_1.activateBridge)((0, state_machine_1.createBridgeState)());
|
|
115
|
+
const bridge = {
|
|
116
|
+
id: idFactory(),
|
|
117
|
+
objective: input.objective,
|
|
118
|
+
summary: input.summary,
|
|
119
|
+
lifecycle: state.lifecycle,
|
|
120
|
+
runtime: state.runtime,
|
|
121
|
+
createdAt: timestamp,
|
|
122
|
+
updatedAt: timestamp,
|
|
123
|
+
attachedSessions: [input.session],
|
|
124
|
+
task: null,
|
|
125
|
+
};
|
|
126
|
+
(0, runtime_1.emitNervesEvent)({
|
|
127
|
+
component: "engine",
|
|
128
|
+
event: "engine.bridge_begin",
|
|
129
|
+
message: "created bridge",
|
|
130
|
+
meta: {
|
|
131
|
+
bridgeId: bridge.id,
|
|
132
|
+
session: sessionIdentityKey(input.session),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return save(bridge);
|
|
136
|
+
},
|
|
137
|
+
attachSession(bridgeId, session) {
|
|
138
|
+
const bridge = requireBridge(bridgeId);
|
|
139
|
+
assertBridgeMutable(bridge, "attach session to");
|
|
140
|
+
const existing = bridge.attachedSessions.some((candidate) => sessionIdentityKey(candidate) === sessionIdentityKey(session));
|
|
141
|
+
if (existing)
|
|
142
|
+
return bridge;
|
|
143
|
+
const updated = {
|
|
144
|
+
...bridge,
|
|
145
|
+
attachedSessions: [...bridge.attachedSessions, session],
|
|
146
|
+
updatedAt: now(),
|
|
147
|
+
};
|
|
148
|
+
(0, runtime_1.emitNervesEvent)({
|
|
149
|
+
component: "engine",
|
|
150
|
+
event: "engine.bridge_attach_session",
|
|
151
|
+
message: "attached canonical session to bridge",
|
|
152
|
+
meta: {
|
|
153
|
+
bridgeId,
|
|
154
|
+
session: sessionIdentityKey(session),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return save(updated);
|
|
158
|
+
},
|
|
159
|
+
detachSession(bridgeId, session) {
|
|
160
|
+
const bridge = requireBridge(bridgeId);
|
|
161
|
+
assertBridgeMutable(bridge, "detach session from");
|
|
162
|
+
const updated = {
|
|
163
|
+
...bridge,
|
|
164
|
+
attachedSessions: bridge.attachedSessions.filter((candidate) => sessionIdentityKey(candidate) !== sessionIdentityKey(session)),
|
|
165
|
+
updatedAt: now(),
|
|
166
|
+
};
|
|
167
|
+
(0, runtime_1.emitNervesEvent)({
|
|
168
|
+
component: "engine",
|
|
169
|
+
event: "engine.bridge_detach_session",
|
|
170
|
+
message: "detached canonical session from bridge",
|
|
171
|
+
meta: {
|
|
172
|
+
bridgeId,
|
|
173
|
+
session: sessionIdentityKey(session),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
return save(updated);
|
|
177
|
+
},
|
|
178
|
+
getBridge(bridgeId) {
|
|
179
|
+
return store.get(bridgeId);
|
|
180
|
+
},
|
|
181
|
+
listBridges() {
|
|
182
|
+
return store.list();
|
|
183
|
+
},
|
|
184
|
+
findBridgesForSession(session) {
|
|
185
|
+
return store.findBySession(session)
|
|
186
|
+
.filter((bridge) => bridge.lifecycle !== "completed" && bridge.lifecycle !== "cancelled");
|
|
187
|
+
},
|
|
188
|
+
reconcileLifecycles(input) {
|
|
189
|
+
return store.list().map((bridge) => {
|
|
190
|
+
const nextState = (0, state_machine_1.reconcileBridgeState)(bridge, {
|
|
191
|
+
hasAttachedSessionActivity: hasAttachedSessionActivity(bridge, input.sessionActivity),
|
|
192
|
+
hasLiveTask: hasLiveTaskStatus(bridge, input.taskBoard),
|
|
193
|
+
currentSessionAttached: isCurrentSessionAttached(bridge, input.currentSession),
|
|
194
|
+
});
|
|
195
|
+
if (nextState.lifecycle === bridge.lifecycle && nextState.runtime === bridge.runtime) {
|
|
196
|
+
return bridge;
|
|
197
|
+
}
|
|
198
|
+
return save({
|
|
199
|
+
...bridge,
|
|
200
|
+
...nextState,
|
|
201
|
+
updatedAt: now(),
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
promoteBridgeToTask(bridgeId, input = {}) {
|
|
206
|
+
const bridge = requireBridge(bridgeId);
|
|
207
|
+
assertBridgeMutable(bridge, "promote");
|
|
208
|
+
if (bridge.task)
|
|
209
|
+
return bridge;
|
|
210
|
+
const taskPath = (0, tasks_1.getTaskModule)().createTask({
|
|
211
|
+
title: input.title?.trim() || bridge.objective,
|
|
212
|
+
type: "ongoing",
|
|
213
|
+
category: input.category?.trim() || "coordination",
|
|
214
|
+
status: "processing",
|
|
215
|
+
body: input.body?.trim() || defaultTaskBody(bridge),
|
|
216
|
+
activeBridge: bridge.id,
|
|
217
|
+
bridgeSessions: bridge.attachedSessions.map((session) => sessionIdentityKey(session)),
|
|
218
|
+
});
|
|
219
|
+
const taskName = taskPath.replace(/^.*\//, "").replace(/\.md$/, "");
|
|
220
|
+
const updated = save({
|
|
221
|
+
...bridge,
|
|
222
|
+
task: {
|
|
223
|
+
taskName,
|
|
224
|
+
path: taskPath,
|
|
225
|
+
mode: "promoted",
|
|
226
|
+
boundAt: now(),
|
|
227
|
+
},
|
|
228
|
+
updatedAt: now(),
|
|
229
|
+
});
|
|
230
|
+
(0, runtime_1.emitNervesEvent)({
|
|
231
|
+
component: "engine",
|
|
232
|
+
event: "engine.bridge_promote_task",
|
|
233
|
+
message: "promoted bridge to task-backed work",
|
|
234
|
+
meta: {
|
|
235
|
+
bridgeId,
|
|
236
|
+
taskName,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
return updated;
|
|
240
|
+
},
|
|
241
|
+
completeBridge(bridgeId) {
|
|
242
|
+
const bridge = requireBridge(bridgeId);
|
|
243
|
+
const updated = save({
|
|
244
|
+
...bridge,
|
|
245
|
+
...(0, state_machine_1.completeBridge)(bridge),
|
|
246
|
+
updatedAt: now(),
|
|
247
|
+
});
|
|
248
|
+
(0, runtime_1.emitNervesEvent)({
|
|
249
|
+
component: "engine",
|
|
250
|
+
event: "engine.bridge_complete",
|
|
251
|
+
message: "completed bridge",
|
|
252
|
+
meta: {
|
|
253
|
+
bridgeId,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
return updated;
|
|
257
|
+
},
|
|
258
|
+
cancelBridge(bridgeId) {
|
|
259
|
+
const bridge = requireBridge(bridgeId);
|
|
260
|
+
const updated = save({
|
|
261
|
+
...bridge,
|
|
262
|
+
...(0, state_machine_1.cancelBridge)(bridge),
|
|
263
|
+
updatedAt: now(),
|
|
264
|
+
});
|
|
265
|
+
(0, runtime_1.emitNervesEvent)({
|
|
266
|
+
component: "engine",
|
|
267
|
+
event: "engine.bridge_cancel",
|
|
268
|
+
message: "cancelled bridge",
|
|
269
|
+
meta: {
|
|
270
|
+
bridgeId,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
return updated;
|
|
274
|
+
},
|
|
275
|
+
async runBridgeTurn(bridgeId, fn) {
|
|
276
|
+
if (!(0, turn_coordinator_1.tryBeginSharedTurn)("bridge", bridgeId)) {
|
|
277
|
+
const bridge = requireBridge(bridgeId);
|
|
278
|
+
const queued = bridge.runtime === "awaiting-follow-up"
|
|
279
|
+
? bridge
|
|
280
|
+
: save({
|
|
281
|
+
...bridge,
|
|
282
|
+
...(0, state_machine_1.queueBridgeFollowUp)(bridge),
|
|
283
|
+
updatedAt: now(),
|
|
284
|
+
});
|
|
285
|
+
(0, turn_coordinator_1.enqueueSharedFollowUp)("bridge", bridgeId, {
|
|
286
|
+
conversationId: bridgeId,
|
|
287
|
+
text: "bridge follow-up",
|
|
288
|
+
receivedAt: Date.now(),
|
|
289
|
+
effect: "none",
|
|
290
|
+
});
|
|
291
|
+
(0, runtime_1.emitNervesEvent)({
|
|
292
|
+
component: "engine",
|
|
293
|
+
event: "engine.bridge_turn_queued",
|
|
294
|
+
message: "queued follow-up bridge turn",
|
|
295
|
+
meta: {
|
|
296
|
+
bridgeId,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
queued: true,
|
|
301
|
+
bridge: queued,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
let current = ensureRunnable(requireBridge(bridgeId), now, store);
|
|
306
|
+
current = save({
|
|
307
|
+
...current,
|
|
308
|
+
...(0, state_machine_1.beginBridgeProcessing)(current),
|
|
309
|
+
updatedAt: now(),
|
|
310
|
+
});
|
|
311
|
+
while (true) {
|
|
312
|
+
(0, runtime_1.emitNervesEvent)({
|
|
313
|
+
component: "engine",
|
|
314
|
+
event: "engine.bridge_turn_start",
|
|
315
|
+
message: "running bridge turn",
|
|
316
|
+
meta: {
|
|
317
|
+
bridgeId,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
await fn();
|
|
321
|
+
let next = requireBridge(bridgeId);
|
|
322
|
+
const bufferedFollowUps = (0, turn_coordinator_1.drainSharedFollowUps)("bridge", bridgeId);
|
|
323
|
+
if (bufferedFollowUps.length > 0 && next.runtime !== "awaiting-follow-up") {
|
|
324
|
+
next = save({
|
|
325
|
+
...next,
|
|
326
|
+
...(0, state_machine_1.queueBridgeFollowUp)(next),
|
|
327
|
+
updatedAt: now(),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const advanced = save({
|
|
331
|
+
...next,
|
|
332
|
+
...(0, state_machine_1.advanceBridgeAfterTurn)(next),
|
|
333
|
+
updatedAt: now(),
|
|
334
|
+
});
|
|
335
|
+
if (advanced.runtime === "processing") {
|
|
336
|
+
current = advanced;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
(0, runtime_1.emitNervesEvent)({
|
|
340
|
+
component: "engine",
|
|
341
|
+
event: "engine.bridge_turn_end",
|
|
342
|
+
message: "bridge turn finished",
|
|
343
|
+
meta: {
|
|
344
|
+
bridgeId,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
queued: false,
|
|
349
|
+
bridge: current = advanced,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
(0, turn_coordinator_1.endSharedTurn)("bridge", bridgeId);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|