@ouro.bot/cli 0.1.0-alpha.929 → 0.1.0-alpha.930

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,13 @@
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.930",
6
+ "changes": [
7
+ "Receive session-native Shepherd feedback with automatic source context, durable receipts, private Teams acknowledgements and correlated clarification replies.",
8
+ "Use each supervising agent's configured display name across Shepherd entry points, addressing and acknowledgements."
9
+ ]
10
+ },
4
11
  {
5
12
  "version": "0.1.0-alpha.929",
6
13
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.929",
2
+ "runtimeVersion": "0.1.0-alpha.930",
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.929</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.930</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -48,6 +48,7 @@ const runtime_1 = require("../../nerves/runtime");
48
48
  const identity_1 = require("../identity");
49
49
  const migrate_config_1 = require("../migrate-config");
50
50
  const provider_models_1 = require("../provider-models");
51
+ const agent_presentation_1 = require("../../senses/shepherd/agent-presentation");
51
52
  const provider_credentials_1 = require("../provider-credentials");
52
53
  const vault_unlock_1 = require("../../repertoire/vault-unlock");
53
54
  const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
@@ -107,6 +108,7 @@ function readAgentConfigForAgent(agentName, bundlesRoot = (0, identity_1.getAgen
107
108
  // equivalent configs for the same file.
108
109
  const config = {
109
110
  ...parsed,
111
+ displayName: (0, agent_presentation_1.validateAgentDisplayName)(parsed.displayName),
110
112
  senses: (0, identity_1.normalizeSenses)(parsed.senses, configPath),
111
113
  habitPaidTurnsPerDay: (0, identity_1.normalizeHabitPaidTurnsPerDay)(agentName, parsed.habitPaidTurnsPerDay, configPath),
112
114
  };
@@ -2049,6 +2049,9 @@ class OuroDaemon {
2049
2049
  case "agent.senseTurn":
2050
2050
  return handleAgentSenseTurn(command, { socketPath: this.socketPath });
2051
2051
  /* v8 ignore stop */
2052
+ case "agent.shepherdFeedback":
2053
+ case "agent.shepherdFeedbackStatus":
2054
+ return (await Promise.resolve().then(() => __importStar(require("../../senses/shepherd/feedback-submit")))).executeFeedbackCommand(command);
2052
2055
  case "cron.list": {
2053
2056
  const jobs = this.scheduler.listJobs();
2054
2057
  const summary = jobs.length === 0
@@ -63,6 +63,7 @@ const path = __importStar(require("path"));
63
63
  const runtime_1 = require("../nerves/runtime");
64
64
  const migrate_config_1 = require("./migrate-config");
65
65
  const autonomy_budget_1 = require("./autonomy-budget");
66
+ const agent_presentation_1 = require("../senses/shepherd/agent-presentation");
66
67
  var autonomy_budget_2 = require("./autonomy-budget");
67
68
  Object.defineProperty(exports, "normalizeHabitPaidTurnsPerDay", { enumerable: true, get: function () { return autonomy_budget_2.normalizeHabitPaidTurnsPerDay; } });
68
69
  /** Single source of truth for per-provider credential field names, env var mappings, and prompt labels. */
@@ -515,6 +516,7 @@ function loadAgentConfig(owner) {
515
516
  // Required<AgentConfig> contract test in identity-contract.test.ts.
516
517
  const config = {
517
518
  ...parsed,
519
+ displayName: (0, agent_presentation_1.validateAgentDisplayName)(parsed.displayName),
518
520
  version,
519
521
  enabled,
520
522
  humanFacing,
@@ -46,6 +46,8 @@ const identity_1 = require("../identity");
46
46
  const runtime_1 = require("../../nerves/runtime");
47
47
  const session_id_resolver_1 = require("../daemon/session-id-resolver");
48
48
  const pending_1 = require("../../mind/pending");
49
+ const feedback_source_1 = require("../../senses/shepherd/feedback-source");
50
+ const redact_1 = require("../../senses/shepherd/redact");
49
51
  exports.SENSE_TURN_MAX_RETRIES = 3;
50
52
  exports.SENSE_TURN_RETRY_DELAYS_MS = [1000, 2000, 4000];
51
53
  exports.SENSE_TURN_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
@@ -416,12 +418,58 @@ function createMcpServer(options) {
416
418
  const toolName = params.name;
417
419
  const toolArgs = (params.arguments ?? {});
418
420
  /* v8 ignore stop */
421
+ if (toolName === "shepherd_feedback" || toolName === "shepherd_feedback_status") {
422
+ await runFeedbackTool(request, toolName, toolArgs);
423
+ return;
424
+ }
419
425
  // ── Conversation tools: these run a real agent turn ──
420
426
  const conversationMessage = buildConversationMessage(toolName, toolArgs);
421
427
  if (conversationMessage !== null) {
422
428
  await runConversationTool(request, conversationMessage);
423
429
  return;
424
430
  }
431
+ async function runFeedbackTool(request, toolName, args) {
432
+ const requestId = stringArg(args, "request_id");
433
+ const validRequestId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(requestId);
434
+ try {
435
+ const submitting = toolName === "shepherd_feedback";
436
+ const allowed = submitting ? ["request_id", "feedback", "context"] : ["request_id"];
437
+ if (!validRequestId || Object.keys(args).some(key => !allowed.includes(key)))
438
+ throw new Error("Invalid feedback request identity or routing arguments");
439
+ const feedback = stringArg(args, "feedback");
440
+ if (submitting && (!feedback.trim() || feedback.length > 2000 ||
441
+ args.context !== undefined && (typeof args.context !== "string" || Buffer.byteLength(JSON.stringify(args.context)) > 8192))) {
442
+ throw new Error("Feedback must be 1–2000 characters with at most 8 KiB of context");
443
+ }
444
+ const command = submitting
445
+ ? { kind: "agent.shepherdFeedback", agent, friendId, origin: await (0, feedback_source_1.resolveFeedbackOrigin)(),
446
+ submission: { requestId, feedback, ...(typeof args.context === "string" ? { context: args.context } : {}) } }
447
+ : { kind: "agent.shepherdFeedbackStatus", agent, friendId, requestId };
448
+ // The dedicated request ID owns reconciliation. Generic sense-turn retry may repeat model effects.
449
+ const response = await (0, socket_client_1.sendDaemonCommand)(socketPath, command);
450
+ if (!response.ok)
451
+ throw new Error(response.error ?? "Feedback has no confirmed result");
452
+ const data = response.data;
453
+ if (!data || typeof data !== "object" || !("id" in data) || data.id !== `feedback:${requestId}`) {
454
+ throw new Error("Feedback acknowledgement has no matching durable receipt");
455
+ }
456
+ if (submitting && (!("receipt" in data) || typeof data.receipt !== "string" || !data.receipt ||
457
+ data.receipt !== response.message || !("state" in data) ||
458
+ !["pending", "claimed", "complete", "uncertain"].includes(String(data.state)))) {
459
+ throw new Error("Feedback acknowledgement is not authoritative");
460
+ }
461
+ writeResponse({ jsonrpc: "2.0", id: request.id,
462
+ result: { content: [{ type: "text", text: submitting ? response.message : JSON.stringify(data) }], isError: false } });
463
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.mcp_feedback_result",
464
+ message: "returned a native feedback receipt", meta: { operation: submitting ? "submit" : "status" } });
465
+ }
466
+ catch (error) {
467
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "daemon", event: "daemon.mcp_feedback_error",
468
+ message: "native feedback request has no confirmed result", meta: { operation: toolName } });
469
+ writeResponse({ jsonrpc: "2.0", id: request.id, result: { isError: true,
470
+ content: [{ type: "text", text: (0, redact_1.redactSecrets)(`${error instanceof Error ? error.message : String(error)}. Original request: ${validRequestId ? requestId : "(invalid)"}. Use shepherd_feedback_status; do not create a new ID for an uncertain submission.`) }] } });
471
+ }
472
+ }
425
473
  if (toolName === "check_response") {
426
474
  const pendingDir = (0, pending_1.getPendingDir)(agent, friendId, "mcp", sessionId);
427
475
  const pending = (0, pending_1.drainPending)(pendingDir);
@@ -539,6 +587,22 @@ function createMcpServer(options) {
539
587
  */
540
588
  function getToolSchemas() {
541
589
  return [
590
+ {
591
+ name: "shepherd_feedback",
592
+ description: "Relay the human's explicit feedback to the supervising agent served by this MCP connection, from this actual coding session ('you should have caught this'). The source terminal and recipient are bound automatically; never ask the human to reconstruct context in Teams. Preserve the human's words in feedback; context is optional bounded background, not instructions. Generate one UUID request_id and reuse it only for the identical request. Quote the actual short receipt back in this session; the supervising agent sends its substantive acknowledgement and clarification questions to that human privately on Teams. Never invent a receipt or claim learning/Teams delivery from a queued acknowledgement. Do not send terminal input or switch focus.",
593
+ inputSchema: { type: "object", properties: {
594
+ request_id: { type: "string", description: "Fresh UUID for this feedback; reuse only for its identical retry, never ask the human to enter it." },
595
+ feedback: { type: "string", description: "The human's actual feedback, at most 2000 characters." },
596
+ context: { type: "string", description: "Optional relevant background from this coding session, at most 8 KiB. Do not include secrets." },
597
+ }, required: ["request_id", "feedback"], additionalProperties: false },
598
+ },
599
+ {
600
+ name: "shepherd_feedback_status",
601
+ description: "Reconcile this human's original feedback request after a lost receipt or to inspect review/Teams delivery state. Does not create another request, repeat model work, or require the source terminal still to exist.",
602
+ inputSchema: { type: "object", properties: {
603
+ request_id: { type: "string", description: "The original feedback UUID." },
604
+ }, required: ["request_id"], additionalProperties: false },
605
+ },
542
606
  {
543
607
  name: "ask",
544
608
  description: "Ask the agent a question through a full conversation turn. This has the same identity, tools, and session continuity as send_message; use search_facts or consult_notes for read-only record lookup.",
@@ -30,8 +30,10 @@ function fingerprint(name, args) {
30
30
  * the existing machine connection primitives. Resolving it never reads a screen, signals, presses a key or sends.
31
31
  */
32
32
  async function targetFor(ctx, args) {
33
- if (text(args.session))
34
- (0, target_binding_1.assertBoundShepherdTarget)(ctx.currentShepherdSurface, text(args.session), text(args.host) || undefined);
33
+ const session = text(args.session) || ctx.currentShepherdSurface?.id || "";
34
+ const hostName = text(args.host) || ctx.currentShepherdSurface?.host || "";
35
+ if (session)
36
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx.currentShepherdSurface, session, hostName || undefined);
35
37
  const records = (0, returns_1.readReturns)(ctx.agentRoot);
36
38
  const returnId = text(args.return_id);
37
39
  if (returnId) {
@@ -43,10 +45,8 @@ async function targetFor(ctx, args) {
43
45
  throw new Error("target does not match the recorded return");
44
46
  return { target: { host: record.host, session: record.session, cwd: record.cwd, task: record.task }, live: [] };
45
47
  }
46
- const session = text(args.session);
47
48
  if (!session)
48
49
  throw new Error("name a session from Shepherd history or overview");
49
- const hostName = text(args.host);
50
50
  const matching = records.filter(r => r.session === session && (!hostName || r.host === hostName));
51
51
  if (new Set(matching.map(r => r.host)).size > 1)
52
52
  throw new Error("session is ambiguous across hosts; name its host");
@@ -79,7 +79,8 @@ function definition(name, description, properties, required, run, mutates = fals
79
79
  if (!ctx?.agentRoot || !ctx.agentName)
80
80
  return json({ error: "Shepherd guidance needs an agent runtime" });
81
81
  try {
82
- const request = { actorId: friend.id, requestId: text(args.request_id), requestFingerprint: fingerprint(name, args) };
82
+ const boundDefault = name === "shepherd_correct" && !text(args.session) && !text(args.return_id) && ctx.currentShepherdSurface;
83
+ const request = { actorId: friend.id, requestId: text(args.request_id), requestFingerprint: fingerprint(name, boundDefault ? { ...args, session: boundDefault.id, host: boundDefault.host } : args) };
83
84
  if (mutates) {
84
85
  const prior = (0, guidance_1.readGuidance)(ctx.agentRoot).requests.find(r => r.actorId === request.actorId && r.requestId === request.requestId);
85
86
  if (prior) {
@@ -104,8 +105,12 @@ function definition(name, description, properties, required, run, mutates = fals
104
105
  }
105
106
  exports.shepherdGuidanceToolDefinitions = [
106
107
  definition("shepherd_history", "Read private Shepherd return history with stable return IDs, actual judgment/reply/outcome and your own ownerNotice receipts when tracked. Historical, not current status; a vanished terminal remains readable. The session filter is a stable ID, not a short ref. Use to understand 'that was wrong because...' before correcting it, or to check whether a notice was sent. Use displayId as the human-facing return label; never invent a shortened ID suffix. Mutations require the exact full id, not displayId. ownerNotice.state sent means transport acceptance, not proof the human read it; pending awaits a safe retry; unknown may already have arrived and must not be blindly replayed. messageId is the activity ID returned by transport. ownerNotice null means no receipt recorded for this recipient, not proof of non-delivery. ownerNoticeError explicitly identifies unavailable receipt evidence. Never types into a terminal.", targetProperties, [], async (args, ctx, request) => {
108
+ const session = text(args.session) || ctx.currentShepherdSurface?.id;
109
+ const host = text(args.host) || ctx.currentShepherdSurface?.host;
110
+ if (session)
111
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx.currentShepherdSurface, session, host);
107
112
  const returns = (0, returns_1.readReturns)(ctx.agentRoot)
108
- .filter(r => (!text(args.session) || r.session === text(args.session)) && (!text(args.host) || r.host === text(args.host)))
113
+ .filter(r => (!session || r.session === session) && (!host || r.host === host))
109
114
  .map(record => {
110
115
  const id = (0, guidance_1.shepherdReturnId)(record);
111
116
  return { ...record, id, displayId: id.slice("return:".length, "return:".length + 8) };
@@ -141,7 +146,7 @@ exports.shepherdGuidanceToolDefinitions = [
141
146
  }, live);
142
147
  }, true),
143
148
  definition("shepherd_guidance", "Inspect what Shepherd learned ('what did you learn?'). With a session, returns revision, pause and applicable active principles with IDs, scopes and operator provenance. Omit session to inspect all active principles and pauses, including their exact targets. No send.", targetProperties, [], async (args, ctx) => {
144
- if (args.session === undefined) {
149
+ if (args.session === undefined && !ctx.currentShepherdSurface) {
145
150
  const state = (0, guidance_1.readGuidance)(ctx.agentRoot);
146
151
  return { revision: state.revision, principles: state.principles.filter(p => p.active), pauses: state.pauses.filter(p => p.paused) };
147
152
  }
@@ -47,6 +47,7 @@ const tools_base_1 = require("./tools-base");
47
47
  const tools_shepherd_guidance_1 = require("./tools-shepherd-guidance");
48
48
  const guidance_1 = require("../senses/shepherd/guidance");
49
49
  const target_binding_1 = require("../senses/shepherd/target-binding");
50
+ const agent_presentation_1 = require("../senses/shepherd/agent-presentation");
50
51
  /**
51
52
  * Shepherd's tools: every coding-agent session on this machine's terminal host with a current screen
52
53
  * and what Shepherd last decided about it (`shepherd_overview`), a deeper read (`shepherd_read`), a status
@@ -123,7 +124,7 @@ exports.shepherdOverviewToolDefinition = {
123
124
  type: "function",
124
125
  function: {
125
126
  name: "shepherd_overview",
126
- description: "A live, read-only snapshot of every terminal on this machine's host (cmux): agent, folder, workspace and tab, plus a freshly read, redacted, bounded currentScreen with its observation time. Use currentScreen to answer what agents are doing now in one reply; lastReturn is historical and must not be presented as current status. Include every listed terminal exactly once with its complete ref (surface:12), including plain shells and Ouro composers; never omit a row or abbreviate subsequent refs to bare numbers. terminalCount is authoritative; derive disjoint category counts from those same rows. Classify activity as working, needs human, waiting on outside dependency, idle/local work finished, shell, or unknown. An idle prompt does not establish delivered work: do not use a generic Done bucket for locally finished work that still needs review, approval, merge, publication, deployment or verification. State those remaining steps inline. An agent explicitly reporting an outside reviewer wait belongs under waiting even when its prompt is idle; that is neither premature completion nor a request for routine continuation. A currentScreen.error explicitly identifies a terminal that could not be read; explain it inline with the other sessions, not by asking for another turn. For deeper details use shepherd_read. Historical lastReturn includes the prior judgment time, kind, action and reason.",
127
+ description: "A live, read-only snapshot of every terminal on this machine's host (cmux): agent, folder, workspace and tab, plus a freshly read, redacted, bounded currentScreen with its observation time. Use currentScreen to answer what agents are doing now in one reply; lastReturn is historical and must not be presented as current status. Include every listed terminal exactly once with its complete ref (surface:12), including plain shells and supervisor composers; never omit a row or abbreviate subsequent refs to bare numbers. terminalCount is authoritative; derive disjoint category counts from those same rows. Classify activity as working, needs human, waiting on outside dependency, idle/local work finished, shell, or unknown. An idle prompt does not establish delivered work: do not use a generic Done bucket for locally finished work that still needs review, approval, merge, publication, deployment or verification. State those remaining steps inline. An agent explicitly reporting an outside reviewer wait belongs under waiting even when its prompt is idle; that is neither premature completion nor a request for routine continuation. A currentScreen.error explicitly identifies a terminal that could not be read; explain it inline with the other sessions, not by asking for another turn. For deeper details use shepherd_read. Historical lastReturn includes the prior judgment time, kind, action and reason.",
127
128
  parameters: { type: "object", properties: {}, additionalProperties: false },
128
129
  },
129
130
  },
@@ -227,12 +228,12 @@ exports.shepherdSendToolDefinition = {
227
228
  type: "function",
228
229
  function: {
229
230
  name: "shepherd_send",
230
- description: `Type into one coding agent's terminal on this machine for the human you are talking with, because they asked you to. Use it only to deliver that human's own instruction or keypress ("tell it to run the tests", "approve that", "stop it"): never on your own initiative, and never for anyone else. Give exactly one of text or key. text is one line (at most ${exports.SEND_TEXT_MAX_CHARS} characters, no control characters), typed as "[Ouro for <their name>] <text>" and submitted with Enter. key is pressed alone: one of ${exports.SEND_KEYS.join(", ")} (a menu or permission prompt, escape to interrupt, ctrl-c to cancel). enter, tab, a digit, y and n can approve or choose on a prompt, so they need screen from a shepherd_read made just before, and nothing is sent if the terminal changed since; tell the human what a prompt asks before approving it unless they have seen it. Pass screen with text too when the text answers what is on screen. To interrupt and then instruct, send escape, read again, then send the text. It also types into the terminal the human has focused on this machine.`,
231
+ description: `Type into one coding agent's terminal on this machine for the human you are talking with, because they asked you to. Use it only to deliver that human's own instruction or keypress ("tell it to run the tests", "approve that", "stop it"): never on your own initiative, and never for anyone else. Give exactly one of text or key. text is one line (at most ${exports.SEND_TEXT_MAX_CHARS} characters, no control characters), typed as "[<your configured name> for <their name>] <text>" and submitted with Enter. key is pressed alone: one of ${exports.SEND_KEYS.join(", ")} (a menu or permission prompt, escape to interrupt, ctrl-c to cancel). enter, tab, a digit, y and n can approve or choose on a prompt, so they need screen from a shepherd_read made just before, and nothing is sent if the terminal changed since; tell the human what a prompt asks before approving it unless they have seen it. Pass screen with text too when the text answers what is on screen. To interrupt and then instruct, send escape, read again, then send the text. It also types into the terminal the human has focused on this machine.`,
231
232
  parameters: {
232
233
  type: "object",
233
234
  properties: {
234
235
  session: { type: "string", description: "Session id or ref (surface:12) from shepherd_overview." },
235
- text: { type: "string", description: "The human's instruction, one line. Sent with the [Ouro for <name>] prefix and Enter." },
236
+ text: { type: "string", description: "The human's instruction, one line. Sent with [<your configured name> for <their name>] and Enter." },
236
237
  key: { type: "string", enum: [...exports.SEND_KEYS], description: "One key to press instead of text." },
237
238
  screen: { type: "string", description: "The screen fingerprint from the shepherd_read just before. Required for enter, tab, a digit, y and n." },
238
239
  },
@@ -273,7 +274,7 @@ exports.shepherdSendToolDefinition = {
273
274
  const now = (0, redact_1.screenFingerprint)(await host.read(target.id, redact_1.FINGERPRINT_READ_LINES));
274
275
  if (seen && seen !== now)
275
276
  return refused("the terminal changed since it was read, so nothing was sent; read it again", meta);
276
- const typed = key ? null : `[Ouro for ${friend.name}] ${line}`;
277
+ const typed = key ? null : `[${(0, agent_presentation_1.readAgentPresentation)(ctx.agentName, ctx.agentRoot).name} for ${friend.name}] ${line}`;
277
278
  if (key)
278
279
  await host.key(target.id, key);
279
280
  else
@@ -207,7 +207,8 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
207
207
  ...(inner ? [tools_flow_1.restTool] : [tools_flow_1.observeTool, tools_flow_1.settleTool]),
208
208
  ...(capabilities?.chatStyle ? [tools_flow_1.speakTool] : []),
209
209
  ].filter((tool) => (!sanctuary || (knownSanctuaryProfile && tool.function.name !== "observe"))
210
- && (!relationship || relationship.advertisedToolNames.includes(tool.function.name)));
210
+ && (!relationship || relationship.advertisedToolNames.includes(tool.function.name))
211
+ && (!context?.allowedEngineTools || context.allowedEngineTools.includes(tool.function.name)));
211
212
  return Object.freeze({
212
213
  ordinary: Object.freeze(ordinary.map((definition) => definition === hostTool ? definition : Object.freeze({
213
214
  ...definition,
@@ -0,0 +1,88 @@
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.validateAgentDisplayName = validateAgentDisplayName;
37
+ exports.agentPresentation = agentPresentation;
38
+ exports.readAgentPresentation = readAgentPresentation;
39
+ exports.agentAddressPattern = agentAddressPattern;
40
+ exports.stripAgentAddress = stripAgentAddress;
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const runtime_1 = require("../../nerves/runtime");
44
+ function validateAgentDisplayName(value) {
45
+ if (value === undefined)
46
+ return undefined;
47
+ if (typeof value !== "string" || !value.length || value.length > 80 || value.trim() !== value ||
48
+ /[<>\[\]\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value)) {
49
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.agent_display_name_invalid",
50
+ message: "configured agent displayName is invalid", meta: { field: "displayName" } });
51
+ throw new Error("agent.json displayName must be 1–80 characters without markup, controls or surrounding whitespace");
52
+ }
53
+ return value;
54
+ }
55
+ function agentPresentation(agent, config) {
56
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(agent) || agent.includes("..")) {
57
+ throw new Error("agent presentation requires a safe configured identifier");
58
+ }
59
+ const name = validateAgentDisplayName(config.displayName) ?? agent;
60
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.agent_presentation_resolved",
61
+ message: "resolved explicit agent presentation", meta: { configured: config.displayName !== undefined } });
62
+ return { name, aliases: [...new Set([name, agent])] };
63
+ }
64
+ function readAgentPresentation(agent, root) {
65
+ let encoded;
66
+ try {
67
+ encoded = fs.readFileSync(path.join(root, "agent.json"), "utf8");
68
+ }
69
+ catch (error) {
70
+ if (error.code !== "ENOENT")
71
+ throw error;
72
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.agent_presentation_missing",
73
+ message: "agent presentation config is missing; using its routing identifier", meta: { configured: false } });
74
+ return agentPresentation(agent, {});
75
+ }
76
+ const config = JSON.parse(encoded);
77
+ if (!config || typeof config !== "object" || Array.isArray(config))
78
+ throw new Error("agent.json presentation must be an object");
79
+ return agentPresentation(agent, { displayName: validateAgentDisplayName(config.displayName) });
80
+ }
81
+ function agentAddressPattern(aliases) {
82
+ if (!aliases.length)
83
+ return "(?!)";
84
+ return `^@(?:${aliases.map(alias => alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s+|$)`;
85
+ }
86
+ function stripAgentAddress(message, aliases) {
87
+ return message.replace(new RegExp(agentAddressPattern(aliases), "iu"), "");
88
+ }
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MAX_ATTENTION_BODY_BYTES = void 0;
4
4
  exports.attentionPage = attentionPage;
5
+ const agent_presentation_1 = require("./agent-presentation");
5
6
  exports.MAX_ATTENTION_BODY_BYTES = 16_384;
6
7
  /** No model content or target metadata is interpolated into executable HTML. */
7
- function attentionPage(nonce) {
8
+ function attentionPage(nonce, presentation = (0, agent_presentation_1.agentPresentation)("Agent", {})) {
8
9
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_page_rendered", message: "rendered the independent session attention view" });
10
+ const name = presentation.name.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
9
11
  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>
12
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${name}</title>
11
13
  <style nonce="${nonce}">
12
14
  :root{color-scheme:dark;font:15px -apple-system,BlinkMacSystemFont,sans-serif;color:#eee;background:#17191d}
13
15
  *{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}
@@ -21,12 +23,13 @@ textarea{width:100%;resize:none;min-height:82px;outline:none}textarea:focus{bord
21
23
  button{cursor:pointer}button:disabled{opacity:.45;cursor:default}#send{background:#a8b8ff;color:#141823;border-color:transparent;font-weight:600}
22
24
  #status{font-size:12px;color:#9da4ae}#error{color:#e0b275;font-size:13px;margin:0}form{display:flex;flex-direction:column;gap:8px}
23
25
  </style></head><body><main>
24
- <header><h1>Ouro</h1><span class="badge">Your session companion</span></header>
26
+ <header><h1>${name}</h1><span class="badge">Your session companion</span></header>
25
27
  <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>
28
+ <section id="conversation" aria-live="polite"><p id="empty">Ask ${name} about this session, steer the work, or correct a handback.<br>Your coding agent's input stays untouched.</p></section>
29
+ <p id="error" role="alert"></p><form id="compose"><textarea id="draft" aria-label="Message ${name}" placeholder="Message ${name}..." maxlength="12000"></textarea>
30
+ <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 ${name}</button><button id="send" type="submit" disabled>Send</button></div></footer></form>
29
31
  </main><script nonce="${nonce}">
32
+ const agentName=${JSON.stringify(presentation.name).replaceAll("<", "\\u003c")},address=new RegExp(${JSON.stringify((0, agent_presentation_1.agentAddressPattern)(presentation.aliases))},'iu');
30
33
  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
34
  if(token){sessionStorage.setItem('ouroAttentionToken',token);history.replaceState(null,'',location.pathname);}
32
35
  const view=sessionStorage.getItem('ouroAttentionView')||crypto.randomUUID();sessionStorage.setItem('ouroAttentionView',view);
@@ -49,7 +52,7 @@ async function api(route,body) {
49
52
  ...(body?{body:JSON.stringify(body)}:{})
50
53
  });
51
54
  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;}
55
+ if(!response.ok) {const error=Error(data.error||agentName+' could not accept this action.');error.status=response.status;throw error;}
53
56
  return data;
54
57
  }
55
58
  function acknowledge(submitted) {
@@ -86,13 +89,13 @@ function render(next) {
86
89
  if(atBottom)conversation.scrollTop=conversation.scrollHeight;
87
90
  } else if(!next.messages.length&&!el('empty')) {
88
91
  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);
92
+ const p=document.createElement('p');p.id='empty';p.textContent="Ask "+agentName+" about this session. Your coding agent's input stays untouched.";conversation.append(p);
90
93
  }
91
94
  messagesSignature=nextMessages;
92
95
  const available=next.targets.some(target=>target.id===next.target);
93
96
  el('send').disabled=next.active||sending||!bindingReady||!available||pendingFault||!!(pending&&pending.target!==next.target);
94
97
  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';
98
+ el('status').textContent=next.active?agentName+' is replying...':!next.targets.length?'No coding sessions available':!available?'Choose an available session':'Independent of the coding agent';
96
99
  }
97
100
  async function refresh() {
98
101
  try {render(await api('/state'));}
@@ -115,7 +118,7 @@ el('compose').onsubmit=async event=>{
115
118
  event.preventDefault();
116
119
  if(sending||!bindingReady||pendingFault||!state?.target||state.active)return;
117
120
  const message=el('draft').value;
118
- if(!message.replace(/^@ouro(?:\\s+|$)/i,'').trim())return;
121
+ if(!message.replace(address,'').trim())return;
119
122
  if(pending&&(pending.message!==message||pending.target!==state.target)) {
120
123
  el('error').textContent='Reconnect before changing a message whose acknowledgement was lost.';return;
121
124
  }
@@ -38,9 +38,11 @@ exports.installAttentionSidebar = installAttentionSidebar;
38
38
  const fs = __importStar(require("node:fs"));
39
39
  const path = __importStar(require("node:path"));
40
40
  const runtime_1 = require("../../nerves/runtime");
41
+ const agent_presentation_1 = require("./agent-presentation");
41
42
  const MARKER = "// Generated by Ouro session attention.";
42
43
  /** Opens beside the coding terminal without moving focus; the view then pins a fresh hint. */
43
- function attentionSidebar(url) {
44
+ function attentionSidebar(url, name = "Agent") {
45
+ (0, agent_presentation_1.validateAgentDisplayName)(name);
44
46
  const parsed = new URL(url);
45
47
  if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1" || !/^[0-9a-f]{64}$/.test(parsed.hash.slice(1))) {
46
48
  throw new Error("Attention sidebar requires its private loopback entry.");
@@ -48,17 +50,17 @@ function attentionSidebar(url) {
48
50
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_sidebar_generated", message: "generated the native session attention entry", meta: { format: "swift" } });
49
51
  return `${MARKER}
50
52
  VStack(alignment: .leading, spacing: 12) {
51
- Text("Ouro").font(.headline)
53
+ Text(${JSON.stringify(name)}).font(.headline)
52
54
  Text("Talk about this session without touching its input.").font(.caption).foregroundColor(.secondary)
53
55
  ForEach(workspaces.filter { $0.selected }) { w in
54
56
  Button(action: { cmux("browser.open_split", focus: false, workspace_id: w.id, url: ${JSON.stringify(url)}) }) {
55
- Text("Talk to Ouro")
57
+ Text(${JSON.stringify(`Talk to ${name}`)})
56
58
  }
57
59
  }
58
60
  }.padding(16)
59
61
  `;
60
62
  }
61
- function installAttentionSidebar(directory, url, agent = "ouro") {
63
+ function installAttentionSidebar(directory, url, agent = "ouro", displayName = agent) {
62
64
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
63
65
  throw new Error("Attention requires a safe agent name.");
64
66
  const file = path.join(directory, `ouro-attention-${agent}.swift`);
@@ -67,7 +69,7 @@ function installAttentionSidebar(directory, url, agent = "ouro") {
67
69
  throw new Error("An independently authored ouro-attention sidebar already exists; it was not overwritten.");
68
70
  }
69
71
  const staging = `${file}.pending`;
70
- fs.writeFileSync(staging, attentionSidebar(url), { mode: 0o600 });
72
+ fs.writeFileSync(staging, attentionSidebar(url, displayName), { mode: 0o600 });
71
73
  fs.chmodSync(staging, 0o600);
72
74
  fs.renameSync(staging, file);
73
75
  return file;
@@ -47,12 +47,18 @@ const composer_1 = require("./composer");
47
47
  const surface_1 = require("./surface");
48
48
  const attention_page_1 = require("./attention-page");
49
49
  const attention_sidebar_1 = require("./attention-sidebar");
50
+ const identity_1 = require("../../heart/identity");
51
+ const agent_presentation_1 = require("./agent-presentation");
50
52
  function suggestedTarget(targets) {
51
53
  const hints = targets.filter(target => target.selected);
52
54
  return hints.length === 1 ? hints[0] : targets.length === 1 ? targets[0] : null;
53
55
  }
54
56
  /** An owner-only loopback view of the existing fixed-target composer, not another model runtime. */
55
57
  async function startShepherdAttention(options) {
58
+ const presentation = options.displayName === undefined
59
+ ? (0, agent_presentation_1.readAgentPresentation)(options.agent, (0, identity_1.getAgentRoot)(options.agent))
60
+ : (0, agent_presentation_1.agentPresentation)(options.agent, { displayName: options.displayName });
61
+ const name = presentation.name;
56
62
  const friendId = await (options.resolveOwner ?? (() => (0, surface_1.resolveExistingLocalOwner)((0, surface_1.localShepherdFriendStore)(options.agent))))();
57
63
  const makeClient = options.client ?? (() => new frontend_socket_client_1.SocketFrontendClient(options.socketPath));
58
64
  const discovery = makeClient();
@@ -78,7 +84,7 @@ async function startShepherdAttention(options) {
78
84
  const getComposer = (view, target) => {
79
85
  if (!view.composer)
80
86
  view.composer = new composer_1.ShepherdComposer({
81
- client: makeClient(), agent: options.agent, friendId, target,
87
+ client: makeClient(), agent: options.agent, displayName: name, friendId, target,
82
88
  notice: text => append(view, "notice", text), answer: text => append(view, "ouro", text),
83
89
  });
84
90
  return view.composer;
@@ -103,7 +109,7 @@ async function startShepherdAttention(options) {
103
109
  "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
110
  "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff",
105
111
  });
106
- return response.end((0, attention_page_1.attentionPage)(nonce));
112
+ return response.end((0, attention_page_1.attentionPage)(nonce, presentation));
107
113
  }
108
114
  if (request.headers["x-ouro-attention"] !== token)
109
115
  return json(403, { error: "Open attention from its local entry." });
@@ -155,7 +161,7 @@ async function startShepherdAttention(options) {
155
161
  }
156
162
  if (request.url === "/select") {
157
163
  if (view.composer?.busy)
158
- return json(409, { error: "Wait for or cancel Ouro before changing the session." });
164
+ return json(409, { error: `Wait for or cancel ${name} before changing the session.` });
159
165
  if (typeof fields.target !== "string")
160
166
  return json(400, { error: "Choose a session." });
161
167
  const refresh = makeClient();
@@ -166,7 +172,7 @@ async function startShepherdAttention(options) {
166
172
  refresh.close();
167
173
  }
168
174
  if (view.composer?.busy)
169
- return json(409, { error: "Ouro started replying while the session list refreshed; the conversation was not changed." });
175
+ return json(409, { error: `${name} started replying while the session list refreshed; the conversation was not changed.` });
170
176
  const next = fields.target === "current" ? suggestedTarget(targets) : (0, composer_1.selectShepherdTarget)(targets, fields.target);
171
177
  if (view.target?.id !== next?.id) {
172
178
  view.composer?.close();
@@ -178,8 +184,8 @@ async function startShepherdAttention(options) {
178
184
  }
179
185
  if (request.url === "/send") {
180
186
  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." });
187
+ || typeof fields.message !== "string" || !(0, agent_presentation_1.stripAgentAddress)(fields.message, presentation.aliases).trim())
188
+ return json(400, { error: `Enter a message for ${name}.` });
183
189
  if (!view.target)
184
190
  return json(409, { error: "Choose the session you want to discuss." });
185
191
  if (fields.target !== undefined && fields.target !== view.target.id)
@@ -191,7 +197,7 @@ async function startShepherdAttention(options) {
191
197
  return json(202, state(view, viewId, fields.id));
192
198
  }
193
199
  if (view.composer?.busy)
194
- return json(409, { error: "Ouro is already replying. Your draft has not been sent." });
200
+ return json(409, { error: `${name} is already replying. Your draft has not been sent.` });
195
201
  if (submissions.size >= 2_000)
196
202
  return json(409, { error: "Attention reached its message limit. Restart Shepherd before sending more messages." });
197
203
  const bound = getComposer(view, view.target);
@@ -245,9 +251,10 @@ async function startShepherdAttention(options) {
245
251
  };
246
252
  }
247
253
  async function startManagedShepherdAttention(options) {
248
- const app = await startShepherdAttention(options);
254
+ const presentation = (0, agent_presentation_1.readAgentPresentation)(options.agent, (0, identity_1.getAgentRoot)(options.agent));
255
+ const app = await startShepherdAttention({ ...options, displayName: presentation.name });
249
256
  try {
250
- (0, attention_sidebar_1.installAttentionSidebar)(path.join(os.homedir(), ".config/cmux/sidebars"), app.url, options.agent);
257
+ (0, attention_sidebar_1.installAttentionSidebar)(path.join(os.homedir(), ".config/cmux/sidebars"), app.url, options.agent, presentation.name);
251
258
  const { socketPath, auth } = options.cmuxConnection;
252
259
  const validated = (0, node_child_process_1.spawnSync)("cmux", ["--socket", socketPath, "sidebar", "validate", `ouro-attention-${options.agent}`], {
253
260
  encoding: "utf8",
@@ -258,7 +265,7 @@ async function startManagedShepherdAttention(options) {
258
265
  },
259
266
  });
260
267
  if (validated.error || validated.status !== 0)
261
- throw new Error(`Could not validate Ouro's native entry: ${validated.error?.message ?? validated.stderr}`);
268
+ throw new Error(`Could not validate ${presentation.name}'s native entry: ${validated.error?.message ?? validated.stderr}`);
262
269
  return app;
263
270
  }
264
271
  catch (error) {
@@ -271,7 +278,7 @@ async function runShepherdAttention(options) {
271
278
  try {
272
279
  const opened = (0, node_child_process_1.spawnSync)("cmux", ["browser", "open", app.url + (options.session ? `&session=${options.session}` : ""), "--focus", "false"], { encoding: "utf8" });
273
280
  if (opened.error || opened.status !== 0)
274
- throw new Error(`Could not open Ouro attention in cmux: ${opened.error?.message ?? opened.stderr}`);
281
+ throw new Error(`Could not open session attention in cmux: ${opened.error?.message ?? opened.stderr}`);
275
282
  await new Promise(resolve => {
276
283
  const stop = () => {
277
284
  process.removeListener("SIGINT", stop);
@@ -147,7 +147,7 @@ function createCmuxHost(client, options = {}) {
147
147
  })));
148
148
  const identities = await (options.identifyTerminals ?? terminal_process_1.identifyTerminalProcesses)(entries.filter((entry) => entry.type === "terminal" && entry.tty).map((entry) => entry.tty));
149
149
  return entries.map((entry) => {
150
- const identity = entry.tty ? identities.get(entry.tty) : undefined;
150
+ const identity = entry.tty ? identities.get(entry.tty.replace(/^\/dev\//, "")) : undefined;
151
151
  return { ...entry, agent: identity?.agent ?? entry.agent, cwd: identity?.cwd ?? entry.cwd };
152
152
  });
153
153
  }