@ouro.bot/cli 0.1.0-alpha.925 → 0.1.0-alpha.926

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/changelog.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.926",
6
+ "changes": [
7
+ "Open a compact independent Ouro session composer with short About labels, draft preservation, canonical replies and no blind resubmission.",
8
+ "Install a managed cmux Swift Talk to Ouro entry that opens a browser split without moving coding focus; pin each conversation independently and retain the existing family-only terminal authority.",
9
+ "Preserve the original session and request identity through lost acknowledgements and listener restarts; refresh choices without retargeting, recover definitively refused starts, and keep one-off web views from replacing the managed native entry.",
10
+ "Validate the UTF-8 request budget before arming a send, keep explicitly oversized unsent drafts editable, and decode multibyte HTTP messages without corruption; an earlier unknown acknowledgement still retains its original identity.",
11
+ "Trace independent view generation, native entry generation and expected frontend conflicts without logging entry credentials, nonces, session keys or human feedback."
12
+ ]
13
+ },
4
14
  {
5
15
  "version": "0.1.0-alpha.925",
6
16
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.925",
2
+ "runtimeVersion": "0.1.0-alpha.926",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.925</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.926</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -7912,7 +7912,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7912
7912
  const { frontendSocketPathForDaemon } = await Promise.resolve().then(() => __importStar(require("../frontend-socket")));
7913
7913
  const socketPath = frontendSocketPathForDaemon(deps.socketPath);
7914
7914
  if (deps.startShepherdChat) {
7915
- await deps.startShepherdChat({ agent: command.agent, ...(command.session ? { session: command.session } : {}), socketPath });
7915
+ await deps.startShepherdChat({ agent: command.agent, ...(command.session ? { session: command.session } : {}), ...(command.web ? { web: true } : {}), socketPath });
7916
+ }
7917
+ else if (command.web) {
7918
+ const { runShepherdAttention } = await Promise.resolve().then(() => __importStar(require("../../senses/shepherd/attention")));
7919
+ await runShepherdAttention({ agent: command.agent, ...(command.session ? { session: command.session } : {}), socketPath });
7916
7920
  }
7917
7921
  else {
7918
7922
  const { runShepherdComposer } = await Promise.resolve().then(() => __importStar(require("../../senses/shepherd/composer")));
@@ -120,8 +120,8 @@ exports.COMMAND_REGISTRY = {
120
120
  },
121
121
  shepherd: {
122
122
  category: "Agents",
123
- description: "Open an independent Ouro composer for one coding terminal. Uses a named picker when ambiguous; /cancel cancels only Ouro's turn, /quit exits. Not an input hook in the coding agent.",
124
- usage: "ouro shepherd chat --agent <name> [--session <UUID>]",
123
+ description: "Open an independent Ouro composer for one coding terminal. --web opens the compact graphical view in cmux; the managed cmux Shepherd sense also installs a native Talk to Ouro button. Without --web, /cancel cancels only Ouro's turn and /quit exits. Not an input hook in the coding agent.",
124
+ usage: "ouro shepherd chat --agent <name> [--session <UUID>] [--web]",
125
125
  example: "ouro shepherd chat --agent ouroboros",
126
126
  subcommands: ["chat"],
127
127
  },
@@ -3122,22 +3122,27 @@ function parseOuroCommand(args) {
3122
3122
  if (head === "mcp")
3123
3123
  return parseMcpCommand(args.slice(1));
3124
3124
  if (head === "shepherd") {
3125
- const usage = "Usage: ouro shepherd chat --agent <name> [--session <UUID>]";
3125
+ const usage = "Usage: ouro shepherd chat --agent <name> [--session <UUID>] [--web]";
3126
3126
  if (args[1] !== "chat")
3127
3127
  throw new Error(usage);
3128
3128
  const values = new Map();
3129
- for (let i = 2; i < args.length; i += 2) {
3129
+ for (let i = 2; i < args.length; i += 1) {
3130
3130
  const flag = args[i];
3131
+ if (flag === "--web" && !values.has(flag)) {
3132
+ values.set(flag, "true");
3133
+ continue;
3134
+ }
3131
3135
  const value = args[i + 1];
3132
3136
  if (!["--agent", "--session"].includes(flag) || values.has(flag) || !value || value.startsWith("--"))
3133
3137
  throw new Error(usage);
3134
3138
  values.set(flag, value);
3139
+ i += 1;
3135
3140
  }
3136
3141
  const agent = values.get("--agent");
3137
3142
  const session = values.get("--session");
3138
3143
  if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent) || (session !== undefined && !surface_1.SHEPHERD_UUID.test(session)))
3139
3144
  throw new Error(usage);
3140
- return { kind: "shepherd.chat", agent, ...(session ? { session } : {}) };
3145
+ return { kind: "shepherd.chat", agent, ...(session ? { session } : {}), ...(values.has("--web") ? { web: true } : {}) };
3141
3146
  }
3142
3147
  if (head === "voice")
3143
3148
  return parseVoiceCommand(args.slice(1));
@@ -1,23 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FrontendSessionService = exports.FrontendSessionConflictError = exports.FrontendTurnConflictError = void 0;
3
+ exports.FrontendSessionService = exports.FrontendTurnConflictError = exports.FrontendSessionConflictError = void 0;
4
4
  const shared_turn_1 = require("../senses/shared-turn");
5
5
  const runtime_1 = require("../nerves/runtime");
6
6
  const surface_1 = require("../senses/shepherd/surface");
7
- class FrontendTurnConflictError extends Error {
8
- constructor(turnId) {
9
- super(`frontend turn already exists: ${turnId}`);
10
- this.name = "FrontendTurnConflictError";
11
- }
12
- }
13
- exports.FrontendTurnConflictError = FrontendTurnConflictError;
14
- class FrontendSessionConflictError extends Error {
15
- constructor(sessionKey) {
16
- super(`frontend session already has an active turn: ${sessionKey}`);
17
- this.name = "FrontendSessionConflictError";
18
- }
19
- }
20
- exports.FrontendSessionConflictError = FrontendSessionConflictError;
7
+ const frontend_turn_errors_1 = require("./frontend-turn-errors");
8
+ var frontend_turn_errors_2 = require("./frontend-turn-errors");
9
+ Object.defineProperty(exports, "FrontendSessionConflictError", { enumerable: true, get: function () { return frontend_turn_errors_2.FrontendSessionConflictError; } });
10
+ Object.defineProperty(exports, "FrontendTurnConflictError", { enumerable: true, get: function () { return frontend_turn_errors_2.FrontendTurnConflictError; } });
21
11
  const JOURNALED_FRONTEND_EVENTS = new Set([
22
12
  "assistant_delivery",
23
13
  "tool_started",
@@ -144,12 +134,12 @@ class FrontendSessionService {
144
134
  ...(request.shepherdSession ? { shepherdSession: Object.freeze({ ...request.shepherdSession }) } : {}),
145
135
  };
146
136
  if (this.activeTurns.has(turnId))
147
- throw new FrontendTurnConflictError(turnId);
137
+ throw new frontend_turn_errors_1.FrontendTurnConflictError(turnId);
148
138
  if (this.journal && this.turnStatus(normalized, turnId).state !== "unknown")
149
- throw new FrontendTurnConflictError(turnId);
139
+ throw new frontend_turn_errors_1.FrontendTurnConflictError(turnId);
150
140
  const sessionIdentity = [normalized.agent, normalized.friendId, normalized.channel, normalized.sessionKey].join("\0");
151
141
  if (this.activeSessions.has(sessionIdentity))
152
- throw new FrontendSessionConflictError(normalized.sessionKey);
142
+ throw new frontend_turn_errors_1.FrontendSessionConflictError(normalized.sessionKey);
153
143
  const controller = new AbortController();
154
144
  this.activeTurns.set(turnId, controller);
155
145
  this.turnRefs.set(turnId, normalized);
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.SocketFrontendClient = void 0;
36
+ exports.SocketFrontendClient = exports.FrontendRequestError = void 0;
37
37
  const net = __importStar(require("node:net"));
38
38
  const runtime_1 = require("../nerves/runtime");
39
39
  // A dropped or refused frontend socket is a transient transport condition, not a
@@ -45,6 +45,15 @@ function markTransientTransport(error) {
45
45
  function transientTransportError(message) {
46
46
  return markTransientTransport(new Error(message));
47
47
  }
48
+ class FrontendRequestError extends Error {
49
+ code;
50
+ constructor(code, message) {
51
+ super(message);
52
+ this.code = code;
53
+ this.name = "FrontendRequestError";
54
+ }
55
+ }
56
+ exports.FrontendRequestError = FrontendRequestError;
48
57
  class SocketFrontendClient {
49
58
  socketPath;
50
59
  pending = new Map();
@@ -143,7 +152,7 @@ class SocketFrontendClient {
143
152
  continue;
144
153
  this.pending.delete(frame.id);
145
154
  if (frame.ok === false) {
146
- pending.reject(new Error(String(frame.error?.message ?? "frontend request failed")));
155
+ pending.reject(new FrontendRequestError(String(frame.error?.code ?? "request_failed"), String(frame.error?.message ?? "frontend request failed")));
147
156
  }
148
157
  else {
149
158
  pending.resolve(frame.result);
@@ -41,6 +41,7 @@ const fs = __importStar(require("node:fs"));
41
41
  const net = __importStar(require("node:net"));
42
42
  const path = __importStar(require("node:path"));
43
43
  const readline = __importStar(require("node:readline"));
44
+ const frontend_turn_errors_1 = require("./frontend-turn-errors");
44
45
  const runtime_1 = require("../nerves/runtime");
45
46
  const PROTOCOL_VERSION = 1;
46
47
  const MAX_SOCKET_PATH_BYTES = 100;
@@ -302,36 +303,44 @@ async function startFrontendSocketServer(options) {
302
303
  return;
303
304
  }
304
305
  if (method === "turn.start" || method === "turn.start.observe-only") {
305
- const observeOnly = method === "turn.start.observe-only";
306
- if (params.toolMode !== undefined) {
307
- throw new Error("toolMode is unsupported; use turn.start.observe-only");
308
- }
309
- if (observeOnly && params.runtimeMcpServers !== undefined) {
310
- throw new Error("turn.start.observe-only cannot include runtimeMcpServers");
311
- }
312
- const runtimeMcpServers = validatedRuntimeMcpServers(params.runtimeMcpServers);
313
- const shepherdSession = params.shepherdSession === undefined ? undefined : await options.service.resolveShepherdSession(requiredString(params.agent, "agent"), requiredString(params.friendId, "friendId"), params.shepherdSession, params.composerTty === undefined ? undefined : requiredString(params.composerTty, "composerTty"));
314
- if (shepherdSession && (observeOnly || params.channel !== "cli" || params.sessionKey !== `shepherd:${shepherdSession.id}`)) {
315
- throw new Error("Shepherd routing requires a cli turn and the exact per-surface session key");
306
+ let prepared;
307
+ // Only preparation can be definitively refused; duplicate IDs must still reconcile.
308
+ try {
309
+ const observeOnly = method === "turn.start.observe-only";
310
+ if (params.toolMode !== undefined) {
311
+ throw new Error("toolMode is unsupported; use turn.start.observe-only");
312
+ }
313
+ if (observeOnly && params.runtimeMcpServers !== undefined) {
314
+ throw new Error("turn.start.observe-only cannot include runtimeMcpServers");
315
+ }
316
+ const runtimeMcpServers = validatedRuntimeMcpServers(params.runtimeMcpServers);
317
+ const shepherdSession = params.shepherdSession === undefined ? undefined : await options.service.resolveShepherdSession(requiredString(params.agent, "agent"), requiredString(params.friendId, "friendId"), params.shepherdSession, params.composerTty === undefined ? undefined : requiredString(params.composerTty, "composerTty"));
318
+ if (shepherdSession && (observeOnly || params.channel !== "cli" || params.sessionKey !== `shepherd:${shepherdSession.id}`)) {
319
+ throw new Error("Shepherd routing requires a cli turn and the exact per-surface session key");
320
+ }
321
+ const turnRequest = {
322
+ turnId: requiredString(params.turnId, "turnId"),
323
+ agent: requiredString(params.agent, "agent"),
324
+ friendId: requiredString(params.friendId, "friendId"),
325
+ channel: requiredString(params.channel, "channel"),
326
+ sessionKey: requiredString(params.sessionKey, "sessionKey"),
327
+ message: (requiredString(params.message, "message"), params.message),
328
+ ...(shepherdSession ? { shepherdSession } : {}),
329
+ ...(observeOnly ? { disableTools: true, ephemeral: true } : {}),
330
+ ...(runtimeMcpServers ? { runtimeMcpServers } : {}),
331
+ };
332
+ // Validation may await the host while this connection exits. Ownership must
333
+ // never be acquired after cleanup has already drained this client's turns.
334
+ if (client.closed || client.socket.destroyed || stopped)
335
+ return;
336
+ prepared = options.service.prepareTurn(turnRequest);
316
337
  }
317
- const turnRequest = {
318
- turnId: requiredString(params.turnId, "turnId"),
319
- agent: requiredString(params.agent, "agent"),
320
- friendId: requiredString(params.friendId, "friendId"),
321
- channel: requiredString(params.channel, "channel"),
322
- sessionKey: requiredString(params.sessionKey, "sessionKey"),
323
- message: (requiredString(params.message, "message"), params.message),
324
- ...(shepherdSession ? { shepherdSession } : {}),
325
- ...(observeOnly ? { disableTools: true, ephemeral: true } : {}),
326
- ...(runtimeMcpServers ? { runtimeMcpServers } : {}),
327
- };
328
- // Validation may await the host while this connection exits. Ownership must
329
- // never be acquired after cleanup has already drained this client's turns.
330
- if (client.closed || client.socket.destroyed || stopped)
338
+ catch (error) {
339
+ client.writer.send(errorResponse(id, error instanceof frontend_turn_errors_1.FrontendTurnConflictError ? "turn_conflict" : "turn_rejected", error instanceof Error ? error.message : String(error)));
331
340
  return;
332
- const prepared = options.service.prepareTurn(turnRequest);
333
- client.ownedTurnIds.add(turnRequest.turnId);
334
- client.writer.send(response(id, { accepted: true, turnId: turnRequest.turnId }));
341
+ }
342
+ client.ownedTurnIds.add(prepared.request.turnId);
343
+ client.writer.send(response(id, { accepted: true, turnId: prepared.request.turnId }));
335
344
  queueMicrotask(() => {
336
345
  void options.service.runPreparedTurn(prepared).catch(() => undefined);
337
346
  });
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FrontendSessionConflictError = exports.FrontendTurnConflictError = void 0;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ class FrontendTurnConflictError extends Error {
6
+ constructor(turnId) {
7
+ super(`frontend turn already exists: ${turnId}`);
8
+ this.name = "FrontendTurnConflictError";
9
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.frontend_turn_conflict", message: "frontend request named an already-owned turn" });
10
+ }
11
+ }
12
+ exports.FrontendTurnConflictError = FrontendTurnConflictError;
13
+ class FrontendSessionConflictError extends Error {
14
+ constructor(sessionKey) {
15
+ super(`frontend session already has an active turn: ${sessionKey}`);
16
+ this.name = "FrontendSessionConflictError";
17
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.frontend_session_conflict", message: "frontend session refused a competing turn" });
18
+ }
19
+ }
20
+ exports.FrontendSessionConflictError = FrontendSessionConflictError;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_ATTENTION_BODY_BYTES = void 0;
4
+ exports.attentionPage = attentionPage;
5
+ exports.MAX_ATTENTION_BODY_BYTES = 16_384;
6
+ /** No model content or target metadata is interpolated into executable HTML. */
7
+ function attentionPage(nonce) {
8
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_page_rendered", message: "rendered the independent session attention view" });
9
+ return `<!doctype html>
10
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ouro</title>
11
+ <style nonce="${nonce}">
12
+ :root{color-scheme:dark;font:15px -apple-system,BlinkMacSystemFont,sans-serif;color:#eee;background:#17191d}
13
+ *{box-sizing:border-box}body{margin:0}main{max-width:680px;height:100dvh;min-height:340px;margin:auto;display:flex;flex-direction:column;padding:20px;gap:16px}
14
+ header{display:flex;align-items:center;gap:10px}h1{font-size:21px;margin:0;letter-spacing:-.5px}.badge{font-size:12px;color:#9da4ae}
15
+ label{display:flex;align-items:center;gap:10px;color:#aab1bb;font-size:13px}select{min-width:0;flex:1}
16
+ select,textarea,button{font:inherit;color:inherit;border:1px solid #3b4049;background:#22262c;border-radius:10px;padding:10px}
17
+ textarea{width:100%;resize:none;min-height:82px;outline:none}textarea:focus{border-color:#9bafff}
18
+ #conversation{flex:1;overflow:auto;display:flex;flex-direction:column;gap:14px;min-height:70px}
19
+ .message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.55;margin:0}.you{align-self:flex-end;background:#2c354a;padding:10px 14px;border-radius:14px;max-width:90%}.notice{font-size:13px;color:#e0b275}
20
+ #empty{color:#9da4ae;line-height:1.6;margin:auto 0}footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px}.actions{display:flex;gap:8px;margin-left:auto}
21
+ button{cursor:pointer}button:disabled{opacity:.45;cursor:default}#send{background:#a8b8ff;color:#141823;border-color:transparent;font-weight:600}
22
+ #status{font-size:12px;color:#9da4ae}#error{color:#e0b275;font-size:13px;margin:0}form{display:flex;flex-direction:column;gap:8px}
23
+ </style></head><body><main>
24
+ <header><h1>Ouro</h1><span class="badge">Your session companion</span></header>
25
+ <label for="target">About<select id="target" aria-label="Session to discuss"></select><button id="refresh" type="button" aria-label="Refresh session choices">Refresh</button></label>
26
+ <section id="conversation" aria-live="polite"><p id="empty">Ask Ouro about this session, steer the work, or correct a handback.<br>Your coding agent's input stays untouched.</p></section>
27
+ <p id="error" role="alert"></p><form id="compose"><textarea id="draft" aria-label="Message Ouro" placeholder="Message Ouro..." maxlength="12000"></textarea>
28
+ <footer><span id="status">Independent of the coding agent</span><div class="actions"><button id="recover" type="button" hidden>Reconnect</button><button id="cancel" type="button" hidden>Cancel Ouro</button><button id="send" type="submit" disabled>Send</button></div></footer></form>
29
+ </main><script nonce="${nonce}">
30
+ const fragment=new URLSearchParams(location.hash.slice(1));const initialTarget=fragment.get('session');const token=fragment.get('token')||location.hash.slice(1).split('&')[0]||sessionStorage.getItem('ouroAttentionToken');
31
+ if(token){sessionStorage.setItem('ouroAttentionToken',token);history.replaceState(null,'',location.pathname);}
32
+ const view=sessionStorage.getItem('ouroAttentionView')||crypto.randomUUID();sessionStorage.setItem('ouroAttentionView',view);
33
+ const el=id=>document.getElementById(id);
34
+ let state=null,pending=null,sending=false,targetsSignature='',messagesSignature='';
35
+ let bindingReady=sessionStorage.getItem('ouroAttentionInitialized')==='true',pendingFault=false;
36
+ try {
37
+ pending=JSON.parse(sessionStorage.getItem('ouroAttentionPending')||'null');
38
+ if(pending&&(typeof pending.id!=='string'||typeof pending.target!=='string'||typeof pending.message!=='string'))throw Error('Stored send identity is incomplete.');
39
+ } catch(error) {
40
+ pendingFault=true;
41
+ el('error').textContent=error.message+' Check this conversation before resending; its request identity has not been discarded.';
42
+ }
43
+ let pendingWasUncertain=!!pending;
44
+ el('draft').value=sessionStorage.getItem('ouroAttentionDraft')||'';el('draft').oninput=()=>sessionStorage.setItem('ouroAttentionDraft',el('draft').value);
45
+ async function api(route,body) {
46
+ const response=await fetch(route,{
47
+ method:body?'POST':'GET',
48
+ headers:{'X-Ouro-Attention':token||'','X-Ouro-View':view,...(pending?{'X-Ouro-Pending':pending.id}:{}),...(body?{'Content-Type':'application/json'}:{})},
49
+ ...(body?{body:JSON.stringify(body)}:{})
50
+ });
51
+ const data=await response.json();
52
+ if(!response.ok) {const error=Error(data.error||'Ouro could not accept this action.');error.status=response.status;throw error;}
53
+ return data;
54
+ }
55
+ function acknowledge(submitted) {
56
+ if(!pending||pending.id!==submitted.id)return;
57
+ pending=null;
58
+ pendingWasUncertain=false;
59
+ sessionStorage.removeItem('ouroAttentionPending');
60
+ if(el('draft').value===submitted.message) {
61
+ el('draft').value='';
62
+ sessionStorage.removeItem('ouroAttentionDraft');
63
+ }
64
+ el('error').textContent='';
65
+ }
66
+ function render(next) {
67
+ state=next;
68
+ if(pending&&next.acknowledged?.id===pending.id&&next.acknowledged.target===pending.target)acknowledge(pending);
69
+ const picker=el('target'),signature=JSON.stringify(next.targets);
70
+ if(signature!==targetsSignature) {
71
+ targetsSignature=signature;picker.replaceChildren();
72
+ const prompt=document.createElement('option');prompt.value='';prompt.textContent='Choose a session';picker.append(prompt);
73
+ for(const target of next.targets) {
74
+ const option=document.createElement('option');option.value=target.id;option.textContent=target.label;picker.append(option);
75
+ }
76
+ }
77
+ picker.value=bindingReady?next.target||'':'';
78
+ picker.disabled=next.active||sending||!!pending||pendingFault;
79
+ const conversation=el('conversation'),nextMessages=JSON.stringify(next.messages);
80
+ if(next.messages.length&&nextMessages!==messagesSignature) {
81
+ const atBottom=conversation.scrollHeight-conversation.scrollTop-conversation.clientHeight<60;
82
+ conversation.replaceChildren();
83
+ for(const message of next.messages) {
84
+ const p=document.createElement('p');p.className='message '+message.role;p.textContent=message.text;conversation.append(p);
85
+ }
86
+ if(atBottom)conversation.scrollTop=conversation.scrollHeight;
87
+ } else if(!next.messages.length&&!el('empty')) {
88
+ conversation.replaceChildren();
89
+ const p=document.createElement('p');p.id='empty';p.textContent="Ask Ouro about this session. Your coding agent's input stays untouched.";conversation.append(p);
90
+ }
91
+ messagesSignature=nextMessages;
92
+ const available=next.targets.some(target=>target.id===next.target);
93
+ el('send').disabled=next.active||sending||!bindingReady||!available||pendingFault||!!(pending&&pending.target!==next.target);
94
+ el('cancel').hidden=!next.active;el('recover').hidden=!next.active&&!pending;
95
+ el('status').textContent=next.active?'Ouro is replying...':!next.targets.length?'No coding sessions available':!available?'Choose an available session':'Independent of the coding agent';
96
+ }
97
+ async function refresh() {
98
+ try {render(await api('/state'));}
99
+ catch(error) {el('error').textContent=error.message;el('send').disabled=true;}
100
+ }
101
+ async function select(target) {
102
+ const next=await api('/select',{target});
103
+ bindingReady=true;sessionStorage.setItem('ouroAttentionInitialized','true');render(next);
104
+ }
105
+ el('target').onchange=async()=>{
106
+ if(pending||pendingFault||sending) {el('error').textContent='Resolve the original message acknowledgement before changing About.';if(state)render(state);return;}
107
+ try {await select(el('target').value);el('error').textContent='';}
108
+ catch(error) {el('error').textContent=error.message;await refresh();}
109
+ };
110
+ el('refresh').onclick=async()=>{
111
+ try {render(await api('/targets',{}));}
112
+ catch(error) {el('error').textContent=error.message;}
113
+ };
114
+ el('compose').onsubmit=async event=>{
115
+ event.preventDefault();
116
+ if(sending||!bindingReady||pendingFault||!state?.target||state.active)return;
117
+ const message=el('draft').value;
118
+ if(!message.replace(/^@ouro(?:\\s+|$)/i,'').trim())return;
119
+ if(pending&&(pending.message!==message||pending.target!==state.target)) {
120
+ el('error').textContent='Reconnect before changing a message whose acknowledgement was lost.';return;
121
+ }
122
+ const submitted=pending||{id:crypto.randomUUID(),target:state.target,message};
123
+ if(new TextEncoder().encode(JSON.stringify(submitted)).byteLength>${exports.MAX_ATTENTION_BODY_BYTES}) {
124
+ el('error').textContent=pending?'The saved request is too large; check its original receipt before changing it.':'Message too large; shorten it. Nothing was sent.';
125
+ return;
126
+ }
127
+ pending=submitted;
128
+ sessionStorage.setItem('ouroAttentionPending',JSON.stringify(pending));
129
+ sending=true;render(state);
130
+ try {const next=await api('/send',submitted);acknowledge(submitted);render(next);}
131
+ catch(error) {
132
+ if(pending?.id===submitted.id) {
133
+ if(error.status===413&&!pendingWasUncertain) {
134
+ pending=null;sessionStorage.removeItem('ouroAttentionPending');
135
+ el('error').textContent=error.message+' Nothing was submitted; shorten the preserved draft.';
136
+ } else {
137
+ pendingWasUncertain=true;
138
+ el('error').textContent=error.message+' Your draft and original request identity are preserved.';
139
+ }
140
+ }
141
+ }
142
+ finally {sending=false;if(state)render(state);}
143
+ };
144
+ el('cancel').onclick=async()=>{
145
+ try {await api('/cancel',{});await refresh();}catch(error) {el('error').textContent=error.message;}
146
+ };
147
+ el('recover').onclick=async()=>{
148
+ try {await api('/recover',{});await refresh();}catch(error) {el('error').textContent=error.message;}
149
+ };
150
+ el('draft').onkeydown=event=>{
151
+ if(event.key==='Enter'&&!event.shiftKey&&!event.isComposing) {event.preventDefault();if(!el('send').disabled)el('compose').requestSubmit();}
152
+ };
153
+ async function initialize() {
154
+ try {
155
+ if(pending&&bindingReady)await refresh();
156
+ else if(initialTarget||!bindingReady)await select(pending?.target||initialTarget||'current');
157
+ else await refresh();
158
+ } catch(error) {bindingReady=false;el('error').textContent=error.message;await refresh();}
159
+ }
160
+ initialize();setInterval(refresh,1000);
161
+ </script></body></html>`;
162
+ }
163
+ const runtime_1 = require("../../nerves/runtime");
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.attentionSidebar = attentionSidebar;
37
+ exports.installAttentionSidebar = installAttentionSidebar;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ const MARKER = "// Generated by Ouro session attention.";
42
+ /** Opens beside the coding terminal without moving focus; the view then pins a fresh hint. */
43
+ function attentionSidebar(url) {
44
+ const parsed = new URL(url);
45
+ if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1" || !/^[0-9a-f]{64}$/.test(parsed.hash.slice(1))) {
46
+ throw new Error("Attention sidebar requires its private loopback entry.");
47
+ }
48
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_sidebar_generated", message: "generated the native session attention entry", meta: { format: "swift" } });
49
+ return `${MARKER}
50
+ VStack(alignment: .leading, spacing: 12) {
51
+ Text("Ouro").font(.headline)
52
+ Text("Talk about this session without touching its input.").font(.caption).foregroundColor(.secondary)
53
+ ForEach(workspaces.filter { $0.selected }) { w in
54
+ Button(action: { cmux("browser.open_split", focus: false, workspace_id: w.id, url: ${JSON.stringify(url)}) }) {
55
+ Text("Talk to Ouro")
56
+ }
57
+ }
58
+ }.padding(16)
59
+ `;
60
+ }
61
+ function installAttentionSidebar(directory, url, agent = "ouro") {
62
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
63
+ throw new Error("Attention requires a safe agent name.");
64
+ const file = path.join(directory, `ouro-attention-${agent}.swift`);
65
+ fs.mkdirSync(directory, { recursive: true });
66
+ if (fs.existsSync(file) && !fs.readFileSync(file, "utf8").startsWith(MARKER)) {
67
+ throw new Error("An independently authored ouro-attention sidebar already exists; it was not overwritten.");
68
+ }
69
+ const staging = `${file}.pending`;
70
+ fs.writeFileSync(staging, attentionSidebar(url), { mode: 0o600 });
71
+ fs.chmodSync(staging, 0o600);
72
+ fs.renameSync(staging, file);
73
+ return file;
74
+ }
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startShepherdAttention = startShepherdAttention;
37
+ exports.startManagedShepherdAttention = startManagedShepherdAttention;
38
+ exports.runShepherdAttention = runShepherdAttention;
39
+ const node_crypto_1 = require("node:crypto");
40
+ const http = __importStar(require("node:http"));
41
+ const node_child_process_1 = require("node:child_process");
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
44
+ const frontend_socket_client_1 = require("../../heart/frontend-socket-client");
45
+ const runtime_1 = require("../../nerves/runtime");
46
+ const composer_1 = require("./composer");
47
+ const surface_1 = require("./surface");
48
+ const attention_page_1 = require("./attention-page");
49
+ const attention_sidebar_1 = require("./attention-sidebar");
50
+ function suggestedTarget(targets) {
51
+ const hints = targets.filter(target => target.selected);
52
+ return hints.length === 1 ? hints[0] : targets.length === 1 ? targets[0] : null;
53
+ }
54
+ /** An owner-only loopback view of the existing fixed-target composer, not another model runtime. */
55
+ async function startShepherdAttention(options) {
56
+ const friendId = await (options.resolveOwner ?? (() => (0, surface_1.resolveExistingLocalOwner)((0, surface_1.localShepherdFriendStore)(options.agent))))();
57
+ const makeClient = options.client ?? (() => new frontend_socket_client_1.SocketFrontendClient(options.socketPath));
58
+ const discovery = makeClient();
59
+ let targets;
60
+ try {
61
+ targets = (await discovery.request("shepherd.targets", { agent: options.agent, friendId })).targets;
62
+ }
63
+ finally {
64
+ discovery.close();
65
+ }
66
+ const selected = options.session
67
+ ? (0, composer_1.selectShepherdTarget)(targets, options.session)
68
+ : suggestedTarget(targets);
69
+ const views = new Map();
70
+ const submissions = new Map();
71
+ const token = (0, node_crypto_1.randomBytes)(32).toString("hex");
72
+ const nonce = (0, node_crypto_1.randomBytes)(24).toString("base64");
73
+ let origin = "";
74
+ const append = (view, role, text) => {
75
+ view.messages.push({ role, text });
76
+ view.messages = view.messages.slice(-40);
77
+ };
78
+ const getComposer = (view, target) => {
79
+ if (!view.composer)
80
+ view.composer = new composer_1.ShepherdComposer({
81
+ client: makeClient(), agent: options.agent, friendId, target,
82
+ notice: text => append(view, "notice", text), answer: text => append(view, "ouro", text),
83
+ });
84
+ return view.composer;
85
+ };
86
+ const state = (view, viewId, requestId) => ({
87
+ targets: targets.map(item => ({ id: item.id, label: item.displayLabel ?? "Coding session" })),
88
+ target: view.target?.id ?? null, active: view.composer?.busy ?? false, messages: view.messages,
89
+ acknowledged: requestId && submissions.get(requestId)?.view === viewId
90
+ ? { id: requestId, target: submissions.get(requestId).target } : null,
91
+ });
92
+ const server = http.createServer((request, response) => {
93
+ const json = (status, value) => {
94
+ response.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
95
+ response.end(JSON.stringify(value));
96
+ };
97
+ void (async () => {
98
+ if (request.headers.host !== new URL(origin).host)
99
+ return json(403, { error: "Invalid attention host." });
100
+ if (request.method === "GET" && request.url === "/") {
101
+ response.writeHead(200, {
102
+ "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store",
103
+ "Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'`,
104
+ "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff",
105
+ });
106
+ return response.end((0, attention_page_1.attentionPage)(nonce));
107
+ }
108
+ if (request.headers["x-ouro-attention"] !== token)
109
+ return json(403, { error: "Open attention from its local entry." });
110
+ const viewId = request.headers["x-ouro-view"] ?? "default";
111
+ if (typeof viewId !== "string" || (viewId !== "default" && !surface_1.SHEPHERD_UUID.test(viewId)))
112
+ return json(400, { error: "Invalid attention view." });
113
+ let view = views.get(viewId);
114
+ if (!view) {
115
+ if (views.size >= 64)
116
+ return json(409, { error: "Attention reached its view limit. Restart Shepherd before opening another view." });
117
+ view = { target: selected ?? null, composer: null, messages: [] };
118
+ views.set(viewId, view);
119
+ }
120
+ const pendingId = typeof request.headers["x-ouro-pending"] === "string" ? request.headers["x-ouro-pending"] : undefined;
121
+ if (request.method === "GET" && request.url === "/state")
122
+ return json(200, state(view, viewId, pendingId));
123
+ if (request.method !== "POST" || request.headers.origin !== origin
124
+ || request.headers["content-type"] !== "application/json")
125
+ return json(403, { error: "Invalid attention origin or content type." });
126
+ const chunks = [];
127
+ let bytes = 0;
128
+ for await (const chunk of request) {
129
+ const buffer = Buffer.from(chunk);
130
+ bytes += buffer.length;
131
+ if (bytes > attention_page_1.MAX_ATTENTION_BODY_BYTES)
132
+ return json(413, { error: "Message is too large." });
133
+ chunks.push(buffer);
134
+ }
135
+ const body = Buffer.concat(chunks).toString("utf8");
136
+ let input;
137
+ try {
138
+ input = JSON.parse(body);
139
+ }
140
+ catch {
141
+ return json(400, { error: "Invalid JSON." });
142
+ }
143
+ if (!input || typeof input !== "object" || Array.isArray(input))
144
+ return json(400, { error: "Invalid request." });
145
+ const fields = input;
146
+ if (request.url === "/targets") {
147
+ const refresh = makeClient();
148
+ try {
149
+ targets = (await refresh.request("shepherd.targets", { agent: options.agent, friendId })).targets;
150
+ }
151
+ finally {
152
+ refresh.close();
153
+ }
154
+ return json(200, state(view, viewId, pendingId));
155
+ }
156
+ if (request.url === "/select") {
157
+ if (view.composer?.busy)
158
+ return json(409, { error: "Wait for or cancel Ouro before changing the session." });
159
+ if (typeof fields.target !== "string")
160
+ return json(400, { error: "Choose a session." });
161
+ const refresh = makeClient();
162
+ try {
163
+ targets = (await refresh.request("shepherd.targets", { agent: options.agent, friendId })).targets;
164
+ }
165
+ finally {
166
+ refresh.close();
167
+ }
168
+ if (view.composer?.busy)
169
+ return json(409, { error: "Ouro started replying while the session list refreshed; the conversation was not changed." });
170
+ const next = fields.target === "current" ? suggestedTarget(targets) : (0, composer_1.selectShepherdTarget)(targets, fields.target);
171
+ if (view.target?.id !== next?.id) {
172
+ view.composer?.close();
173
+ view.composer = null;
174
+ view.target = next;
175
+ view.messages = [];
176
+ }
177
+ return json(200, state(view, viewId, pendingId));
178
+ }
179
+ if (request.url === "/send") {
180
+ if (typeof fields.id !== "string" || !surface_1.SHEPHERD_UUID.test(fields.id)
181
+ || typeof fields.message !== "string" || !fields.message.replace(/^@ouro(?:\s+|$)/i, "").trim())
182
+ return json(400, { error: "Enter a message for Ouro." });
183
+ if (!view.target)
184
+ return json(409, { error: "Choose the session you want to discuss." });
185
+ if (fields.target !== undefined && fields.target !== view.target.id)
186
+ return json(409, { error: "This pending message is pinned to another session." });
187
+ const prior = submissions.get(fields.id);
188
+ if (prior) {
189
+ if (prior.view !== viewId || prior.target !== view.target.id || prior.message !== fields.message)
190
+ return json(409, { error: "This request id belongs to a different message or session." });
191
+ return json(202, state(view, viewId, fields.id));
192
+ }
193
+ if (view.composer?.busy)
194
+ return json(409, { error: "Ouro is already replying. Your draft has not been sent." });
195
+ if (submissions.size >= 2_000)
196
+ return json(409, { error: "Attention reached its message limit. Restart Shepherd before sending more messages." });
197
+ const bound = getComposer(view, view.target);
198
+ submissions.set(fields.id, { view: viewId, target: view.target.id, message: fields.message });
199
+ append(view, "you", fields.message);
200
+ const submittedView = view;
201
+ void bound.submit(fields.message, fields.id).catch(error => append(submittedView, "notice", String(error)));
202
+ return json(202, state(view, viewId, fields.id));
203
+ }
204
+ if (request.url === "/cancel") {
205
+ const cancelled = await view.composer?.cancel() ?? false;
206
+ return json(200, { cancelled });
207
+ }
208
+ if (request.url === "/recover") {
209
+ await view.composer?.recover();
210
+ return json(200, state(view, viewId, pendingId));
211
+ }
212
+ return json(404, { error: "Unknown attention action." });
213
+ })().catch(error => {
214
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_attention_error", message: "attention action failed", meta: { error: String(error) } });
215
+ if (!response.headersSent)
216
+ json(400, { error: String(error) });
217
+ else
218
+ response.destroy(error instanceof Error ? error : new Error(String(error)));
219
+ });
220
+ });
221
+ await new Promise((resolve, reject) => {
222
+ server.once("error", reject);
223
+ server.listen(0, "127.0.0.1", resolve);
224
+ });
225
+ const address = server.address();
226
+ if (!address || typeof address === "string") {
227
+ server.close();
228
+ throw new Error("Attention did not bind a loopback port.");
229
+ }
230
+ origin = `http://127.0.0.1:${address.port}`;
231
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_started", message: "opened independent graphical session attention", meta: { agent: options.agent } });
232
+ return {
233
+ url: `${origin}/#${token}`,
234
+ async close() {
235
+ try {
236
+ await Promise.all([...views.values()].map(view => view.composer?.cancel()));
237
+ }
238
+ finally {
239
+ for (const view of views.values())
240
+ view.composer?.close();
241
+ server.closeAllConnections();
242
+ await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
243
+ }
244
+ },
245
+ };
246
+ }
247
+ async function startManagedShepherdAttention(options) {
248
+ const app = await startShepherdAttention(options);
249
+ try {
250
+ (0, attention_sidebar_1.installAttentionSidebar)(path.join(os.homedir(), ".config/cmux/sidebars"), app.url, options.agent);
251
+ const validated = (0, node_child_process_1.spawnSync)("cmux", ["sidebar", "validate", `ouro-attention-${options.agent}`], { encoding: "utf8" });
252
+ if (validated.error || validated.status !== 0)
253
+ throw new Error(`Could not validate Ouro's native entry: ${validated.error?.message ?? validated.stderr}`);
254
+ return app;
255
+ }
256
+ catch (error) {
257
+ await app.close();
258
+ throw error;
259
+ }
260
+ }
261
+ async function runShepherdAttention(options) {
262
+ const app = await startShepherdAttention(options);
263
+ try {
264
+ const opened = (0, node_child_process_1.spawnSync)("cmux", ["browser", "open", app.url + (options.session ? `&session=${options.session}` : ""), "--focus", "false"], { encoding: "utf8" });
265
+ if (opened.error || opened.status !== 0)
266
+ throw new Error(`Could not open Ouro attention in cmux: ${opened.error?.message ?? opened.stderr}`);
267
+ await new Promise(resolve => {
268
+ const stop = () => {
269
+ process.removeListener("SIGINT", stop);
270
+ process.removeListener("SIGTERM", stop);
271
+ resolve();
272
+ };
273
+ process.once("SIGINT", stop);
274
+ process.once("SIGTERM", stop);
275
+ });
276
+ }
277
+ finally {
278
+ await app.close();
279
+ }
280
+ }
@@ -142,6 +142,8 @@ function createCmuxHost(client, options = {}) {
142
142
  cwd: hooked.get(str(surface.id) ?? "")?.cwd ?? null,
143
143
  // The human's terminal: the focused surface of the selected workspace in a visible window.
144
144
  focused: surface.focused === true && workspace.selected === true && window.visible !== false,
145
+ ...(surface.focused === true && workspace.selected === true && window.visible !== false && window.key === true
146
+ ? { selected: true } : {}),
145
147
  })));
146
148
  const identities = await (options.identifyTerminals ?? terminal_process_1.identifyTerminalProcesses)(entries.filter((entry) => entry.type === "terminal" && entry.tty).map((entry) => entry.tty));
147
149
  return entries.map((entry) => {
@@ -77,6 +77,7 @@ class ShepherdComposer {
77
77
  unsubscribe;
78
78
  unsubscribeClose;
79
79
  notice;
80
+ answer;
80
81
  constructor(options) {
81
82
  this.client = options.client;
82
83
  this.agent = options.agent;
@@ -85,6 +86,7 @@ class ShepherdComposer {
85
86
  this.target = Object.freeze({ ...options.target });
86
87
  this.sessionKey = `shepherd:${this.target.id}`;
87
88
  this.notice = options.notice ?? (() => { });
89
+ this.answer = options.answer ?? (() => { });
88
90
  this.unsubscribe = this.client.onEvent(frame => {
89
91
  if (frame.event === "replay_required") {
90
92
  const turn = this.active;
@@ -103,14 +105,17 @@ class ShepherdComposer {
103
105
  this.fail(error, true); });
104
106
  });
105
107
  }
106
- async submit(input) {
108
+ get busy() { return this.active !== null; }
109
+ async submit(input, requestId) {
107
110
  if (this.active)
108
111
  throw new Error("Ouro already has an active turn; use /cancel");
112
+ if (requestId !== undefined && !surface_1.SHEPHERD_UUID.test(requestId))
113
+ throw new Error("Ouro request id must be a UUID");
109
114
  const message = input.replace(/^@ouro(?:\s+|$)/i, "");
110
115
  if (!message.trim())
111
116
  throw new Error("message must not be empty");
112
117
  const completion = Promise.withResolvers();
113
- const turn = { id: (0, node_crypto_1.randomUUID)(), events: [], seen: new Set(), recovering: 0, reconciliationPending: false, dispatched: false, cancelled: false,
118
+ const turn = { id: requestId ?? (0, node_crypto_1.randomUUID)(), events: [], seen: new Set(), recovering: 0, reconciliationPending: false, dispatched: false, cancelled: false,
114
119
  unresolved: false, resolve: completion.resolve, reject: completion.reject };
115
120
  this.active = turn;
116
121
  // Attach a rejection handler before transport work to avoid unhandled rejection races.
@@ -128,6 +133,11 @@ class ShepherdComposer {
128
133
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_composer_turn_submitted", message: "submitted an independent fixed-target Ouro turn", meta: { agent: this.agent, session: this.target.id, turnId } });
129
134
  }
130
135
  catch (sendError) {
136
+ if (sendError instanceof frontend_socket_client_1.FrontendRequestError && sendError.code === "turn_rejected") {
137
+ if (this.active === turn)
138
+ this.fail(sendError);
139
+ return completion.promise;
140
+ }
131
141
  // Unknown acknowledgement is never retried. Even a rejected send may have run.
132
142
  if (this.active === turn) {
133
143
  try {
@@ -230,6 +240,7 @@ class ShepherdComposer {
230
240
  }
231
241
  else {
232
242
  const answer = deliveries.length ? deliveries.join("\n") : String(data.result?.response ?? "");
243
+ this.answer(answer);
233
244
  if (turn.unresolved)
234
245
  this.notice(`Ouro: ${answer}`);
235
246
  turn.resolve(answer);
@@ -99,6 +99,10 @@ async function listShepherdTargets(host, composerTty) {
99
99
  host: "cmux",
100
100
  label: [session.agent, session.cwd, session.workspaceTitle, session.title]
101
101
  .filter(Boolean).map(part => part.replace(/[\x00-\x1f\x7f-\x9f]/g, "").slice(0, 160)).join(" / "),
102
+ displayLabel: [session.title, session.workspaceTitle, `${session.agent} session`, "Coding session"]
103
+ .map(part => (part ?? "").replace(/[\x00-\x1f\x7f-\x9f]/g, "").trim())
104
+ .find(part => part && !/[\\/]/.test(part) && !/[0-9a-f]{8}-[0-9a-f-]{27,}/i.test(part)).slice(0, 72),
105
+ selected: session.selected === true,
102
106
  }));
103
107
  }
104
108
  async function validateShepherdTarget(host, id) {
@@ -57,9 +57,29 @@ Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"
57
57
  }
58
58
  const { startShepherdSenseApp } = await Promise.resolve().then(() => __importStar(require("./shepherd/sense")));
59
59
  const app = await startShepherdSenseApp({ agentName: agentName });
60
+ let attention;
61
+ if (process.platform === "darwin") {
62
+ const { resolveShepherdConnection } = await Promise.resolve().then(() => __importStar(require("../heart/shepherd-config")));
63
+ const machine = readMachineRuntimeCredentialConfig(agentName);
64
+ const connection = machine.ok ? resolveShepherdConnection(agentName, machine.config) : null;
65
+ if (connection?.ok && connection.connection.host === "cmux") {
66
+ const { startManagedShepherdAttention } = await Promise.resolve().then(() => __importStar(require("./shepherd/attention")));
67
+ const { DEFAULT_DAEMON_SOCKET_PATH } = await Promise.resolve().then(() => __importStar(require("../heart/daemon/socket-client")));
68
+ const { frontendSocketPathForDaemon } = await Promise.resolve().then(() => __importStar(require("../heart/frontend-socket")));
69
+ try {
70
+ attention = await startManagedShepherdAttention({ agent: agentName, socketPath: frontendSocketPathForDaemon(DEFAULT_DAEMON_SOCKET_PATH) });
71
+ }
72
+ catch (error) {
73
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_attention_unavailable", message: "native session attention is unavailable; Shepherd supervision remains running", meta: { agentName, error: String(error) } });
74
+ }
75
+ }
76
+ }
60
77
  const shutdown = () => {
61
78
  app.stop();
62
- process.exit(0);
79
+ void (attention?.close() ?? Promise.resolve()).then(() => process.exit(0), error => {
80
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_attention_shutdown_error", message: "could not close native session attention", meta: { error: String(error) } });
81
+ process.exit(1);
82
+ });
63
83
  };
64
84
  process.once("SIGTERM", shutdown);
65
85
  process.once("SIGINT", shutdown);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.925",
3
+ "version": "0.1.0-alpha.926",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.925",
9
+ "version": "0.1.0-alpha.926",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.925",
3
+ "version": "0.1.0-alpha.926",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },