@ouro.bot/cli 0.1.0-alpha.923 → 0.1.0-alpha.925
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 +20 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-exec.js +23 -0
- package/dist/heart/daemon/cli-help.js +7 -0
- package/dist/heart/daemon/cli-parse.js +19 -0
- package/dist/heart/frontend-session-service.js +45 -1
- package/dist/heart/frontend-socket.js +26 -1
- package/dist/repertoire/tools-shepherd-guidance.js +166 -0
- package/dist/repertoire/tools-shepherd.js +15 -3
- package/dist/senses/shared-turn.js +11 -0
- package/dist/senses/shepherd/composer.js +317 -0
- package/dist/senses/shepherd/guidance.js +270 -0
- package/dist/senses/shepherd/judge.js +8 -2
- package/dist/senses/shepherd/notice-outbox.js +262 -0
- package/dist/senses/shepherd/owner-notice.js +98 -24
- package/dist/senses/shepherd/sense.js +45 -15
- package/dist/senses/shepherd/surface.js +119 -0
- package/dist/senses/shepherd/target-binding.js +13 -0
- package/dist/senses/shepherd/terminal-process.js +22 -1
- package/dist/senses/teams.js +23 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
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.925",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Report every Shepherd terminal with its full label and distinguish outside dependency waits, idle local completion, and delivered work; forgetting a learned preference leaves built-in waiting policy unchanged."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.924",
|
|
12
|
+
"changes": [
|
|
13
|
+
"Talk to Ouro about a fixed coding terminal through an independent Shepherd composer, even while its agent is busy.",
|
|
14
|
+
"Correct Shepherd after an intervention, inspect or change scoped learned guidance, and pause automatic replies without interrupting coding work.",
|
|
15
|
+
"Retain every eligible owner notice across restart, record Teams acceptance, and retry only definitely unsent deliveries.",
|
|
16
|
+
"Enforce the composer's fixed UUID and host at terminal and guidance execution boundaries; concurrent notice workers cannot replay an accepted or uncertain send.",
|
|
17
|
+
"Recognize current Node-hosted Copilot terminals by their foreground launcher entry point, so the independent composer remains available on Copilot 1.0.97.",
|
|
18
|
+
"Trace fixed-target composer submissions, authorized target discovery, committed guidance revisions, and bound-target refusals without logging private human feedback.",
|
|
19
|
+
"Answer notice-delivery questions from private recipient-specific accepted, pending or uncertain receipts, including archived acceptances, without replaying sends or hiding unavailable evidence.",
|
|
20
|
+
"Keep private error details in redacted human responses and record only structured failure codes in control/learning telemetry.",
|
|
21
|
+
"Use authoritative short return labels in human answers while retaining exact immutable identifiers for post-action corrections."
|
|
22
|
+
]
|
|
23
|
+
},
|
|
4
24
|
{
|
|
5
25
|
"version": "0.1.0-alpha.923",
|
|
6
26
|
"changes": [
|
|
@@ -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.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.925</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 only that preference from future judgments but retains the private operator trail. Built-in judgment policy, including legitimate outside dependency waits, and all existing guards remain unchanged: forgetting does not make dependency waits premature or delivered done. 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);
|
|
@@ -114,7 +123,7 @@ exports.shepherdOverviewToolDefinition = {
|
|
|
114
123
|
type: "function",
|
|
115
124
|
function: {
|
|
116
125
|
name: "shepherd_overview",
|
|
117
|
-
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. 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.
|
|
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.",
|
|
118
127
|
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
119
128
|
},
|
|
120
129
|
},
|
|
@@ -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 } : {}),
|