@ouro.bot/cli 0.1.0-alpha.923 → 0.1.0-alpha.924

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,20 @@
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.924",
6
+ "changes": [
7
+ "Talk to Ouro about a fixed coding terminal through an independent Shepherd composer, even while its agent is busy.",
8
+ "Correct Shepherd after an intervention, inspect or change scoped learned guidance, and pause automatic replies without interrupting coding work.",
9
+ "Retain every eligible owner notice across restart, record Teams acceptance, and retry only definitely unsent deliveries.",
10
+ "Enforce the composer's fixed UUID and host at terminal and guidance execution boundaries; concurrent notice workers cannot replay an accepted or uncertain send.",
11
+ "Recognize current Node-hosted Copilot terminals by their foreground launcher entry point, so the independent composer remains available on Copilot 1.0.97.",
12
+ "Trace fixed-target composer submissions, authorized target discovery, committed guidance revisions, and bound-target refusals without logging private human feedback.",
13
+ "Answer notice-delivery questions from private recipient-specific accepted, pending or uncertain receipts, including archived acceptances, without replaying sends or hiding unavailable evidence.",
14
+ "Keep private error details in redacted human responses and record only structured failure codes in control/learning telemetry.",
15
+ "Use authoritative short return labels in human answers while retaining exact immutable identifiers for post-action corrections."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.923",
6
20
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.923",
2
+ "runtimeVersion": "0.1.0-alpha.924",
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.923</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.924</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -7897,6 +7897,29 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7897
7897
  await runMcpServeCliLifecycle(server, mcpInput);
7898
7898
  return "";
7899
7899
  }
7900
+ if (command.kind === "shepherd.chat") {
7901
+ const daemonResult = deps.ensureDaemonForFrontend
7902
+ ? await deps.ensureDaemonForFrontend(command.agent)
7903
+ : await ensureDaemonRunning({
7904
+ ...deps,
7905
+ startDaemonProcess: (socketPath) => deps.startDaemonForFrontend
7906
+ ? deps.startDaemonForFrontend(socketPath, command.agent)
7907
+ : deps.startDaemonProcess(socketPath),
7908
+ reportDaemonStartupPhase: () => { },
7909
+ });
7910
+ if (!daemonResult.ok)
7911
+ throw new Error(daemonResult.message);
7912
+ const { frontendSocketPathForDaemon } = await Promise.resolve().then(() => __importStar(require("../frontend-socket")));
7913
+ const socketPath = frontendSocketPathForDaemon(deps.socketPath);
7914
+ if (deps.startShepherdChat) {
7915
+ await deps.startShepherdChat({ agent: command.agent, ...(command.session ? { session: command.session } : {}), socketPath });
7916
+ }
7917
+ else {
7918
+ const { runShepherdComposer } = await Promise.resolve().then(() => __importStar(require("../../senses/shepherd/composer")));
7919
+ await runShepherdComposer({ agent: command.agent, ...(command.session ? { session: command.session } : {}), socketPath });
7920
+ }
7921
+ return "";
7922
+ }
7900
7923
  if (command.kind === "acp-serve") {
7901
7924
  const createAcpServer = deps.createAcpServer ?? (await Promise.resolve().then(() => __importStar(require("../../senses/acp-server")))).createAcpServer;
7902
7925
  const resolveFriendId = deps.resolveFrontendFriendId ?? resolveFrontendFriendId;
@@ -118,6 +118,13 @@ exports.COMMAND_REGISTRY = {
118
118
  example: "ouro voice join --agent slugger --participants \"Ari and Sam\" --occasion \"podcast prep\"",
119
119
  subcommands: ["join", "leave", "status"],
120
120
  },
121
+ shepherd: {
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>]",
125
+ example: "ouro shepherd chat --agent ouroboros",
126
+ subcommands: ["chat"],
127
+ },
121
128
  config: {
122
129
  category: "Agents",
123
130
  description: "Legacy model compatibility helpers; prefer `ouro use` and `ouro check`",
@@ -52,6 +52,7 @@ const fs = __importStar(require("fs"));
52
52
  const path = __importStar(require("path"));
53
53
  const friends_1 = require("@ouro.bot/friends");
54
54
  const cli_help_1 = require("./cli-help");
55
+ const surface_1 = require("../../senses/shepherd/surface");
55
56
  const local_audio_control_1 = require("../../senses/voice/local-audio-control");
56
57
  const cli_desk_1 = require("./cli-desk");
57
58
  const flight_recorder_1 = require("../../arc/flight-recorder");
@@ -3120,6 +3121,24 @@ function parseOuroCommand(args) {
3120
3121
  return parseConfigCommand(args.slice(1));
3121
3122
  if (head === "mcp")
3122
3123
  return parseMcpCommand(args.slice(1));
3124
+ if (head === "shepherd") {
3125
+ const usage = "Usage: ouro shepherd chat --agent <name> [--session <UUID>]";
3126
+ if (args[1] !== "chat")
3127
+ throw new Error(usage);
3128
+ const values = new Map();
3129
+ for (let i = 2; i < args.length; i += 2) {
3130
+ const flag = args[i];
3131
+ const value = args[i + 1];
3132
+ if (!["--agent", "--session"].includes(flag) || values.has(flag) || !value || value.startsWith("--"))
3133
+ throw new Error(usage);
3134
+ values.set(flag, value);
3135
+ }
3136
+ const agent = values.get("--agent");
3137
+ const session = values.get("--session");
3138
+ if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent) || (session !== undefined && !surface_1.SHEPHERD_UUID.test(session)))
3139
+ throw new Error(usage);
3140
+ return { kind: "shepherd.chat", agent, ...(session ? { session } : {}) };
3141
+ }
3123
3142
  if (head === "voice")
3124
3143
  return parseVoiceCommand(args.slice(1));
3125
3144
  if (head === "whoami") {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FrontendSessionService = exports.FrontendSessionConflictError = exports.FrontendTurnConflictError = void 0;
4
4
  const shared_turn_1 = require("../senses/shared-turn");
5
5
  const runtime_1 = require("../nerves/runtime");
6
+ const surface_1 = require("../senses/shepherd/surface");
6
7
  class FrontendTurnConflictError extends Error {
7
8
  constructor(turnId) {
8
9
  super(`frontend turn already exists: ${turnId}`);
@@ -33,15 +34,52 @@ function required(value, field) {
33
34
  class FrontendSessionService {
34
35
  activeTurns = new Map();
35
36
  activeSessions = new Map();
37
+ turnRefs = new Map();
36
38
  listeners = new Set();
37
39
  runner;
38
40
  journal;
39
41
  authority;
40
42
  closed = false;
43
+ shepherdTargets;
41
44
  constructor(options = {}) {
42
45
  this.runner = options.runner ?? shared_turn_1.runSenseTurn;
43
46
  this.journal = options.journal ?? null;
44
47
  this.authority = options.authority ?? null;
48
+ this.shepherdTargets = options.shepherdTargets ?? surface_1.authorizedShepherdTargets;
49
+ }
50
+ listShepherdTargets(agent, friendId, composerTty) {
51
+ if (composerTty !== undefined && !/^\/dev\/[A-Za-z0-9/_-]{1,120}$/.test(composerTty))
52
+ throw new Error("composerTty must be a terminal device path");
53
+ return this.shepherdTargets(required(agent, "agent"), required(friendId, "friendId"), composerTty);
54
+ }
55
+ async resolveShepherdSession(agent, friendId, value, composerTty) {
56
+ const id = value && typeof value === "object" ? value.id : undefined;
57
+ if (typeof id !== "string")
58
+ throw new Error("shepherdSession requires a stable UUID");
59
+ const targets = await this.listShepherdTargets(agent, friendId, composerTty);
60
+ const target = targets.find(candidate => candidate.id.toLowerCase() === id.toLowerCase());
61
+ if (!target)
62
+ throw new Error(`selected Shepherd terminal is unavailable: ${id}`);
63
+ return Object.freeze({ ...target });
64
+ }
65
+ turnStatus(ref, turnId) {
66
+ const events = [];
67
+ if (this.journal) {
68
+ let afterSequence = 0;
69
+ for (;;) {
70
+ const replay = this.loadSession(ref, { afterSequence });
71
+ if (replay.degraded)
72
+ throw new Error("frontend journal is degraded; turn status is unknown");
73
+ events.push(...replay.events.filter(event => event.turnId === turnId));
74
+ if (!replay.hasMore)
75
+ break;
76
+ afterSequence = replay.events.at(-1).sequence;
77
+ }
78
+ }
79
+ const activeRef = this.turnRefs.get(turnId);
80
+ const active = activeRef && activeRef.agent === ref.agent && activeRef.friendId === ref.friendId && activeRef.sessionKey === ref.sessionKey;
81
+ const terminal = events.some(event => ["turn_completed", "turn_failed", "turn_cancelled"].includes(event.type));
82
+ return { state: terminal ? "completed" : active ? "active" : events.length ? "incomplete" : "unknown", events };
45
83
  }
46
84
  hasTurn(turnId) {
47
85
  return this.activeTurns.has(turnId);
@@ -102,15 +140,19 @@ class FrontendSessionService {
102
140
  agent: required(request.agent, "agent"),
103
141
  friendId: required(request.friendId, "friendId"),
104
142
  sessionKey: required(request.sessionKey, "sessionKey"),
105
- message: required(request.message, "message"),
143
+ message: (required(request.message, "message"), request.message),
144
+ ...(request.shepherdSession ? { shepherdSession: Object.freeze({ ...request.shepherdSession }) } : {}),
106
145
  };
107
146
  if (this.activeTurns.has(turnId))
108
147
  throw new FrontendTurnConflictError(turnId);
148
+ if (this.journal && this.turnStatus(normalized, turnId).state !== "unknown")
149
+ throw new FrontendTurnConflictError(turnId);
109
150
  const sessionIdentity = [normalized.agent, normalized.friendId, normalized.channel, normalized.sessionKey].join("\0");
110
151
  if (this.activeSessions.has(sessionIdentity))
111
152
  throw new FrontendSessionConflictError(normalized.sessionKey);
112
153
  const controller = new AbortController();
113
154
  this.activeTurns.set(turnId, controller);
155
+ this.turnRefs.set(turnId, normalized);
114
156
  this.activeSessions.set(sessionIdentity, turnId);
115
157
  return { request: normalized, sessionIdentity, controller, started: false };
116
158
  }
@@ -143,6 +185,7 @@ class FrontendSessionService {
143
185
  channel: normalized.channel,
144
186
  sessionKey: normalized.sessionKey,
145
187
  userMessage: normalized.message,
188
+ ...(normalized.shepherdSession ? { shepherdSession: normalized.shepherdSession } : {}),
146
189
  signal: controller.signal,
147
190
  latencyMode: "live",
148
191
  frontendEventSink,
@@ -191,6 +234,7 @@ class FrontendSessionService {
191
234
  }
192
235
  finally {
193
236
  this.activeTurns.delete(turnId);
237
+ this.turnRefs.delete(turnId);
194
238
  this.activeSessions.delete(sessionIdentity);
195
239
  }
196
240
  }
@@ -255,6 +255,20 @@ async function startFrontendSocketServer(options) {
255
255
  }
256
256
  const method = requiredString(request.method, "method");
257
257
  const params = record(request.params ?? {});
258
+ if (method === "shepherd.targets") {
259
+ const targets = await options.service.listShepherdTargets(requiredString(params.agent, "agent"), requiredString(params.friendId, "friendId"), params.composerTty === undefined ? undefined : requiredString(params.composerTty, "composerTty"));
260
+ client.writer.send(response(id, { targets }));
261
+ return;
262
+ }
263
+ if (method === "turn.status") {
264
+ const result = options.service.turnStatus({
265
+ agent: requiredString(params.agent, "agent"),
266
+ friendId: requiredString(params.friendId, "friendId"),
267
+ sessionKey: requiredString(params.sessionKey, "sessionKey"),
268
+ }, requiredString(params.turnId, "turnId"));
269
+ client.writer.send(response(id, result));
270
+ return;
271
+ }
258
272
  if (method === "session.load") {
259
273
  const result = options.service.loadSession({
260
274
  agent: requiredString(params.agent, "agent"),
@@ -296,16 +310,25 @@ async function startFrontendSocketServer(options) {
296
310
  throw new Error("turn.start.observe-only cannot include runtimeMcpServers");
297
311
  }
298
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");
316
+ }
299
317
  const turnRequest = {
300
318
  turnId: requiredString(params.turnId, "turnId"),
301
319
  agent: requiredString(params.agent, "agent"),
302
320
  friendId: requiredString(params.friendId, "friendId"),
303
321
  channel: requiredString(params.channel, "channel"),
304
322
  sessionKey: requiredString(params.sessionKey, "sessionKey"),
305
- message: requiredString(params.message, "message"),
323
+ message: (requiredString(params.message, "message"), params.message),
324
+ ...(shepherdSession ? { shepherdSession } : {}),
306
325
  ...(observeOnly ? { disableTools: true, ephemeral: true } : {}),
307
326
  ...(runtimeMcpServers ? { runtimeMcpServers } : {}),
308
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)
331
+ return;
309
332
  const prepared = options.service.prepareTurn(turnRequest);
310
333
  client.ownedTurnIds.add(turnRequest.turnId);
311
334
  client.writer.send(response(id, { accepted: true, turnId: turnRequest.turnId }));
@@ -323,6 +346,7 @@ async function startFrontendSocketServer(options) {
323
346
  writer: new FrontendFrameWriter(socket, maxQueuedFrames, ownedTurnIds.clear.bind(ownedTurnIds)),
324
347
  subscriptions: new Set(),
325
348
  ownedTurnIds,
349
+ closed: false,
326
350
  };
327
351
  clients.add(client);
328
352
  const lines = readline.createInterface({ input: socket, crlfDelay: Infinity });
@@ -332,6 +356,7 @@ async function startFrontendSocketServer(options) {
332
356
  if (cleanedUp)
333
357
  return;
334
358
  cleanedUp = true;
359
+ client.closed = true;
335
360
  for (const turnId of client.ownedTurnIds)
336
361
  options.service.cancelTurn(turnId);
337
362
  clients.delete(client);
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shepherdGuidanceToolDefinitions = void 0;
4
+ const runtime_credentials_1 = require("../heart/runtime-credentials");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const shepherd_config_1 = require("../heart/shepherd-config");
7
+ const host_1 = require("../senses/shepherd/host");
8
+ const guidance_1 = require("../senses/shepherd/guidance");
9
+ const redact_1 = require("../senses/shepherd/redact");
10
+ const returns_1 = require("../senses/shepherd/returns");
11
+ const notice_outbox_1 = require("../senses/shepherd/notice-outbox");
12
+ const target_binding_1 = require("../senses/shepherd/target-binding");
13
+ const tools_base_1 = require("./tools-base");
14
+ const runtime_1 = require("../nerves/runtime");
15
+ const text = (value) => typeof value === "string" ? value.trim() : "";
16
+ const json = (value) => JSON.stringify(value, (_key, item) => typeof item === "string" ? (0, redact_1.redactSecrets)(item) : item);
17
+ const stringProperty = (description) => ({ type: "string", description });
18
+ const targetProperties = {
19
+ host: stringProperty("Host name from history, when known. Never invent a host."),
20
+ session: stringProperty("Stable session ID from history, or a live session ID/ref from shepherd_overview."),
21
+ };
22
+ const requestProperty = { request_id: stringProperty("Unique request ID for this human instruction. Reuse it only to retry the identical request.") };
23
+ function fingerprint(name, args) {
24
+ const body = JSON.stringify({ name, args }, (_key, value) => value !== null && typeof value === "object" && !Array.isArray(value)
25
+ ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value);
26
+ return (0, node_crypto_1.createHash)("sha256").update(body).digest("hex");
27
+ }
28
+ /**
29
+ * Resolve history without a terminal connection, including vanished sessions. Only a session with no history needs
30
+ * the existing machine connection primitives. Resolving it never reads a screen, signals, presses a key or sends.
31
+ */
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);
35
+ const records = (0, returns_1.readReturns)(ctx.agentRoot);
36
+ const returnId = text(args.return_id);
37
+ if (returnId) {
38
+ const record = records.find(r => (0, guidance_1.shepherdReturnId)(r) === returnId);
39
+ if (!record)
40
+ throw new Error("Shepherd return ID not found");
41
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx.currentShepherdSurface, record.session, record.host);
42
+ if ((text(args.host) && text(args.host) !== record.host) || (text(args.session) && text(args.session) !== record.session))
43
+ throw new Error("target does not match the recorded return");
44
+ return { target: { host: record.host, session: record.session, cwd: record.cwd, task: record.task }, live: [] };
45
+ }
46
+ const session = text(args.session);
47
+ if (!session)
48
+ throw new Error("name a session from Shepherd history or overview");
49
+ const hostName = text(args.host);
50
+ const matching = records.filter(r => r.session === session && (!hostName || r.host === hostName));
51
+ if (new Set(matching.map(r => r.host)).size > 1)
52
+ throw new Error("session is ambiguous across hosts; name its host");
53
+ const record = matching.at(-1);
54
+ if (record) {
55
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx.currentShepherdSurface, record.session, record.host);
56
+ return { target: { host: record.host, session, cwd: record.cwd, task: record.task }, live: [] };
57
+ }
58
+ const machine = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(ctx.agentName);
59
+ const resolved = (0, shepherd_config_1.resolveShepherdConnection)(ctx.agentName, machine.ok ? machine.config : {});
60
+ if (!resolved.ok)
61
+ throw new Error(resolved.error);
62
+ const host = (0, host_1.createShepherdHost)(resolved.connection, { agentName: ctx.agentName });
63
+ const sessions = await host.list();
64
+ const live = sessions.map(s => ({ host: host.name, session: s.id, cwd: s.cwd ?? null, task: null }));
65
+ const found = sessions.find(s => s.id === session || s.ref === session);
66
+ if (!found || (hostName && hostName !== host.name))
67
+ throw new Error("unknown session on the configured Shepherd host");
68
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx.currentShepherdSurface, found.id, host.name);
69
+ return { target: live.find(t => t.session === found.id), live };
70
+ }
71
+ function definition(name, description, properties, required, run, mutates = false) {
72
+ return {
73
+ tool: { type: "function", function: { name, description, parameters: { type: "object", properties, required, additionalProperties: false } } },
74
+ handler: async (args, ctx) => {
75
+ // Same real-family gate as shepherd_send, including rejection of the private runtime's family self.
76
+ const friend = ctx?.context?.friend;
77
+ if (!friend || friend.trustLevel !== "family" || !friend.id || friend.id === tools_base_1.SELF_FRIEND_ID)
78
+ return json({ error: "Shepherd private guidance is only for a real family member in this conversation" });
79
+ if (!ctx?.agentRoot || !ctx.agentName)
80
+ return json({ error: "Shepherd guidance needs an agent runtime" });
81
+ try {
82
+ const request = { actorId: friend.id, requestId: text(args.request_id), requestFingerprint: fingerprint(name, args) };
83
+ if (mutates) {
84
+ const prior = (0, guidance_1.readGuidance)(ctx.agentRoot).requests.find(r => r.actorId === request.actorId && r.requestId === request.requestId);
85
+ if (prior) {
86
+ if (prior.requestFingerprint !== request.requestFingerprint)
87
+ throw new Error("request ID was already used with a different body");
88
+ return json(prior.receipt);
89
+ }
90
+ }
91
+ const result = await run(args, ctx, request);
92
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.shepherd_guidance_tool", message: "executed a private Shepherd guidance tool", meta: { tool: name, mutates } });
93
+ return json(result);
94
+ }
95
+ catch (error) {
96
+ const message = (0, redact_1.redactSecrets)(error instanceof Error ? error.message : String(error));
97
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "repertoire", event: "repertoire.shepherd_guidance_tool_error", message: "private Shepherd guidance tool failed", meta: { tool: name, error: "private_guidance_operation_failed" } });
98
+ return json({ error: message });
99
+ }
100
+ },
101
+ summaryKeys: [],
102
+ riskProfile: mutates ? { mutates: "durable_state_write", risk: "high", reason: "changes private operator preferences or the automatic-reply pause" } : { mutates: "none", risk: "low" },
103
+ };
104
+ }
105
+ exports.shepherdGuidanceToolDefinitions = [
106
+ 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) => {
107
+ 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)))
109
+ .map(record => {
110
+ const id = (0, guidance_1.shepherdReturnId)(record);
111
+ return { ...record, id, displayId: id.slice("return:".length, "return:".length + 8) };
112
+ });
113
+ try {
114
+ const notices = (0, notice_outbox_1.readNoticeOutbox)(ctx.agentRoot);
115
+ for (const record of returns) {
116
+ const notice = (0, notice_outbox_1.knownNotice)(ctx.agentRoot, (0, notice_outbox_1.noticeRecordId)(record), notices);
117
+ if (notice)
118
+ record.ownerNotice = Object.hasOwn(notice.deliveries, request.actorId) ? notice.deliveries[request.actorId] : null;
119
+ }
120
+ return { returns };
121
+ }
122
+ catch (error) {
123
+ const message = (0, redact_1.redactSecrets)(error instanceof Error ? error.message : String(error));
124
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "repertoire", event: "repertoire.shepherd_notice_history_error", message: "owner notice receipt evidence is unavailable; return history remains readable", meta: { error: "notice_receipts_unavailable" } });
125
+ return { returns, ownerNoticeError: message };
126
+ }
127
+ }),
128
+ definition("shepherd_correct", "Record the human's post-action correction ('that was wrong because…') or prospective instruction. Optionally learn a principle for 'in future…'. A return_id must name a real history entry; omit it for a prospective instruction, never fabricate one. Session scope is the default; project is the exact recorded folder and optional task; global only when the human deliberately generalizes. Feedback <=2000 characters; principle <=1000; at most 20 active rules. This is not an approval gate and never sends anything. For requested immediate steering use existing shepherd_send.", {
129
+ ...targetProperties, ...requestProperty,
130
+ return_id: stringProperty("Real stable return ID from shepherd_history, if correcting an actual judgment."),
131
+ feedback: stringProperty("The human's explanation or prospective instruction, at most 2000 characters."),
132
+ principle: stringProperty("Optional scoped future preference, at most 1000 characters."),
133
+ scope: { type: "string", enum: ["session", "project", "global"], description: "Default session. Choose global only when the human expressed generalization." },
134
+ }, ["request_id", "feedback"], async (args, ctx, request) => {
135
+ const { target, live } = await targetFor(ctx, args);
136
+ return (0, guidance_1.recordShepherdCorrection)(ctx.agentRoot, {
137
+ ...request, target, feedback: typeof args.feedback === "string" ? args.feedback : "",
138
+ ...(args.return_id !== undefined ? { returnId: text(args.return_id) } : {}),
139
+ ...(args.principle !== undefined ? { principle: typeof args.principle === "string" ? args.principle : "" } : {}),
140
+ ...(args.scope !== undefined ? { scope: args.scope } : {}),
141
+ }, live);
142
+ }, true),
143
+ 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) {
145
+ const state = (0, guidance_1.readGuidance)(ctx.agentRoot);
146
+ return { revision: state.revision, principles: state.principles.filter(p => p.active), pauses: state.pauses.filter(p => p.paused) };
147
+ }
148
+ const { target } = await targetFor(ctx, args);
149
+ return (0, guidance_1.applicableGuidance)((0, guidance_1.readGuidance)(ctx.agentRoot), target);
150
+ }),
151
+ definition("shepherd_update_principle", "Change a learned preference ('change that') by its principle ID. Keeps its existing scope, never silently generalizes it. No send.", {
152
+ ...requestProperty, principle_id: stringProperty("Active principle ID from shepherd_guidance."), text: stringProperty("Replacement preference, nonempty and at most 1000 characters."),
153
+ }, ["request_id", "principle_id", "text"], async (args, ctx, request) => (0, guidance_1.updateShepherdPrinciple)(ctx.agentRoot, {
154
+ ...request, principleId: text(args.principle_id), text: typeof args.text === "string" ? args.text : "",
155
+ }), true),
156
+ definition("shepherd_forget_principle", "Forget a learned preference ('forget that') by its ID. Removes it from future judgments but retains the private operator trail. No send.", {
157
+ ...requestProperty, principle_id: stringProperty("Active principle ID from shepherd_guidance."),
158
+ }, ["request_id", "principle_id"], async (args, ctx, request) => (0, guidance_1.forgetShepherdPrinciple)(ctx.agentRoot, {
159
+ ...request, principleId: text(args.principle_id),
160
+ }), true),
161
+ ...["pause", "resume"].map(action => definition(`shepherd_${action}`, action === "pause" ? "Leave this one alone: pause automatic Shepherd replies for this session only, durably. Independent judgment/owner notices remain. Never interrupts or types into the terminal."
162
+ : "Resume automatic Shepherd replies for this paused session. All focus, screen, syntax and loop guards still apply; resume is not an immediate send.", { ...targetProperties, ...requestProperty }, ["session", "request_id"], async (args, ctx, request) => {
163
+ const { target, live } = await targetFor(ctx, args);
164
+ return (0, guidance_1.setShepherdSessionPause)(ctx.agentRoot, { ...request, target, paused: action === "pause" }, live);
165
+ }, true)),
166
+ ];
@@ -44,6 +44,9 @@ const host_1 = require("../senses/shepherd/host");
44
44
  const redact_1 = require("../senses/shepherd/redact");
45
45
  const returns_1 = require("../senses/shepherd/returns");
46
46
  const tools_base_1 = require("./tools-base");
47
+ const tools_shepherd_guidance_1 = require("./tools-shepherd-guidance");
48
+ const guidance_1 = require("../senses/shepherd/guidance");
49
+ const target_binding_1 = require("../senses/shepherd/target-binding");
47
50
  /**
48
51
  * Shepherd's tools: every coding-agent session on this machine's terminal host with a current screen
49
52
  * and what Shepherd last decided about it (`shepherd_overview`), a deeper read (`shepherd_read`), a status
@@ -85,6 +88,12 @@ async function withHost(tool, ctx, run) {
85
88
  if (typeof host === "string")
86
89
  return JSON.stringify({ error: host });
87
90
  try {
91
+ if (ctx.currentShepherdSurface) {
92
+ const bound = ctx.currentShepherdSurface;
93
+ (0, target_binding_1.assertBoundShepherdTarget)(bound, bound.id, host.name);
94
+ if (!(await host.list()).some(session => session.id === bound.id))
95
+ throw new Error(`bound terminal ${bound.id} is gone; nothing was read or sent`);
96
+ }
88
97
  const result = await run(host);
89
98
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.shepherd_tool", message: "ran a Shepherd tool", meta: { tool } });
90
99
  return JSON.stringify(result);
@@ -129,7 +138,7 @@ exports.shepherdOverviewToolDefinition = {
129
138
  terminalCount: sessions.length,
130
139
  sessions: await Promise.all(sessions.map(async (session) => {
131
140
  const record = last.get(session.id);
132
- return { ...session, currentScreen: await currentScreen(host, session.id, screenLimit), ...(record ? { lastReturn: { at: record.at, kind: record.kind, action: record.action, reason: record.reason, reply: record.reply, task: record.task } } : {}) };
141
+ return { ...session, currentScreen: await currentScreen(host, session.id, screenLimit), ...(record ? { lastReturn: { id: (0, guidance_1.shepherdReturnId)(record), at: record.at, kind: record.kind, action: record.action, reason: record.reason, reply: record.reply, task: record.task } } : {}) };
133
142
  })),
134
143
  };
135
144
  }),
@@ -162,6 +171,7 @@ exports.shepherdReadToolDefinition = {
162
171
  if (!session)
163
172
  return Promise.resolve(JSON.stringify({ error: "name a session id or ref from shepherd_overview" }));
164
173
  return withHost("shepherd_read", ctx, async (host) => {
174
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx?.currentShepherdSurface, session, host.name);
165
175
  const lines = boundedLines(args.lines);
166
176
  // A fingerprint needs enough lines to match the one shepherd_send takes, whatever the caller asked to see.
167
177
  const raw = await host.read(session, Math.max(lines, redact_1.FINGERPRINT_READ_LINES));
@@ -200,6 +210,7 @@ exports.shepherdSignalToolDefinition = {
200
210
  if (status.length > STATUS_MAX_CHARS || /[\r\n]/.test(status))
201
211
  return Promise.resolve(JSON.stringify({ error: `status must be one line of at most ${STATUS_MAX_CHARS} characters` }));
202
212
  return withHost("shepherd_signal", ctx, async (host) => {
213
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx?.currentShepherdSurface, session, host.name);
203
214
  await host.signal(session, status || null, title ? { title, body: text(args.notify_body) } : undefined);
204
215
  return { session, status: status ? "set" : "cleared", notified: !!title };
205
216
  });
@@ -254,6 +265,7 @@ exports.shepherdSendToolDefinition = {
254
265
  if (CHOOSING_KEYS.has(key) && !seen)
255
266
  return refuse(`${key} can approve or choose on a prompt, so pass screen from a shepherd_read made just before`);
256
267
  return withHost("shepherd_send", ctx, async (host) => {
268
+ (0, target_binding_1.assertBoundShepherdTarget)(ctx?.currentShepherdSurface, session, host.name);
257
269
  const target = (await host.list()).find((entry) => entry.id === session || entry.ref === session);
258
270
  if (!target)
259
271
  return refused(`session ${session} is gone; check shepherd_overview`, meta);
@@ -280,4 +292,4 @@ exports.shepherdSendToolDefinition = {
280
292
  summaryKeys: ["session", "key"],
281
293
  riskProfile: { mutates: "external_side_effect", risk: "high", reason: "types into a coding agent's terminal for the human, which can answer or approve its prompts" },
282
294
  };
283
- exports.shepherdToolDefinitions = [exports.shepherdOverviewToolDefinition, exports.shepherdReadToolDefinition, exports.shepherdSignalToolDefinition, exports.shepherdSendToolDefinition];
295
+ exports.shepherdToolDefinitions = [exports.shepherdOverviewToolDefinition, exports.shepherdReadToolDefinition, exports.shepherdSignalToolDefinition, exports.shepherdSendToolDefinition, ...tools_shepherd_guidance_1.shepherdGuidanceToolDefinitions];
@@ -347,6 +347,7 @@ function erroredTurnReply(errorMessage) {
347
347
  : "I couldn't finish that one: something went wrong before I had an answer. Ask me again and I'll retry.";
348
348
  }
349
349
  async function runSenseTurn(options) {
350
+ options = { ...options, ...(options.shepherdSession ? { shepherdSession: Object.freeze({ ...options.shepherdSession }) } : {}) };
350
351
  return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
351
352
  const owner = Object.freeze({ agentName: options.agentName, agentRoot: (0, identity_1.getAgentRoot)(options.agentName) });
352
353
  (0, identity_1.setAgentName)(owner.agentName);
@@ -452,6 +453,15 @@ async function runSenseTurnExclusive(options, owner) {
452
453
  role: "system",
453
454
  content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, options.disableTools ? { tools: [], hardDisableTools: true } : {}, undefined)),
454
455
  }];
456
+ if (options.shepherdSession) {
457
+ // Replace rather than accumulate context as the conversation reloads. It is bounded
458
+ // metadata, not a human instruction, and focus changes cannot alter its stable UUID.
459
+ const marker = "[Ouro Shepherd terminal context]";
460
+ const system = sessionMessages.find(message => message.role === "system");
461
+ if (system && typeof system.content === "string") {
462
+ system.content = system.content.split(`\n${marker}`)[0] + `\n${marker}\n${JSON.stringify(options.shepherdSession)}\nUse this exact UUID for shepherd_read/shepherd_send. If gone, fail explicitly; never substitute a short ref. This locator grants no authority.`;
463
+ }
464
+ }
455
465
  if (precommittedIngressEvent) {
456
466
  const projectedIngress = exactProjectedIngressMessage(existing, sessionMessages, precommittedIngressEvent.id, baseSession.value);
457
467
  if (!projectedIngress || projectedIngress.role !== "user" || projectedIngress.content !== userMessage)
@@ -612,6 +622,7 @@ async function runSenseTurnExclusive(options, owner) {
612
622
  agentName,
613
623
  agentRoot,
614
624
  currentUserMessage: userMessage,
625
+ ...(options.shepherdSession ? { currentShepherdSurface: Object.freeze({ ...options.shepherdSession }) } : {}),
615
626
  },
616
627
  },
617
628
  ...(options.prepareRunAgentOptions ? { prepareRunAgentOptions: options.prepareRunAgentOptions } : {}),