@ouro.bot/cli 0.1.0-alpha.12 → 0.1.0-alpha.14

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
  }
@@ -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,6 +47,7 @@ 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
@@ -387,6 +388,8 @@ async function buildSystem(channel = "cli", options, context) {
387
388
  message: "buildSystem started",
388
389
  meta: { channel, has_context: Boolean(context), tool_choice_required: Boolean(options?.toolChoiceRequired) },
389
390
  });
391
+ // Backfill bundle-meta.json for existing agents that don't have one
392
+ (0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
390
393
  const system = [
391
394
  soulSection(),
392
395
  identitySection(),