@ouro.bot/cli 0.1.0-alpha.11 → 0.1.0-alpha.13

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.
@@ -34,99 +34,201 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getSpecialistTools = getSpecialistTools;
37
- exports.execSpecialistTool = execSpecialistTool;
37
+ exports.createSpecialistExecTool = createSpecialistExecTool;
38
38
  const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
39
40
  const tools_base_1 = require("../../repertoire/tools-base");
40
41
  const hatch_flow_1 = require("./hatch-flow");
41
42
  const hatch_animation_1 = require("./hatch-animation");
43
+ const bundle_manifest_1 = require("../../mind/bundle-manifest");
42
44
  const runtime_1 = require("../../nerves/runtime");
43
- const hatchAgentTool = {
45
+ const completeAdoptionTool = {
44
46
  type: "function",
45
47
  function: {
46
- name: "hatch_agent",
47
- description: "create a new agent bundle with the given name. call this when you have gathered enough information from the human to hatch their agent.",
48
+ name: "complete_adoption",
49
+ description: "finalize the agent bundle and hatch the new agent. call this only when you have written all 5 psyche files and agent.json to the temp directory, and the human has approved the bundle.",
48
50
  parameters: {
49
51
  type: "object",
50
52
  properties: {
51
53
  name: {
52
54
  type: "string",
53
- description: "the name for the new agent (PascalCase, e.g. 'Slugger')",
55
+ description: "the PascalCase name for the new agent (e.g. 'Slugger')",
54
56
  },
55
- humanName: {
57
+ handoff_message: {
56
58
  type: "string",
57
- description: "the human's preferred name, as they told you during conversation",
59
+ description: "a warm handoff message to display to the human after the agent is hatched",
58
60
  },
59
61
  },
60
- required: ["name", "humanName"],
62
+ required: ["name", "handoff_message"],
61
63
  },
62
64
  },
63
65
  };
64
66
  const readFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "read_file");
67
+ const writeFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "write_file");
65
68
  const listDirTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "list_directory");
66
69
  /**
67
70
  * Returns the specialist's tool schema array.
68
71
  */
69
72
  function getSpecialistTools() {
70
- return [hatchAgentTool, tools_base_1.finalAnswerTool, readFileTool.tool, listDirTool.tool];
73
+ return [completeAdoptionTool, tools_base_1.finalAnswerTool, readFileTool.tool, writeFileTool.tool, listDirTool.tool];
74
+ }
75
+ const PSYCHE_FILES = ["SOUL.md", "IDENTITY.md", "LORE.md", "TACIT.md", "ASPIRATIONS.md"];
76
+ function isPascalCase(name) {
77
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name);
78
+ }
79
+ function writeReadme(dir, purpose) {
80
+ fs.mkdirSync(dir, { recursive: true });
81
+ const readmePath = path.join(dir, "README.md");
82
+ /* v8 ignore next -- defensive: guard against re-scaffold on existing bundle @preserve */
83
+ if (!fs.existsSync(readmePath)) {
84
+ fs.writeFileSync(readmePath, `# ${path.basename(dir)}\n\n${purpose}\n`, "utf-8");
85
+ }
86
+ }
87
+ function scaffoldBundle(bundleRoot) {
88
+ writeReadme(path.join(bundleRoot, "memory"), "Persistent memory store.");
89
+ writeReadme(path.join(bundleRoot, "memory", "daily"), "Daily memory entries.");
90
+ writeReadme(path.join(bundleRoot, "memory", "archive"), "Archived memory.");
91
+ writeReadme(path.join(bundleRoot, "friends"), "Known friend records.");
92
+ writeReadme(path.join(bundleRoot, "tasks"), "Task files.");
93
+ writeReadme(path.join(bundleRoot, "tasks", "habits"), "Recurring tasks.");
94
+ writeReadme(path.join(bundleRoot, "tasks", "one-shots"), "One-shot tasks.");
95
+ writeReadme(path.join(bundleRoot, "tasks", "ongoing"), "Ongoing tasks.");
96
+ writeReadme(path.join(bundleRoot, "skills"), "Local skill files.");
97
+ writeReadme(path.join(bundleRoot, "senses"), "Sense-specific config.");
98
+ writeReadme(path.join(bundleRoot, "senses", "teams"), "Teams sense config.");
99
+ // Memory scaffold files
100
+ const memoryRoot = path.join(bundleRoot, "memory");
101
+ const factsPath = path.join(memoryRoot, "facts.jsonl");
102
+ const entitiesPath = path.join(memoryRoot, "entities.json");
103
+ /* v8 ignore next -- defensive: guard against re-scaffold on existing bundle @preserve */
104
+ if (!fs.existsSync(factsPath))
105
+ fs.writeFileSync(factsPath, "", "utf-8");
106
+ /* v8 ignore next -- defensive: guard against re-scaffold on existing bundle @preserve */
107
+ if (!fs.existsSync(entitiesPath))
108
+ fs.writeFileSync(entitiesPath, "{}\n", "utf-8");
109
+ // bundle-meta.json
110
+ const meta = (0, bundle_manifest_1.createBundleMeta)();
111
+ fs.writeFileSync(path.join(bundleRoot, "bundle-meta.json"), JSON.stringify(meta, null, 2) + "\n", "utf-8");
112
+ }
113
+ function moveDir(src, dest) {
114
+ try {
115
+ fs.renameSync(src, dest);
116
+ }
117
+ catch {
118
+ /* v8 ignore start -- cross-device fallback: only triggers on EXDEV (e.g. /tmp → different mount), untestable in CI @preserve */
119
+ fs.cpSync(src, dest, { recursive: true });
120
+ fs.rmSync(src, { recursive: true, force: true });
121
+ /* v8 ignore stop */
122
+ }
123
+ }
124
+ async function execCompleteAdoption(args, deps) {
125
+ const name = args.name;
126
+ const handoffMessage = args.handoff_message;
127
+ if (!name) {
128
+ return "error: missing required 'name' parameter";
129
+ }
130
+ if (!isPascalCase(name)) {
131
+ return `error: name '${name}' must be PascalCase (e.g. 'Slugger', 'MyAgent')`;
132
+ }
133
+ // Validate psyche files exist
134
+ const psycheDir = path.join(deps.tempDir, "psyche");
135
+ const missingPsyche = PSYCHE_FILES.filter((f) => !fs.existsSync(path.join(psycheDir, f)));
136
+ if (missingPsyche.length > 0) {
137
+ return `error: missing psyche files in temp directory: ${missingPsyche.join(", ")}. write them first using write_file.`;
138
+ }
139
+ // Validate agent.json exists
140
+ const agentJsonPath = path.join(deps.tempDir, "agent.json");
141
+ if (!fs.existsSync(agentJsonPath)) {
142
+ return "error: agent.json not found in temp directory. write it first using write_file.";
143
+ }
144
+ // Validate target doesn't exist
145
+ const targetBundle = path.join(deps.bundlesRoot, `${name}.ouro`);
146
+ if (fs.existsSync(targetBundle)) {
147
+ return `error: bundle '${name}.ouro' already exists at ${deps.bundlesRoot}. choose a different name.`;
148
+ }
149
+ // Scaffold structural dirs into tempDir
150
+ scaffoldBundle(deps.tempDir);
151
+ // Move tempDir -> final bundle location
152
+ moveDir(deps.tempDir, targetBundle);
153
+ // Write secrets
154
+ try {
155
+ (0, hatch_flow_1.writeSecretsFile)(name, deps.provider, deps.credentials, deps.secretsRoot);
156
+ }
157
+ catch (e) {
158
+ // Rollback: remove the moved bundle
159
+ try {
160
+ fs.rmSync(targetBundle, { recursive: true, force: true });
161
+ }
162
+ catch {
163
+ // Best effort cleanup
164
+ }
165
+ return `error: failed to write secrets: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
166
+ }
167
+ // Play hatch animation
168
+ await (0, hatch_animation_1.playHatchAnimation)(name, deps.animationWriter);
169
+ // Display handoff message
170
+ /* v8 ignore next -- UI-only: handoff message display, covered by integration @preserve */
171
+ if (handoffMessage && deps.animationWriter) {
172
+ deps.animationWriter(`\n${handoffMessage}\n`);
173
+ }
174
+ (0, runtime_1.emitNervesEvent)({
175
+ component: "daemon",
176
+ event: "daemon.adoption_complete",
177
+ message: "adoption completed successfully",
178
+ meta: { agentName: name, bundlePath: targetBundle },
179
+ });
180
+ return JSON.stringify({ success: true, agentName: name, bundlePath: targetBundle });
71
181
  }
72
182
  /**
73
- * Execute a specialist tool call.
74
- * Returns the tool result string.
183
+ * Create a specialist tool executor with the given dependencies captured in closure.
75
184
  */
76
- async function execSpecialistTool(name, args, deps) {
185
+ function createSpecialistExecTool(deps) {
77
186
  (0, runtime_1.emitNervesEvent)({
78
187
  component: "daemon",
79
- event: "daemon.specialist_tool_exec",
80
- message: "executing specialist tool",
81
- meta: { tool: name },
188
+ event: "daemon.specialist_exec_tool_created",
189
+ message: "specialist exec tool created",
190
+ meta: { tempDir: deps.tempDir },
82
191
  });
83
- if (name === "hatch_agent") {
84
- const agentName = args.name;
85
- if (!agentName) {
86
- return "error: missing required 'name' parameter for hatch_agent";
87
- }
88
- const input = {
89
- agentName,
90
- humanName: args.humanName || deps.humanName,
91
- provider: deps.provider,
92
- credentials: deps.credentials,
93
- };
94
- // Pass identity dirs to prevent hatch flow from syncing to ~/AgentBundles/AdoptionSpecialist.ouro/
95
- // or cwd/AdoptionSpecialist.ouro/. The specialist already picked its identity; the hatch flow
96
- // just needs a valid source dir to pick from for the hatchling's LORE.md seed.
97
- const identitiesDir = deps.specialistIdentitiesDir;
98
- const result = await (0, hatch_flow_1.runHatchFlow)(input, {
99
- bundlesRoot: deps.bundlesRoot,
100
- secretsRoot: deps.secretsRoot,
101
- ...(identitiesDir ? { specialistIdentitySourceDir: identitiesDir, specialistIdentityTargetDir: identitiesDir } : {}),
192
+ return async (name, args) => {
193
+ (0, runtime_1.emitNervesEvent)({
194
+ component: "daemon",
195
+ event: "daemon.specialist_tool_exec",
196
+ message: "executing specialist tool",
197
+ meta: { tool: name },
102
198
  });
103
- await (0, hatch_animation_1.playHatchAnimation)(agentName, deps.animationWriter);
104
- return [
105
- `hatched ${agentName} successfully.`,
106
- `bundle path: ${result.bundleRoot}`,
107
- `identity seed: ${result.selectedIdentity}`,
108
- `specialist secrets: ${result.specialistSecretsPath}`,
109
- `hatchling secrets: ${result.hatchlingSecretsPath}`,
110
- ].join("\n");
111
- }
112
- if (name === "read_file") {
113
- try {
114
- return fs.readFileSync(args.path, "utf-8");
199
+ if (name === "complete_adoption") {
200
+ return execCompleteAdoption(args, deps);
115
201
  }
116
- catch (e) {
117
- return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
202
+ if (name === "read_file") {
203
+ try {
204
+ return fs.readFileSync(args.path, "utf-8");
205
+ }
206
+ catch (e) {
207
+ return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
208
+ }
118
209
  }
119
- }
120
- if (name === "list_directory") {
121
- try {
122
- return fs
123
- .readdirSync(args.path, { withFileTypes: true })
124
- .map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
125
- .join("\n");
210
+ if (name === "write_file") {
211
+ try {
212
+ const dir = path.dirname(args.path);
213
+ fs.mkdirSync(dir, { recursive: true });
214
+ fs.writeFileSync(args.path, args.content, "utf-8");
215
+ return `wrote ${args.path}`;
216
+ }
217
+ catch (e) {
218
+ return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
219
+ }
126
220
  }
127
- catch (e) {
128
- return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
221
+ if (name === "list_directory") {
222
+ try {
223
+ return fs
224
+ .readdirSync(args.path, { withFileTypes: true })
225
+ .map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
226
+ .join("\n");
227
+ }
228
+ catch (e) {
229
+ return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
230
+ }
129
231
  }
130
- }
131
- return `error: unknown tool '${name}'`;
232
+ return `error: unknown tool '${name}'`;
233
+ };
132
234
  }
@@ -55,9 +55,18 @@ function listSubagentSources(subagentsDir) {
55
55
  .map((name) => path.join(subagentsDir, name))
56
56
  .sort((a, b) => a.localeCompare(b));
57
57
  }
58
+ function pathExists(target) {
59
+ try {
60
+ fs.lstatSync(target);
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
58
67
  function ensureSymlink(source, target) {
59
68
  fs.mkdirSync(path.dirname(target), { recursive: true });
60
- if (fs.existsSync(target)) {
69
+ if (pathExists(target)) {
61
70
  const stats = fs.lstatSync(target);
62
71
  if (stats.isSymbolicLink()) {
63
72
  const linkedPath = fs.readlinkSync(target);
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
36
+ exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
37
37
  exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
38
38
  exports.getAgentName = getAgentName;
39
39
  exports.getRepoRoot = getRepoRoot;
@@ -57,12 +57,73 @@ exports.DEFAULT_AGENT_PHRASES = {
57
57
  tool: ["running tool"],
58
58
  followup: ["processing"],
59
59
  };
60
+ exports.DEFAULT_AGENT_SENSES = {
61
+ cli: { enabled: true },
62
+ teams: { enabled: false },
63
+ bluebubbles: { enabled: false },
64
+ };
65
+ function normalizeSenses(value, configFile) {
66
+ const defaults = {
67
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
68
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
69
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
70
+ };
71
+ if (value === undefined) {
72
+ return defaults;
73
+ }
74
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
75
+ (0, runtime_1.emitNervesEvent)({
76
+ level: "error",
77
+ event: "config_identity.error",
78
+ component: "config/identity",
79
+ message: "agent config has invalid senses block",
80
+ meta: { path: configFile },
81
+ });
82
+ throw new Error(`agent.json at ${configFile} must include senses as an object when present.`);
83
+ }
84
+ const raw = value;
85
+ const senseNames = ["cli", "teams", "bluebubbles"];
86
+ for (const senseName of senseNames) {
87
+ const rawSense = raw[senseName];
88
+ if (rawSense === undefined) {
89
+ continue;
90
+ }
91
+ if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
92
+ (0, runtime_1.emitNervesEvent)({
93
+ level: "error",
94
+ event: "config_identity.error",
95
+ component: "config/identity",
96
+ message: "agent config has invalid sense config",
97
+ meta: { path: configFile, sense: senseName },
98
+ });
99
+ throw new Error(`agent.json at ${configFile} has invalid senses.${senseName} config.`);
100
+ }
101
+ const enabled = rawSense.enabled;
102
+ if (typeof enabled !== "boolean") {
103
+ (0, runtime_1.emitNervesEvent)({
104
+ level: "error",
105
+ event: "config_identity.error",
106
+ component: "config/identity",
107
+ message: "agent config has invalid sense enabled flag",
108
+ meta: { path: configFile, sense: senseName, enabled: enabled ?? null },
109
+ });
110
+ throw new Error(`agent.json at ${configFile} must include senses.${senseName}.enabled as boolean.`);
111
+ }
112
+ defaults[senseName] = { enabled };
113
+ }
114
+ return defaults;
115
+ }
60
116
  function buildDefaultAgentTemplate(_agentName) {
61
117
  return {
62
118
  version: 1,
63
119
  enabled: true,
64
120
  provider: "anthropic",
65
121
  context: { ...exports.DEFAULT_AGENT_CONTEXT },
122
+ senses: {
123
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
124
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
125
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
126
+ },
66
127
  phrases: {
67
128
  thinking: [...exports.DEFAULT_AGENT_PHRASES.thinking],
68
129
  tool: [...exports.DEFAULT_AGENT_PHRASES.tool],
@@ -257,6 +318,7 @@ function loadAgentConfig() {
257
318
  provider: rawProvider,
258
319
  context: parsed.context,
259
320
  logging: parsed.logging,
321
+ senses: normalizeSenses(parsed.senses, configFile),
260
322
  phrases: parsed.phrases,
261
323
  };
262
324
  (0, runtime_1.emitNervesEvent)({
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSenseInventory = getSenseInventory;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ const identity_1 = require("./identity");
6
+ const SENSES = [
7
+ { sense: "cli", label: "CLI", daemonManaged: false },
8
+ { sense: "teams", label: "Teams", daemonManaged: true },
9
+ { sense: "bluebubbles", label: "BlueBubbles", daemonManaged: true },
10
+ ];
11
+ function configuredSenses(senses) {
12
+ return senses ?? {
13
+ cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
14
+ teams: { ...identity_1.DEFAULT_AGENT_SENSES.teams },
15
+ bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
16
+ };
17
+ }
18
+ function resolveStatus(enabled, daemonManaged, runtimeInfo) {
19
+ if (!enabled) {
20
+ return "disabled";
21
+ }
22
+ if (!daemonManaged) {
23
+ return "interactive";
24
+ }
25
+ if (runtimeInfo?.runtime === "error") {
26
+ return "error";
27
+ }
28
+ if (runtimeInfo?.runtime === "running") {
29
+ return "running";
30
+ }
31
+ if (runtimeInfo?.configured === false) {
32
+ return "needs_config";
33
+ }
34
+ return "ready";
35
+ }
36
+ function getSenseInventory(agent, runtime = {}) {
37
+ const senses = configuredSenses(agent.senses);
38
+ const inventory = SENSES.map(({ sense, label, daemonManaged }) => {
39
+ const enabled = senses[sense].enabled;
40
+ return {
41
+ sense,
42
+ label,
43
+ enabled,
44
+ daemonManaged,
45
+ status: resolveStatus(enabled, daemonManaged, runtime[sense]),
46
+ };
47
+ });
48
+ (0, runtime_1.emitNervesEvent)({
49
+ component: "channels",
50
+ event: "channel.sense_inventory_built",
51
+ message: "built sense inventory",
52
+ meta: {
53
+ senses: inventory.map((entry) => ({
54
+ sense: entry.sense,
55
+ enabled: entry.enabled,
56
+ status: entry.status,
57
+ })),
58
+ },
59
+ });
60
+ return inventory;
61
+ }
@@ -34,6 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CANONICAL_BUNDLE_MANIFEST = void 0;
37
+ exports.getPackageVersion = getPackageVersion;
38
+ exports.createBundleMeta = createBundleMeta;
39
+ exports.backfillBundleMeta = backfillBundleMeta;
40
+ exports.resetBackfillTracking = resetBackfillTracking;
37
41
  exports.isCanonicalBundlePath = isCanonicalBundlePath;
38
42
  exports.findNonCanonicalBundlePaths = findNonCanonicalBundlePaths;
39
43
  const fs = __importStar(require("fs"));
@@ -41,6 +45,7 @@ const path = __importStar(require("path"));
41
45
  const runtime_1 = require("../nerves/runtime");
42
46
  exports.CANONICAL_BUNDLE_MANIFEST = [
43
47
  { path: "agent.json", kind: "file" },
48
+ { path: "bundle-meta.json", kind: "file" },
44
49
  { path: "psyche/SOUL.md", kind: "file" },
45
50
  { path: "psyche/IDENTITY.md", kind: "file" },
46
51
  { path: "psyche/LORE.md", kind: "file" },
@@ -53,6 +58,59 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
53
58
  { path: "senses", kind: "dir" },
54
59
  { path: "senses/teams", kind: "dir" },
55
60
  ];
61
+ function getPackageVersion() {
62
+ const packageJsonPath = path.resolve(__dirname, "../../package.json");
63
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
64
+ const parsed = JSON.parse(raw);
65
+ (0, runtime_1.emitNervesEvent)({
66
+ component: "mind",
67
+ event: "mind.package_version_read",
68
+ message: "read package version",
69
+ meta: { version: parsed.version },
70
+ });
71
+ return parsed.version;
72
+ }
73
+ function createBundleMeta() {
74
+ return {
75
+ runtimeVersion: getPackageVersion(),
76
+ bundleSchemaVersion: 1,
77
+ lastUpdated: new Date().toISOString(),
78
+ };
79
+ }
80
+ const _backfilledRoots = new Set();
81
+ /**
82
+ * If bundle-meta.json is missing from the agent root, create it with current runtime version.
83
+ * This backfills existing agent bundles that were created before bundle-meta.json was introduced.
84
+ * Only attempts once per bundleRoot per process.
85
+ */
86
+ function backfillBundleMeta(bundleRoot) {
87
+ if (_backfilledRoots.has(bundleRoot))
88
+ return;
89
+ _backfilledRoots.add(bundleRoot);
90
+ const metaPath = path.join(bundleRoot, "bundle-meta.json");
91
+ try {
92
+ if (fs.existsSync(metaPath)) {
93
+ return;
94
+ }
95
+ const meta = createBundleMeta();
96
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
97
+ (0, runtime_1.emitNervesEvent)({
98
+ component: "mind",
99
+ event: "mind.bundle_meta_backfill",
100
+ message: "backfilled missing bundle-meta.json",
101
+ meta: { bundleRoot },
102
+ });
103
+ }
104
+ catch {
105
+ // Non-blocking: if we can't write, that's okay
106
+ }
107
+ }
108
+ /**
109
+ * Reset the backfill tracking set. Used in tests.
110
+ */
111
+ function resetBackfillTracking() {
112
+ _backfilledRoots.clear();
113
+ }
56
114
  const CANONICAL_FILE_PATHS = new Set(exports.CANONICAL_BUNDLE_MANIFEST
57
115
  .filter((entry) => entry.kind === "file")
58
116
  .map((entry) => entry.path));
@@ -47,10 +47,12 @@ const identity_1 = require("../heart/identity");
47
47
  const os = __importStar(require("os"));
48
48
  const channel_1 = require("./friends/channel");
49
49
  const runtime_1 = require("../nerves/runtime");
50
+ const bundle_manifest_1 = require("./bundle-manifest");
50
51
  const first_impressions_1 = require("./first-impressions");
51
52
  const tasks_1 = require("../repertoire/tasks");
52
53
  // Lazy-loaded psyche text cache
53
54
  let _psycheCache = null;
55
+ let _senseStatusLinesCache = null;
54
56
  function loadPsycheFile(name) {
55
57
  try {
56
58
  const psycheDir = path.join((0, identity_1.getAgentRoot)(), "psyche");
@@ -74,6 +76,7 @@ function loadPsyche() {
74
76
  }
75
77
  function resetPsycheCache() {
76
78
  _psycheCache = null;
79
+ _senseStatusLinesCache = null;
77
80
  }
78
81
  const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
79
82
  function resolveFriendName(friendId, friendsDir, agentName) {
@@ -194,6 +197,7 @@ function runtimeInfoSection(channel) {
194
197
  lines.push(`agent: ${agentName}`);
195
198
  lines.push(`cwd: ${process.cwd()}`);
196
199
  lines.push(`channel: ${channel}`);
200
+ lines.push(`current sense: ${channel}`);
197
201
  lines.push(`i can read and modify my own source code.`);
198
202
  if (channel === "cli") {
199
203
  lines.push("i introduce myself on boot with a fun random greeting.");
@@ -204,8 +208,73 @@ function runtimeInfoSection(channel) {
204
208
  else {
205
209
  lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
206
210
  }
211
+ lines.push("");
212
+ lines.push(...senseRuntimeGuidance(channel));
207
213
  return lines.join("\n");
208
214
  }
215
+ function hasTextField(record, key) {
216
+ return typeof record?.[key] === "string" && record[key].trim().length > 0;
217
+ }
218
+ function localSenseStatusLines() {
219
+ if (_senseStatusLinesCache) {
220
+ return [..._senseStatusLinesCache];
221
+ }
222
+ const config = (0, identity_1.loadAgentConfig)();
223
+ const senses = config.senses ?? {
224
+ cli: { enabled: true },
225
+ teams: { enabled: false },
226
+ bluebubbles: { enabled: false },
227
+ };
228
+ let payload = {};
229
+ try {
230
+ const raw = fs.readFileSync((0, identity_1.getAgentSecretsPath)(), "utf-8");
231
+ const parsed = JSON.parse(raw);
232
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
233
+ payload = parsed;
234
+ }
235
+ }
236
+ catch {
237
+ payload = {};
238
+ }
239
+ const teams = payload.teams;
240
+ const bluebubbles = payload.bluebubbles;
241
+ const configured = {
242
+ cli: true,
243
+ teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
244
+ bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
245
+ };
246
+ const rows = [
247
+ { label: "CLI", status: "interactive" },
248
+ {
249
+ label: "Teams",
250
+ status: !senses.teams.enabled ? "disabled" : configured.teams ? "ready" : "needs_config",
251
+ },
252
+ {
253
+ label: "BlueBubbles",
254
+ status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "needs_config",
255
+ },
256
+ ];
257
+ _senseStatusLinesCache = rows.map((row) => `- ${row.label}: ${row.status}`);
258
+ return [..._senseStatusLinesCache];
259
+ }
260
+ function senseRuntimeGuidance(channel) {
261
+ const lines = ["available senses:"];
262
+ lines.push(...localSenseStatusLines());
263
+ lines.push("sense states:");
264
+ lines.push("- interactive = available when opened by the user instead of kept running by the daemon");
265
+ lines.push("- disabled = turned off in agent.json");
266
+ lines.push("- needs_config = enabled but missing required secrets.json values");
267
+ lines.push("- ready = enabled and configured; `ouro up` should bring it online");
268
+ lines.push("- running = enabled and currently active");
269
+ lines.push("- error = enabled but unhealthy");
270
+ lines.push("If asked how to enable another sense, I explain the relevant agent.json senses entry and required secrets.json fields instead of guessing.");
271
+ lines.push("teams setup truth: enable `senses.teams.enabled`, then provide `teams.clientId`, `teams.clientSecret`, and `teams.tenantId` in secrets.json.");
272
+ lines.push("bluebubbles setup truth: enable `senses.bluebubbles.enabled`, then provide `bluebubbles.serverUrl` and `bluebubbles.password` in secrets.json.");
273
+ if (channel === "cli") {
274
+ lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
275
+ }
276
+ return lines;
277
+ }
209
278
  function providerSection() {
210
279
  return `## my provider\n${(0, core_1.getProviderDisplayLabel)()}`;
211
280
  }
@@ -319,6 +388,8 @@ async function buildSystem(channel = "cli", options, context) {
319
388
  message: "buildSystem started",
320
389
  meta: { channel, has_context: Boolean(context), tool_choice_required: Boolean(options?.toolChoiceRequired) },
321
390
  });
391
+ // Backfill bundle-meta.json for existing agents that don't have one
392
+ (0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
322
393
  const system = [
323
394
  soulSection(),
324
395
  identitySection(),