@ouro.bot/cli 0.1.0-alpha.205 → 0.1.0-alpha.206
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 +6 -0
- package/dist/heart/config-registry.js +305 -0
- package/dist/heart/core.js +4 -2
- package/dist/heart/start-of-turn-packet.js +103 -5
- package/dist/repertoire/tools-base.js +225 -0
- package/dist/repertoire/tools.js +7 -0
- package/dist/senses/pipeline.js +4 -0
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
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.206",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat: capability discovery and tiered self-configuration — config registry with tier-aware metadata (T1 self-service, T2 proposal, T3 operator-only), read_config tool with topic-filtered discovery, update_config tool for T1 immediate changes, propose_config tool for T2 operator-approval flow, version-change surfacing in start-of-turn packet via buildCapabilitiesSection"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.205",
|
|
6
12
|
"changes": [
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONFIG_REGISTRY = void 0;
|
|
4
|
+
exports.getRegistryEntries = getRegistryEntries;
|
|
5
|
+
exports.getRegistryEntriesByTier = getRegistryEntriesByTier;
|
|
6
|
+
exports.getRegistryEntriesByTopic = getRegistryEntriesByTopic;
|
|
7
|
+
exports.getRegistryEntry = getRegistryEntry;
|
|
8
|
+
const runtime_1 = require("../nerves/runtime");
|
|
9
|
+
// --- Validation helpers ---
|
|
10
|
+
const KNOWN_PROVIDERS = ["anthropic", "azure", "minimax", "openai-codex", "github-copilot"];
|
|
11
|
+
function validateNumber(value) {
|
|
12
|
+
if (typeof value !== "number")
|
|
13
|
+
return `expected number, got ${typeof value}`;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
function validateBoolean(value) {
|
|
17
|
+
if (typeof value !== "boolean")
|
|
18
|
+
return `expected boolean, got ${typeof value}`;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function validateString(value) {
|
|
22
|
+
if (typeof value !== "string")
|
|
23
|
+
return `expected string, got ${typeof value}`;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function validateStringEnum(allowed) {
|
|
27
|
+
return (value) => {
|
|
28
|
+
if (typeof value !== "string")
|
|
29
|
+
return `expected string, got ${typeof value}`;
|
|
30
|
+
if (!allowed.includes(value))
|
|
31
|
+
return `expected one of [${allowed.join(", ")}], got "${value}"`;
|
|
32
|
+
return undefined;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function validateInteger(min, max) {
|
|
36
|
+
return (value) => {
|
|
37
|
+
if (typeof value !== "number")
|
|
38
|
+
return `expected number, got ${typeof value}`;
|
|
39
|
+
if (!Number.isInteger(value))
|
|
40
|
+
return `expected integer, got ${value}`;
|
|
41
|
+
if (value < min || value > max)
|
|
42
|
+
return `expected integer between ${min} and ${max}, got ${value}`;
|
|
43
|
+
return undefined;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function validateStringArray(value) {
|
|
47
|
+
if (!Array.isArray(value))
|
|
48
|
+
return `expected array, got ${typeof value}`;
|
|
49
|
+
for (let i = 0; i < value.length; i++) {
|
|
50
|
+
if (typeof value[i] !== "string")
|
|
51
|
+
return `expected string at index ${i}, got ${typeof value[i]}`;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function validateStringEnumArray(allowed) {
|
|
56
|
+
return (value) => {
|
|
57
|
+
if (!Array.isArray(value))
|
|
58
|
+
return `expected array, got ${typeof value}`;
|
|
59
|
+
for (let i = 0; i < value.length; i++) {
|
|
60
|
+
if (typeof value[i] !== "string")
|
|
61
|
+
return `expected string at index ${i}, got ${typeof value[i]}`;
|
|
62
|
+
if (!allowed.includes(value[i]))
|
|
63
|
+
return `expected one of [${allowed.join(", ")}] at index ${i}, got "${value[i]}"`;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function validateObject(requiredFields) {
|
|
69
|
+
return (value) => {
|
|
70
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
71
|
+
return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
|
|
72
|
+
}
|
|
73
|
+
const obj = value;
|
|
74
|
+
for (const [field, validator] of Object.entries(requiredFields)) {
|
|
75
|
+
if (!(field in obj))
|
|
76
|
+
return `missing required field "${field}"`;
|
|
77
|
+
const err = validator(obj[field]);
|
|
78
|
+
if (err)
|
|
79
|
+
return `field "${field}": ${err}`;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const registryData = [
|
|
85
|
+
// --- Tier 3: Operator-only ---
|
|
86
|
+
{
|
|
87
|
+
path: "version",
|
|
88
|
+
tier: 3,
|
|
89
|
+
description: "Agent config schema version. Must be an integer >= 1.",
|
|
90
|
+
default: 2,
|
|
91
|
+
effects: "Controls which config migrations apply on load. Changing incorrectly can corrupt config.",
|
|
92
|
+
topics: ["schema", "migration"],
|
|
93
|
+
validate: validateNumber,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: "enabled",
|
|
97
|
+
tier: 3,
|
|
98
|
+
description: "Whether the agent is enabled. When false, the agent refuses to start.",
|
|
99
|
+
default: true,
|
|
100
|
+
effects: "Disables all agent functionality when set to false.",
|
|
101
|
+
topics: ["lifecycle", "activation"],
|
|
102
|
+
validate: validateBoolean,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
path: "mcpServers",
|
|
106
|
+
tier: 3,
|
|
107
|
+
description: "MCP server configurations. Maps server name to command, args, and env.",
|
|
108
|
+
default: undefined,
|
|
109
|
+
effects: "Adds or removes MCP tool servers. Incorrect config can expose the agent to untrusted tool sources.",
|
|
110
|
+
topics: ["tools", "mcp", "servers", "extensions"],
|
|
111
|
+
},
|
|
112
|
+
// --- Tier 2: Proposal ---
|
|
113
|
+
{
|
|
114
|
+
path: "humanFacing.provider",
|
|
115
|
+
tier: 2,
|
|
116
|
+
description: "Provider for human-facing interactions (CLI, Teams, BlueBubbles).",
|
|
117
|
+
default: "anthropic",
|
|
118
|
+
effects: "Changes the LLM provider used for all human-facing conversations. Affects quality, latency, and cost.",
|
|
119
|
+
topics: ["model", "provider", "llm", "human"],
|
|
120
|
+
validate: validateStringEnum(KNOWN_PROVIDERS),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
path: "humanFacing.model",
|
|
124
|
+
tier: 2,
|
|
125
|
+
description: "Model name for human-facing interactions.",
|
|
126
|
+
default: "claude-opus-4-6",
|
|
127
|
+
effects: "Changes the specific model used for human-facing conversations.",
|
|
128
|
+
topics: ["model", "provider", "llm", "human"],
|
|
129
|
+
validate: validateString,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
path: "agentFacing.provider",
|
|
133
|
+
tier: 2,
|
|
134
|
+
description: "Provider for agent-facing interactions (inner dialog, delegation).",
|
|
135
|
+
default: "anthropic",
|
|
136
|
+
effects: "Changes the LLM provider used for inner dialog and agent-to-agent communication.",
|
|
137
|
+
topics: ["model", "provider", "llm", "agent", "inner-dialog"],
|
|
138
|
+
validate: validateStringEnum(KNOWN_PROVIDERS),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
path: "agentFacing.model",
|
|
142
|
+
tier: 2,
|
|
143
|
+
description: "Model name for agent-facing interactions.",
|
|
144
|
+
default: "claude-opus-4-6",
|
|
145
|
+
effects: "Changes the specific model used for inner dialog and agent-to-agent communication.",
|
|
146
|
+
topics: ["model", "provider", "llm", "agent", "inner-dialog"],
|
|
147
|
+
validate: validateString,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
path: "context.maxTokens",
|
|
151
|
+
tier: 2,
|
|
152
|
+
description: "Maximum context window size in tokens.",
|
|
153
|
+
default: 80000,
|
|
154
|
+
effects: "Larger values allow more context but increase cost and latency. Must match model capability.",
|
|
155
|
+
topics: ["context", "tokens", "memory", "performance"],
|
|
156
|
+
validate: validateInteger(1000, 1000000),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
path: "senses.cli",
|
|
160
|
+
tier: 2,
|
|
161
|
+
description: "CLI sense configuration. Controls whether the CLI interface is enabled.",
|
|
162
|
+
default: { enabled: true },
|
|
163
|
+
effects: "Enables or disables the CLI (terminal) interaction channel.",
|
|
164
|
+
topics: ["senses", "cli", "channels", "interface"],
|
|
165
|
+
validate: validateObject({ enabled: validateBoolean }),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
path: "senses.teams",
|
|
169
|
+
tier: 2,
|
|
170
|
+
description: "Teams sense configuration. Controls whether the Teams interface is enabled.",
|
|
171
|
+
default: { enabled: false },
|
|
172
|
+
effects: "Enables or disables the Microsoft Teams interaction channel.",
|
|
173
|
+
topics: ["senses", "teams", "channels", "interface"],
|
|
174
|
+
validate: validateObject({ enabled: validateBoolean }),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: "senses.bluebubbles",
|
|
178
|
+
tier: 2,
|
|
179
|
+
description: "BlueBubbles sense configuration. Controls whether the iMessage interface is enabled.",
|
|
180
|
+
default: { enabled: false },
|
|
181
|
+
effects: "Enables or disables the BlueBubbles (iMessage) interaction channel.",
|
|
182
|
+
topics: ["senses", "bluebubbles", "imessage", "channels", "interface"],
|
|
183
|
+
validate: validateObject({ enabled: validateBoolean }),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
path: "sync.enabled",
|
|
187
|
+
tier: 2,
|
|
188
|
+
description: "Whether git-based bundle sync is enabled.",
|
|
189
|
+
default: false,
|
|
190
|
+
effects: "Enables automatic synchronization of agent state via git. Requires sync.remote to be configured.",
|
|
191
|
+
topics: ["sync", "git", "state", "backup"],
|
|
192
|
+
validate: validateBoolean,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
path: "sync.remote",
|
|
196
|
+
tier: 2,
|
|
197
|
+
description: "Git remote name used for bundle sync.",
|
|
198
|
+
default: "origin",
|
|
199
|
+
effects: "Controls which git remote is used when sync is enabled.",
|
|
200
|
+
topics: ["sync", "git", "remote"],
|
|
201
|
+
validate: validateString,
|
|
202
|
+
},
|
|
203
|
+
// --- Tier 1: Self-service ---
|
|
204
|
+
{
|
|
205
|
+
path: "context.contextMargin",
|
|
206
|
+
tier: 1,
|
|
207
|
+
description: "Percentage of context window reserved as margin before compaction triggers.",
|
|
208
|
+
default: 20,
|
|
209
|
+
effects: "Higher values trigger compaction earlier, preserving more headroom. Lower values use more context.",
|
|
210
|
+
topics: ["context", "compaction", "memory", "performance"],
|
|
211
|
+
validate: validateInteger(0, 100),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
path: "phrases.thinking",
|
|
215
|
+
tier: 1,
|
|
216
|
+
description: "Array of phrases displayed while the agent is thinking.",
|
|
217
|
+
default: ["working"],
|
|
218
|
+
effects: "Changes the thinking indicator text shown to users. Purely cosmetic.",
|
|
219
|
+
topics: ["phrases", "ux", "display", "personality"],
|
|
220
|
+
validate: validateStringArray,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
path: "phrases.tool",
|
|
224
|
+
tier: 1,
|
|
225
|
+
description: "Array of phrases displayed while the agent is running a tool.",
|
|
226
|
+
default: ["running tool"],
|
|
227
|
+
effects: "Changes the tool-use indicator text shown to users. Purely cosmetic.",
|
|
228
|
+
topics: ["phrases", "ux", "display", "personality"],
|
|
229
|
+
validate: validateStringArray,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
path: "phrases.followup",
|
|
233
|
+
tier: 1,
|
|
234
|
+
description: "Array of phrases displayed during follow-up processing.",
|
|
235
|
+
default: ["processing"],
|
|
236
|
+
effects: "Changes the follow-up indicator text shown to users. Purely cosmetic.",
|
|
237
|
+
topics: ["phrases", "ux", "display", "personality"],
|
|
238
|
+
validate: validateStringArray,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
path: "shell.defaultTimeout",
|
|
242
|
+
tier: 1,
|
|
243
|
+
description: "Default timeout in milliseconds for shell command execution.",
|
|
244
|
+
default: undefined,
|
|
245
|
+
effects: "Controls how long shell commands run before timing out. Undefined uses system default.",
|
|
246
|
+
topics: ["shell", "timeout", "execution", "tools"],
|
|
247
|
+
validate: validateInteger(1000, 600000),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
path: "logging.level",
|
|
251
|
+
tier: 1,
|
|
252
|
+
description: "Minimum log level: debug, info, warn, or error.",
|
|
253
|
+
default: undefined,
|
|
254
|
+
effects: "Controls verbosity of runtime logging. Lower levels produce more output.",
|
|
255
|
+
topics: ["logging", "debug", "diagnostics"],
|
|
256
|
+
validate: validateStringEnum(["debug", "info", "warn", "error"]),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
path: "logging.sinks",
|
|
260
|
+
tier: 1,
|
|
261
|
+
description: "Array of log sink types: 'terminal' and/or 'ndjson'.",
|
|
262
|
+
default: undefined,
|
|
263
|
+
effects: "Controls where log output is directed. Terminal shows in console, ndjson writes structured logs.",
|
|
264
|
+
topics: ["logging", "output", "diagnostics"],
|
|
265
|
+
validate: validateStringEnumArray(["terminal", "ndjson"]),
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
exports.CONFIG_REGISTRY = new Map(registryData.map((entry) => [entry.path, entry]));
|
|
269
|
+
function getRegistryEntries() {
|
|
270
|
+
(0, runtime_1.emitNervesEvent)({
|
|
271
|
+
component: "heart",
|
|
272
|
+
event: "config_registry.access",
|
|
273
|
+
message: "listing all registry entries",
|
|
274
|
+
meta: { count: exports.CONFIG_REGISTRY.size },
|
|
275
|
+
});
|
|
276
|
+
return [...exports.CONFIG_REGISTRY.values()];
|
|
277
|
+
}
|
|
278
|
+
function getRegistryEntriesByTier(tier) {
|
|
279
|
+
(0, runtime_1.emitNervesEvent)({
|
|
280
|
+
component: "heart",
|
|
281
|
+
event: "config_registry.access",
|
|
282
|
+
message: `filtering registry entries by tier ${tier}`,
|
|
283
|
+
meta: { tier },
|
|
284
|
+
});
|
|
285
|
+
return [...exports.CONFIG_REGISTRY.values()].filter((entry) => entry.tier === tier);
|
|
286
|
+
}
|
|
287
|
+
function getRegistryEntriesByTopic(topic) {
|
|
288
|
+
const needle = topic.toLowerCase();
|
|
289
|
+
(0, runtime_1.emitNervesEvent)({
|
|
290
|
+
component: "heart",
|
|
291
|
+
event: "config_registry.access",
|
|
292
|
+
message: `filtering registry entries by topic "${topic}"`,
|
|
293
|
+
meta: { topic },
|
|
294
|
+
});
|
|
295
|
+
return [...exports.CONFIG_REGISTRY.values()].filter((entry) => entry.topics.some((t) => t.toLowerCase().includes(needle)));
|
|
296
|
+
}
|
|
297
|
+
function getRegistryEntry(path) {
|
|
298
|
+
(0, runtime_1.emitNervesEvent)({
|
|
299
|
+
component: "heart",
|
|
300
|
+
event: "config_registry.access",
|
|
301
|
+
message: `looking up registry entry for "${path}"`,
|
|
302
|
+
meta: { path },
|
|
303
|
+
});
|
|
304
|
+
return exports.CONFIG_REGISTRY.get(path);
|
|
305
|
+
}
|
package/dist/heart/core.js
CHANGED
|
@@ -924,8 +924,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
924
924
|
providerRuntime.appendToolOutput(tc.id, rejection);
|
|
925
925
|
continue;
|
|
926
926
|
}
|
|
927
|
-
// Confirmation check for mutate tools
|
|
928
|
-
|
|
927
|
+
// Confirmation check for mutate tools.
|
|
928
|
+
// confirmationAlwaysRequired tools cannot be bypassed by skipConfirmation.
|
|
929
|
+
const needsConfirmation = (0, tools_1.isConfirmationRequired)(tc.name) && (!options?.skipConfirmation || (0, tools_1.isConfirmationAlwaysRequired)(tc.name));
|
|
930
|
+
if (needsConfirmation) {
|
|
929
931
|
let decision = "denied";
|
|
930
932
|
if (callbacks.onConfirmAction) {
|
|
931
933
|
decision = await callbacks.onConfirmAction(tc.name, args);
|
|
@@ -1,8 +1,44 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildCapabilitiesSection = buildCapabilitiesSection;
|
|
3
37
|
exports.buildStartOfTurnPacket = buildStartOfTurnPacket;
|
|
4
38
|
exports.renderStartOfTurnPacket = renderStartOfTurnPacket;
|
|
5
39
|
exports.renderCompactStartOfTurnPacket = renderCompactStartOfTurnPacket;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
6
42
|
const runtime_1 = require("../nerves/runtime");
|
|
7
43
|
const tempo_1 = require("./tempo");
|
|
8
44
|
function estimateTokens(text) {
|
|
@@ -87,6 +123,64 @@ function buildResumeHint(view, obligations) {
|
|
|
87
123
|
return "";
|
|
88
124
|
return hints.join("; ");
|
|
89
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Build a compact summary of capability changes between runtime versions.
|
|
128
|
+
* Reads bundle-meta.json for version info and changelog.json for change descriptions.
|
|
129
|
+
* Returns undefined if no version change detected or files are missing/malformed.
|
|
130
|
+
*/
|
|
131
|
+
function buildCapabilitiesSection(bundleRoot) {
|
|
132
|
+
let meta;
|
|
133
|
+
try {
|
|
134
|
+
const raw = fs.readFileSync(path.join(bundleRoot, "bundle-meta.json"), "utf-8");
|
|
135
|
+
meta = JSON.parse(raw);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
(0, runtime_1.emitNervesEvent)({
|
|
139
|
+
component: "heart",
|
|
140
|
+
event: "heart.capabilities_check",
|
|
141
|
+
message: "bundle-meta.json missing or malformed",
|
|
142
|
+
meta: { bundleRoot },
|
|
143
|
+
});
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const currentVersion = meta.runtimeVersion;
|
|
147
|
+
const previousVersion = meta.previousRuntimeVersion;
|
|
148
|
+
if (typeof currentVersion !== "string" || typeof previousVersion !== "string") {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (currentVersion === previousVersion) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
(0, runtime_1.emitNervesEvent)({
|
|
155
|
+
component: "heart",
|
|
156
|
+
event: "heart.capabilities_version_change",
|
|
157
|
+
message: `runtime version changed from ${previousVersion} to ${currentVersion}`,
|
|
158
|
+
meta: { previousVersion, currentVersion },
|
|
159
|
+
});
|
|
160
|
+
let changelog;
|
|
161
|
+
try {
|
|
162
|
+
const raw = fs.readFileSync(path.join(bundleRoot, "changelog.json"), "utf-8");
|
|
163
|
+
changelog = JSON.parse(raw);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// changelog missing -- still report the version change
|
|
167
|
+
}
|
|
168
|
+
const parts = [`Updated from ${previousVersion} to ${currentVersion}`];
|
|
169
|
+
if (changelog && Array.isArray(changelog)) {
|
|
170
|
+
const relevantChanges = [];
|
|
171
|
+
for (const entry of changelog) {
|
|
172
|
+
if (entry.version === previousVersion)
|
|
173
|
+
break;
|
|
174
|
+
if (Array.isArray(entry.changes)) {
|
|
175
|
+
relevantChanges.push(...entry.changes);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (relevantChanges.length > 0) {
|
|
179
|
+
parts.push(`: ${relevantChanges.join("; ")}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return parts.join("");
|
|
183
|
+
}
|
|
90
184
|
function buildStartOfTurnPacket(view, opts) {
|
|
91
185
|
const tempo = view.tempo;
|
|
92
186
|
const tokenBudget = tempo_1.TEMPO_BUDGETS[tempo];
|
|
@@ -128,11 +222,12 @@ function renderStartOfTurnPacket(packet) {
|
|
|
128
222
|
// Assemble sections in priority order (highest priority first for budget allocation)
|
|
129
223
|
// Each section is { label, content, priority } where lower priority number = truncated first
|
|
130
224
|
const sections = [
|
|
131
|
-
{ label: "syncFailure", content: packet.syncFailure ?? "", priority:
|
|
132
|
-
{ label: "resume", content: packet.resumeHint, priority:
|
|
133
|
-
{ label: "obligations", content: packet.obligations, priority:
|
|
134
|
-
{ label: "cares", content: packet.cares, priority:
|
|
135
|
-
{ label: "plot", content: packet.plotLine, priority:
|
|
225
|
+
{ label: "syncFailure", content: packet.syncFailure ?? "", priority: 7 },
|
|
226
|
+
{ label: "resume", content: packet.resumeHint, priority: 6 },
|
|
227
|
+
{ label: "obligations", content: packet.obligations, priority: 5 },
|
|
228
|
+
{ label: "cares", content: packet.cares, priority: 4 },
|
|
229
|
+
{ label: "plot", content: packet.plotLine, priority: 3 },
|
|
230
|
+
{ label: "capabilities", content: packet.capabilities ?? "", priority: 2 },
|
|
136
231
|
{ label: "presence", content: packet.presence, priority: 1 },
|
|
137
232
|
].filter((s) => s.content.length > 0);
|
|
138
233
|
if (sections.length === 0) {
|
|
@@ -193,6 +288,9 @@ function formatSections(sections) {
|
|
|
193
288
|
case "presence":
|
|
194
289
|
parts.push(`**Peers:**\n${section.content}`);
|
|
195
290
|
break;
|
|
291
|
+
case "capabilities":
|
|
292
|
+
parts.push(`**Capabilities:** ${section.content}`);
|
|
293
|
+
break;
|
|
196
294
|
case "syncFailure":
|
|
197
295
|
parts.push(`**Sync warning:** ${section.content}`);
|
|
198
296
|
break;
|
|
@@ -47,6 +47,7 @@ const file_state_1 = require("../mind/file-state");
|
|
|
47
47
|
const scrutiny_1 = require("../mind/scrutiny");
|
|
48
48
|
const identity_1 = require("../heart/identity");
|
|
49
49
|
const identity_2 = require("../heart/identity");
|
|
50
|
+
const config_registry_1 = require("../heart/config-registry");
|
|
50
51
|
const socket_client_1 = require("../heart/daemon/socket-client");
|
|
51
52
|
const thoughts_1 = require("../heart/daemon/thoughts");
|
|
52
53
|
const manager_1 = require("../heart/bridges/manager");
|
|
@@ -1738,6 +1739,230 @@ exports.baseToolDefinitions = [
|
|
|
1738
1739
|
return JSON.stringify(result, null, 2);
|
|
1739
1740
|
},
|
|
1740
1741
|
},
|
|
1742
|
+
// --- Config discovery tools ---
|
|
1743
|
+
{
|
|
1744
|
+
tool: {
|
|
1745
|
+
type: "function",
|
|
1746
|
+
function: {
|
|
1747
|
+
name: "read_config",
|
|
1748
|
+
description: "Read current agent configuration with tier annotations, descriptions, defaults, and effects. Optionally filter by topic to see only related settings.",
|
|
1749
|
+
parameters: {
|
|
1750
|
+
type: "object",
|
|
1751
|
+
properties: {
|
|
1752
|
+
related_to: {
|
|
1753
|
+
type: "string",
|
|
1754
|
+
description: "Optional topic to filter results (e.g., 'model', 'logging', 'senses'). Case-insensitive partial match.",
|
|
1755
|
+
},
|
|
1756
|
+
},
|
|
1757
|
+
},
|
|
1758
|
+
},
|
|
1759
|
+
},
|
|
1760
|
+
handler: (a) => {
|
|
1761
|
+
const entries = a.related_to
|
|
1762
|
+
? (0, config_registry_1.getRegistryEntriesByTopic)(a.related_to)
|
|
1763
|
+
: (0, config_registry_1.getRegistryEntries)();
|
|
1764
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
1765
|
+
const configPath = path.join(agentRoot, "agent.json");
|
|
1766
|
+
let rawConfig = {};
|
|
1767
|
+
try {
|
|
1768
|
+
rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1769
|
+
}
|
|
1770
|
+
catch {
|
|
1771
|
+
/* v8 ignore next -- defensive: agent.json read failure in read_config @preserve */
|
|
1772
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.read_config_error", message: "failed to read agent.json", meta: { path: configPath } });
|
|
1773
|
+
}
|
|
1774
|
+
const result = entries.map((entry) => {
|
|
1775
|
+
const parts = entry.path.split(".");
|
|
1776
|
+
let current = rawConfig;
|
|
1777
|
+
for (const part of parts) {
|
|
1778
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1779
|
+
current = current[part];
|
|
1780
|
+
}
|
|
1781
|
+
else {
|
|
1782
|
+
current = undefined;
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const isExplicit = current !== undefined;
|
|
1787
|
+
let source;
|
|
1788
|
+
let currentValue;
|
|
1789
|
+
if (isExplicit) {
|
|
1790
|
+
source = "explicit";
|
|
1791
|
+
currentValue = current;
|
|
1792
|
+
}
|
|
1793
|
+
else if (entry.default !== undefined) {
|
|
1794
|
+
source = "default";
|
|
1795
|
+
currentValue = entry.default;
|
|
1796
|
+
}
|
|
1797
|
+
else {
|
|
1798
|
+
source = "unset";
|
|
1799
|
+
currentValue = null;
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
path: entry.path,
|
|
1803
|
+
currentValue,
|
|
1804
|
+
source,
|
|
1805
|
+
tier: entry.tier,
|
|
1806
|
+
description: entry.description,
|
|
1807
|
+
default: entry.default !== undefined ? entry.default : null,
|
|
1808
|
+
effects: entry.effects,
|
|
1809
|
+
topics: entry.topics,
|
|
1810
|
+
};
|
|
1811
|
+
});
|
|
1812
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.read_config", message: `read_config returned ${result.length} entries`, meta: { count: result.length, topic: a.related_to ?? null } });
|
|
1813
|
+
return JSON.stringify({ entries: result }, null, 2);
|
|
1814
|
+
},
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
tool: {
|
|
1818
|
+
type: "function",
|
|
1819
|
+
function: {
|
|
1820
|
+
name: "update_config",
|
|
1821
|
+
description: "Update a Tier 1 (self-service) agent configuration value immediately. For Tier 2 keys that require operator approval, use propose_config instead. Tier 3 (operator-only) keys are refused.",
|
|
1822
|
+
parameters: {
|
|
1823
|
+
type: "object",
|
|
1824
|
+
properties: {
|
|
1825
|
+
path: { type: "string", description: "Config key in dot-notation (e.g., 'context.contextMargin', 'logging.level')" },
|
|
1826
|
+
value: { type: "string", description: "New value as JSON (e.g., '25', '\"debug\"', '[\"terminal\", \"ndjson\"]')" },
|
|
1827
|
+
},
|
|
1828
|
+
required: ["path", "value"],
|
|
1829
|
+
},
|
|
1830
|
+
},
|
|
1831
|
+
},
|
|
1832
|
+
handler: (a) => {
|
|
1833
|
+
const entry = (0, config_registry_1.getRegistryEntry)(a.path);
|
|
1834
|
+
if (!entry) {
|
|
1835
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_error", message: `unknown config path: ${a.path}`, meta: { path: a.path } });
|
|
1836
|
+
return `Error: unknown config path "${a.path}". Use read_config to see available paths.`;
|
|
1837
|
+
}
|
|
1838
|
+
let parsedValue;
|
|
1839
|
+
try {
|
|
1840
|
+
parsedValue = JSON.parse(a.value);
|
|
1841
|
+
}
|
|
1842
|
+
catch {
|
|
1843
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_error", message: `invalid JSON value for ${a.path}`, meta: { path: a.path, value: a.value } });
|
|
1844
|
+
return `Error: invalid JSON value. Provide value as valid JSON (e.g., 25, "debug", ["terminal"]).`;
|
|
1845
|
+
}
|
|
1846
|
+
// Validate value against registry entry's validator
|
|
1847
|
+
/* v8 ignore next -- all current entries have validators; guard kept for future entries without one @preserve */
|
|
1848
|
+
if (entry.validate) {
|
|
1849
|
+
const validationError = entry.validate(parsedValue);
|
|
1850
|
+
if (validationError) {
|
|
1851
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_error", message: `validation failed for ${a.path}: ${validationError}`, meta: { path: a.path, value: a.value, validationError } });
|
|
1852
|
+
return `Error: validation failed for "${a.path}": ${validationError}`;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
// Tier 3: refuse
|
|
1856
|
+
if (entry.tier === 3) {
|
|
1857
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_refused", message: `refused T3 change to ${a.path}`, meta: { path: a.path, tier: 3 } });
|
|
1858
|
+
return `Refused: "${a.path}" is an operator-only (Tier 3) key. ${entry.description} Only the operator can change this value directly in agent.json.`;
|
|
1859
|
+
}
|
|
1860
|
+
// Tier 2: refuse — must use propose_config
|
|
1861
|
+
if (entry.tier === 2) {
|
|
1862
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_refused", message: `refused T2 change to ${a.path} — use propose_config`, meta: { path: a.path, tier: 2 } });
|
|
1863
|
+
return `Refused: "${a.path}" is a Tier 2 (proposal) key that requires operator approval. Use propose_config instead of update_config to propose this change.`;
|
|
1864
|
+
}
|
|
1865
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
1866
|
+
const configPath = path.join(agentRoot, "agent.json");
|
|
1867
|
+
let rawConfig;
|
|
1868
|
+
try {
|
|
1869
|
+
rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1870
|
+
}
|
|
1871
|
+
catch {
|
|
1872
|
+
/* v8 ignore next -- defensive: agent.json read failure in update_config @preserve */
|
|
1873
|
+
return `Error: failed to read agent.json at ${configPath}`;
|
|
1874
|
+
}
|
|
1875
|
+
// Apply T1 change immediately
|
|
1876
|
+
const parts = entry.path.split(".");
|
|
1877
|
+
let target = rawConfig;
|
|
1878
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1879
|
+
if (!(parts[i] in target) || typeof target[parts[i]] !== "object" || target[parts[i]] === null) {
|
|
1880
|
+
target[parts[i]] = {};
|
|
1881
|
+
}
|
|
1882
|
+
target = target[parts[i]];
|
|
1883
|
+
}
|
|
1884
|
+
target[parts[parts.length - 1]] = parsedValue;
|
|
1885
|
+
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8");
|
|
1886
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_applied", message: `applied config change to ${a.path}`, meta: { path: a.path, tier: entry.tier, value: parsedValue } });
|
|
1887
|
+
return `Success: "${a.path}" updated to ${JSON.stringify(parsedValue)}. Change applied immediately.`;
|
|
1888
|
+
},
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
tool: {
|
|
1892
|
+
type: "function",
|
|
1893
|
+
function: {
|
|
1894
|
+
name: "propose_config",
|
|
1895
|
+
description: "Propose a change to a Tier 2 (proposal) agent configuration value. Requires operator approval before the change is applied. For Tier 1 self-service keys, use update_config instead.",
|
|
1896
|
+
parameters: {
|
|
1897
|
+
type: "object",
|
|
1898
|
+
properties: {
|
|
1899
|
+
path: { type: "string", description: "Config key in dot-notation (e.g., 'context.maxTokens', 'humanFacing.model')" },
|
|
1900
|
+
value: { type: "string", description: "New value as JSON (e.g., '120000', '\"azure\"', '{\"enabled\": true}')" },
|
|
1901
|
+
},
|
|
1902
|
+
required: ["path", "value"],
|
|
1903
|
+
},
|
|
1904
|
+
},
|
|
1905
|
+
},
|
|
1906
|
+
confirmationRequired: true,
|
|
1907
|
+
confirmationAlwaysRequired: true,
|
|
1908
|
+
handler: (a) => {
|
|
1909
|
+
const entry = (0, config_registry_1.getRegistryEntry)(a.path);
|
|
1910
|
+
if (!entry) {
|
|
1911
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `unknown config path: ${a.path}`, meta: { path: a.path } });
|
|
1912
|
+
return `Error: unknown config path "${a.path}". Use read_config to see available paths.`;
|
|
1913
|
+
}
|
|
1914
|
+
let parsedValue;
|
|
1915
|
+
try {
|
|
1916
|
+
parsedValue = JSON.parse(a.value);
|
|
1917
|
+
}
|
|
1918
|
+
catch {
|
|
1919
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `invalid JSON value for ${a.path}`, meta: { path: a.path, value: a.value } });
|
|
1920
|
+
return `Error: invalid JSON value. Provide value as valid JSON (e.g., 120000, "azure", {"enabled": true}).`;
|
|
1921
|
+
}
|
|
1922
|
+
// Validate value against registry entry's validator
|
|
1923
|
+
/* v8 ignore next -- all current entries have validators; guard kept for future entries without one @preserve */
|
|
1924
|
+
if (entry.validate) {
|
|
1925
|
+
const validationError = entry.validate(parsedValue);
|
|
1926
|
+
if (validationError) {
|
|
1927
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `validation failed for ${a.path}: ${validationError}`, meta: { path: a.path, value: a.value, validationError } });
|
|
1928
|
+
return `Error: validation failed for "${a.path}": ${validationError}`;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
// Tier 3: refuse
|
|
1932
|
+
if (entry.tier === 3) {
|
|
1933
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_refused", message: `refused T3 change to ${a.path}`, meta: { path: a.path, tier: 3 } });
|
|
1934
|
+
return `Refused: "${a.path}" is an operator-only (Tier 3) key. ${entry.description} Only the operator can change this value directly in agent.json.`;
|
|
1935
|
+
}
|
|
1936
|
+
// Tier 1: refuse — use update_config for self-service keys
|
|
1937
|
+
if (entry.tier === 1) {
|
|
1938
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_refused", message: `refused T1 change to ${a.path} — use update_config`, meta: { path: a.path, tier: 1 } });
|
|
1939
|
+
return `Refused: "${a.path}" is a Tier 1 (self-service) key. Use update_config instead — no operator approval needed.`;
|
|
1940
|
+
}
|
|
1941
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
1942
|
+
const configPath = path.join(agentRoot, "agent.json");
|
|
1943
|
+
let rawConfig;
|
|
1944
|
+
try {
|
|
1945
|
+
rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1946
|
+
}
|
|
1947
|
+
catch {
|
|
1948
|
+
/* v8 ignore next -- defensive: agent.json read failure in propose_config @preserve */
|
|
1949
|
+
return `Error: failed to read agent.json at ${configPath}`;
|
|
1950
|
+
}
|
|
1951
|
+
// Apply T2 change (operator confirmation already obtained via confirmationAlwaysRequired)
|
|
1952
|
+
const parts = entry.path.split(".");
|
|
1953
|
+
let target = rawConfig;
|
|
1954
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1955
|
+
if (!(parts[i] in target) || typeof target[parts[i]] !== "object" || target[parts[i]] === null) {
|
|
1956
|
+
target[parts[i]] = {};
|
|
1957
|
+
}
|
|
1958
|
+
target = target[parts[i]];
|
|
1959
|
+
}
|
|
1960
|
+
target[parts[parts.length - 1]] = parsedValue;
|
|
1961
|
+
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8");
|
|
1962
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_applied", message: `applied approved config change to ${a.path}`, meta: { path: a.path, tier: entry.tier, value: parsedValue } });
|
|
1963
|
+
return `Success: "${a.path}" updated to ${JSON.stringify(parsedValue)}. Change approved and applied.`;
|
|
1964
|
+
},
|
|
1965
|
+
},
|
|
1741
1966
|
...tools_1.codingToolDefinitions,
|
|
1742
1967
|
];
|
|
1743
1968
|
exports.tools = exports.baseToolDefinitions.map((d) => d.tool);
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.surfaceToolDef = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
|
|
37
37
|
exports.getToolsForChannel = getToolsForChannel;
|
|
38
38
|
exports.isConfirmationRequired = isConfirmationRequired;
|
|
39
|
+
exports.isConfirmationAlwaysRequired = isConfirmationAlwaysRequired;
|
|
39
40
|
exports.execTool = execTool;
|
|
40
41
|
exports.summarizeArgs = summarizeArgs;
|
|
41
42
|
exports.buildToolResultSummary = buildToolResultSummary;
|
|
@@ -325,6 +326,12 @@ function isConfirmationRequired(toolName) {
|
|
|
325
326
|
const def = allDefinitions.find((d) => d.tool.function.name === toolName);
|
|
326
327
|
return def?.confirmationRequired === true;
|
|
327
328
|
}
|
|
329
|
+
// Check whether a tool's confirmation is non-bypassable (cannot be skipped by skipConfirmation).
|
|
330
|
+
// Used for T2 config proposals that must always require operator approval.
|
|
331
|
+
function isConfirmationAlwaysRequired(toolName) {
|
|
332
|
+
const def = allDefinitions.find((d) => d.tool.function.name === toolName);
|
|
333
|
+
return def?.confirmationAlwaysRequired === true;
|
|
334
|
+
}
|
|
328
335
|
function normalizeGuardArgs(_name, args) {
|
|
329
336
|
return args;
|
|
330
337
|
}
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -390,6 +390,10 @@ async function handleInboundTurn(input) {
|
|
|
390
390
|
if (syncFailure) {
|
|
391
391
|
startOfTurnPacket.syncFailure = syncFailure;
|
|
392
392
|
}
|
|
393
|
+
const capabilities = (0, start_of_turn_packet_1.buildCapabilitiesSection)(agentRoot);
|
|
394
|
+
if (capabilities) {
|
|
395
|
+
startOfTurnPacket.capabilities = capabilities;
|
|
396
|
+
}
|
|
393
397
|
renderedStartOfTurnPacket = (0, start_of_turn_packet_1.renderStartOfTurnPacket)(startOfTurnPacket);
|
|
394
398
|
if (!renderedStartOfTurnPacket)
|
|
395
399
|
renderedStartOfTurnPacket = undefined;
|