@ouro.bot/cli 0.1.0-alpha.361 → 0.1.0-alpha.363

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/README.md CHANGED
@@ -169,6 +169,7 @@ ouro auth --agent <name>
169
169
  ouro auth --agent <name> --provider <provider>
170
170
  ouro use --agent <name> --lane <outward|inner> --provider <provider> --model <model>
171
171
  ouro hatch
172
+ ouro clone <remote> [--agent <name>] # clone an existing agent from a git remote (see docs/cross-machine-setup.md)
172
173
  ouro chat <agent>
173
174
  ouro msg --to <agent> [--session <id>] [--task <ref>] <message>
174
175
  ouro poke <agent> --task <task-id>
@@ -183,6 +184,10 @@ ouro mcp-serve --agent <name> # start MCP server on stdin/stdout (us
183
184
  ouro hook <event> --agent <name> # fire a lifecycle hook (SessionStart, Stop, PostToolUse)
184
185
  ```
185
186
 
187
+ ## Setting Up On Another Machine
188
+
189
+ To clone an existing agent onto a new machine (macOS, Linux, or Windows via WSL2), see **[docs/cross-machine-setup.md](docs/cross-machine-setup.md)**. The short version: `npx ouro.bot`, pick "clone", enter the bundle's git remote URL, run `ouro auth run`, then `ouro up`.
190
+
186
191
  ## The Agent's Inner Life
187
192
 
188
193
  Agents in Ouroboros aren't just responders — they have an autonomous inner life.
package/changelog.json CHANGED
@@ -1,6 +1,22 @@
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.363",
6
+ "changes": [
7
+ "Bootstrap first-install PATH hint is now shell-aware: shows correct source command for zsh, bash, fish, or generic fallback for unknown shells."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.362",
12
+ "changes": [
13
+ "New `ouro clone <remote>` command for cross-machine agent setup: clones bundle from git remote, creates machine identity, enables sync, guides to auth flow. Infers agent name from URL.",
14
+ "WSL-aware `ouro setup --tool claude-code`: detects WSL2, resolves Windows-side home, calls claude.exe with wsl-prefixed MCP serve and hook commands, writes settings to Windows .claude/ directory.",
15
+ "Platform detection module (detectPlatform) returning macos/linux/wsl/windows-native with injectable deps.",
16
+ "First-run hatch-or-clone interactive choice when no bundles exist. Manual-clone detection during `ouro up` offers to enable sync on git-cloned bundles.",
17
+ "npx ouro.bot bootstrap now passes through to CLI on first install instead of stopping early."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.361",
6
22
  "changes": [
@@ -450,6 +450,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
450
450
  // Refresh system prompt at start of each turn when channel is provided.
451
451
  // If refresh fails, keep existing system prompt (or inject a minimal safe fallback)
452
452
  // so turn execution remains consistent and non-fatal.
453
+ let structuredSystemPrompt;
453
454
  if (channel) {
454
455
  try {
455
456
  const buildSystemOptions = {
@@ -458,7 +459,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
458
459
  supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
459
460
  };
460
461
  const refreshed = await (0, prompt_1.buildSystem)(channel, buildSystemOptions, currentContext);
461
- upsertSystemPrompt(messages, refreshed);
462
+ structuredSystemPrompt = refreshed;
463
+ upsertSystemPrompt(messages, (0, prompt_1.flattenSystemPrompt)(refreshed));
462
464
  }
463
465
  catch (error) {
464
466
  const hadExistingSystemPrompt = messages[0]?.role === "system" && typeof messages[0].content === "string";
@@ -611,6 +613,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
611
613
  toolChoiceRequired,
612
614
  reasoningEffort: currentReasoningEffort,
613
615
  eagerSettleStreaming: true,
616
+ systemPrompt: structuredSystemPrompt,
614
617
  });
615
618
  }
616
619
  catch (error) {
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.mergeStartupStability = mergeStartupStability;
43
43
  exports.ensureDaemonRunning = ensureDaemonRunning;
44
44
  exports.listGithubCopilotModels = listGithubCopilotModels;
45
+ exports.checkManualCloneBundles = checkManualCloneBundles;
45
46
  exports.runOuroCli = runOuroCli;
46
47
  const child_process_1 = require("child_process");
47
48
  const crypto_1 = require("crypto");
@@ -54,6 +55,7 @@ const runtime_1 = require("../../nerves/runtime");
54
55
  const store_file_1 = require("../../mind/friends/store-file");
55
56
  const runtime_metadata_1 = require("./runtime-metadata");
56
57
  const runtime_mode_1 = require("./runtime-mode");
58
+ const platform_1 = require("../platform");
57
59
  const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
58
60
  const update_hooks_1 = require("../versioning/update-hooks");
59
61
  const bundle_meta_1 = require("./hooks/bundle-meta");
@@ -441,7 +443,78 @@ async function verifyProviderCredentials(provider, providers) {
441
443
  return `failed (${error instanceof Error ? error.message : String(error)})`;
442
444
  }
443
445
  }
444
- /* v8 ignore stop */
446
+ async function checkManualCloneBundles(deps) {
447
+ if (!deps.promptInput)
448
+ return;
449
+ let entries;
450
+ try {
451
+ entries = fs.readdirSync(deps.bundlesRoot).filter((e) => e.endsWith(".ouro"));
452
+ }
453
+ catch {
454
+ return;
455
+ }
456
+ for (const agentDir of entries) {
457
+ const bundlePath = path.join(deps.bundlesRoot, agentDir);
458
+ const gitDir = path.join(bundlePath, ".git");
459
+ if (!fs.existsSync(gitDir))
460
+ continue;
461
+ // Check for remotes
462
+ let remoteOutput;
463
+ try {
464
+ remoteOutput = (0, child_process_1.execFileSync)("git", ["remote", "-v"], { cwd: bundlePath, stdio: "pipe" }).toString().trim();
465
+ }
466
+ catch {
467
+ continue;
468
+ }
469
+ if (!remoteOutput)
470
+ continue;
471
+ // Check if sync is already enabled
472
+ const agentJsonPath = path.join(bundlePath, "agent.json");
473
+ if (fs.existsSync(agentJsonPath)) {
474
+ try {
475
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
476
+ const config = JSON.parse(raw);
477
+ if (config.sync?.enabled)
478
+ continue;
479
+ }
480
+ catch {
481
+ // Can't read agent.json — skip
482
+ continue;
483
+ }
484
+ }
485
+ // Parse first remote name
486
+ const firstLine = remoteOutput.split("\n")[0];
487
+ /* v8 ignore next -- defensive fallback: .trim() above strips leading tabs so empty-field path is unreachable @preserve */
488
+ const remoteName = firstLine.split("\t")[0] || "origin";
489
+ (0, runtime_1.emitNervesEvent)({
490
+ component: "daemon",
491
+ event: "daemon.manual_clone_detected",
492
+ message: "bundle appears to be a manually cloned git repo",
493
+ meta: { agent: agentDir, remote: remoteName },
494
+ });
495
+ const answer = await deps.promptInput(`Bundle ${agentDir} appears to be a git clone with a remote. Enable sync? (y/n): `);
496
+ if (answer.trim().toLowerCase() === "y") {
497
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
498
+ const config = JSON.parse(raw);
499
+ config.sync = { enabled: true, remote: remoteName };
500
+ fs.writeFileSync(agentJsonPath, JSON.stringify(config, null, 2) + "\n");
501
+ (0, runtime_1.emitNervesEvent)({
502
+ component: "daemon",
503
+ event: "daemon.manual_clone_sync_enabled",
504
+ message: "sync enabled for manually cloned bundle",
505
+ meta: { agent: agentDir, remote: remoteName },
506
+ });
507
+ }
508
+ else {
509
+ (0, runtime_1.emitNervesEvent)({
510
+ component: "daemon",
511
+ event: "daemon.manual_clone_sync_skipped",
512
+ message: "user declined sync for manually cloned bundle",
513
+ meta: { agent: agentDir },
514
+ });
515
+ }
516
+ }
517
+ }
445
518
  // ── toDaemonCommand ──
446
519
  function toDaemonCommand(command) {
447
520
  return command;
@@ -1270,6 +1343,28 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1270
1343
  if (args.length === 0) {
1271
1344
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : (0, cli_defaults_1.defaultListDiscoveredAgents)());
1272
1345
  if (discovered.length === 0 && deps.runSerpentGuide) {
1346
+ // Hatch-or-clone choice when promptInput is available
1347
+ if (deps.promptInput) {
1348
+ const choice = await deps.promptInput("No agents found. Would you like to hatch a new agent or clone an existing one? (hatch/clone): ");
1349
+ if (choice.trim().toLowerCase() === "clone") {
1350
+ (0, runtime_1.emitNervesEvent)({
1351
+ component: "daemon",
1352
+ event: "daemon.first_run_choice_clone",
1353
+ message: "user chose clone in first-run flow",
1354
+ meta: {},
1355
+ });
1356
+ const remote = await deps.promptInput("Enter the git remote URL for the agent bundle: ");
1357
+ // Run clone execution path
1358
+ const cloneCommand = { kind: "clone", remote: remote.trim() };
1359
+ return await runOuroCli(["clone", cloneCommand.remote], deps);
1360
+ }
1361
+ (0, runtime_1.emitNervesEvent)({
1362
+ component: "daemon",
1363
+ event: "daemon.first_run_choice_hatch",
1364
+ message: "user chose hatch in first-run flow",
1365
+ meta: {},
1366
+ });
1367
+ }
1273
1368
  // System setup first — ouro command, subagents, UTI — before the interactive specialist
1274
1369
  await performSystemSetup(deps);
1275
1370
  const hatchlingName = await deps.runSerpentGuide();
@@ -1497,6 +1592,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1497
1592
  progress.startPhase("bundle cleanup");
1498
1593
  progress.completePhase("bundle cleanup", `pruned ${prunedBundles.length} stale bundle${prunedBundles.length === 1 ? "" : "s"}`);
1499
1594
  }
1595
+ // ── manual-clone detection: offer to enable sync for manually cloned bundles ──
1596
+ await checkManualCloneBundles({
1597
+ bundlesRoot: deps.bundlesRoot ?? bundlesRoot,
1598
+ promptInput: deps.promptInput,
1599
+ });
1500
1600
  progress.startPhase("starting daemon");
1501
1601
  const daemonResult = await ensureDaemonRunning({
1502
1602
  ...deps,
@@ -1908,17 +2008,57 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1908
2008
  // ── setup: configure dev tool integration ──
1909
2009
  if (command.kind === "setup") {
1910
2010
  const { tool, agent: setupAgent } = command;
2011
+ const platform = (0, platform_1.detectPlatform)();
2012
+ // Windows native is not yet supported
2013
+ if (platform === "windows-native") {
2014
+ (0, runtime_1.emitNervesEvent)({
2015
+ component: "daemon",
2016
+ event: "daemon.setup_windows_native_unsupported",
2017
+ message: "Windows native setup not yet supported",
2018
+ meta: { tool, agent: setupAgent },
2019
+ });
2020
+ const message = "Windows native is not yet supported. Please run from WSL2: https://learn.microsoft.com/en-us/windows/wsl/install";
2021
+ deps.writeStdout(message);
2022
+ return message;
2023
+ }
2024
+ // Resolve platform-specific paths and commands
2025
+ let claudeCmd;
2026
+ let mcpServePrefix;
2027
+ let hookPrefix;
2028
+ let claudeConfigDir;
2029
+ if (platform === "wsl") {
2030
+ const winProfile = (0, child_process_1.execFileSync)("cmd.exe", ["/C", "echo", "%USERPROFILE%"], { stdio: "pipe" }).toString().trim();
2031
+ const windowsHome = (0, child_process_1.execFileSync)("wslpath", ["-u", winProfile], { stdio: "pipe" }).toString().trim();
2032
+ (0, runtime_1.emitNervesEvent)({
2033
+ component: "daemon",
2034
+ event: "daemon.setup_wsl_home_resolved",
2035
+ message: "resolved Windows home from WSL",
2036
+ meta: { windowsHome },
2037
+ });
2038
+ claudeCmd = "claude.exe";
2039
+ mcpServePrefix = "wsl ";
2040
+ hookPrefix = "wsl ";
2041
+ claudeConfigDir = path.join(windowsHome, ".claude");
2042
+ }
2043
+ else {
2044
+ // macos or linux
2045
+ claudeCmd = "claude";
2046
+ mcpServePrefix = "";
2047
+ hookPrefix = "";
2048
+ claudeConfigDir = path.join(os.homedir(), ".claude");
2049
+ }
1911
2050
  const sourceRoot = (0, identity_1.getRepoRoot)();
1912
2051
  const runtimeMode = (0, runtime_mode_1.detectRuntimeMode)(sourceRoot);
1913
- const mcpServeCommand = runtimeMode === "dev"
2052
+ const baseMcpServeCommand = runtimeMode === "dev"
1914
2053
  ? `node ${path.join(sourceRoot, "dist", "heart", "daemon", "ouro-bot-entry.js")} mcp-serve --agent ${setupAgent}`
1915
2054
  : `ouro mcp-serve --agent ${setupAgent}`;
2055
+ const mcpServeCommand = `${mcpServePrefix}${baseMcpServeCommand}`;
1916
2056
  if (tool === "claude-code") {
1917
2057
  // 1. Register MCP server with Claude Code
1918
- const mcpAddCmd = `claude mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
2058
+ const mcpAddCmd = `${claudeCmd} mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
1919
2059
  (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
1920
- // 2. Write hooks config to ~/.claude/settings.json
1921
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
2060
+ // 2. Write hooks config
2061
+ const settingsPath = path.join(claudeConfigDir, "settings.json");
1922
2062
  let settings = {};
1923
2063
  if (fs.existsSync(settingsPath)) {
1924
2064
  try {
@@ -1926,13 +2066,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1926
2066
  }
1927
2067
  catch { /* start fresh */ }
1928
2068
  }
1929
- // Use `ouro hook <event>` — resolves the right code based on dev vs installed mode.
1930
- // Bare `ouro` works because ouro is on PATH via ~/.ouro-cli/bin/.
1931
2069
  settings.hooks = {
1932
2070
  ...(settings.hooks ?? {}),
1933
- SessionStart: [{ hooks: [{ type: "command", command: `ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
1934
- Stop: [{ hooks: [{ type: "command", command: `ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
1935
- PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
2071
+ SessionStart: [{ hooks: [{ type: "command", command: `${hookPrefix}ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
2072
+ Stop: [{ hooks: [{ type: "command", command: `${hookPrefix}ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
2073
+ PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `${hookPrefix}ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
1936
2074
  };
1937
2075
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1938
2076
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
@@ -1940,10 +2078,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1940
2078
  component: "daemon",
1941
2079
  event: "daemon.setup_complete",
1942
2080
  message: "dev tool setup complete",
1943
- meta: { tool, agent: setupAgent, runtimeMode },
2081
+ meta: { tool, agent: setupAgent, runtimeMode, platform },
1944
2082
  });
1945
- // 3. Write conversation formatting instructions to ~/.claude/CLAUDE.md
1946
- const claudeMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
2083
+ // 3. Write conversation formatting instructions
2084
+ const claudeMdPath = path.join(claudeConfigDir, "CLAUDE.md");
1947
2085
  const agentInstructions = `\n## Agent conversations (ouro)\nWhen using MCP \`send_message\` to talk to an ouro agent, format the exchange clearly:\n- Before the tool call, briefly say what you're asking/telling the agent\n- After the response, quote the agent's reply in a blockquote, then add your reaction\n- Example: **Me → Agent:** "question" / > **Agent:** "response" / Your synthesis here\n`;
1948
2086
  let existingClaudeMd = "";
1949
2087
  if (fs.existsSync(claudeMdPath)) {
@@ -1964,7 +2102,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1964
2102
  component: "daemon",
1965
2103
  event: "daemon.setup_complete",
1966
2104
  message: "dev tool setup complete",
1967
- meta: { tool, agent: setupAgent, runtimeMode },
2105
+ meta: { tool, agent: setupAgent, runtimeMode, platform },
1968
2106
  });
1969
2107
  const message = `setup complete: codex + ${setupAgent}\n MCP server registered`;
1970
2108
  deps.writeStdout(message);
@@ -2616,6 +2754,65 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2616
2754
  });
2617
2755
  return output;
2618
2756
  }
2757
+ // ── clone: clone an agent bundle from a git remote ──
2758
+ if (command.kind === "clone") {
2759
+ (0, runtime_1.emitNervesEvent)({
2760
+ component: "daemon",
2761
+ event: "daemon.clone_start",
2762
+ message: "starting agent bundle clone",
2763
+ meta: { remote: command.remote, agent: command.agent },
2764
+ });
2765
+ // 1. Check git is installed
2766
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_git_check", message: "checking git installation", meta: {} });
2767
+ try {
2768
+ (0, child_process_1.execFileSync)("git", ["--version"], { stdio: "pipe" });
2769
+ }
2770
+ catch (err) {
2771
+ const message = "git is not installed -- install it from https://git-scm.com\nOn macOS: brew install git\nOn Ubuntu/Debian: sudo apt install git\nOn Windows: download from https://git-scm.com/download/win";
2772
+ deps.writeStdout(message);
2773
+ return message;
2774
+ }
2775
+ // 2. Infer agent name
2776
+ const agentName = command.agent ?? (0, cli_parse_1.inferAgentNameFromRemote)(command.remote);
2777
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
2778
+ const targetPath = path.join(bundlesRoot, agentName + ".ouro");
2779
+ // 3. Check target path does not exist
2780
+ if (fs.existsSync(targetPath)) {
2781
+ const message = `${targetPath} already exists. Remove it first or use --agent to pick a different name.`;
2782
+ deps.writeStdout(message);
2783
+ return message;
2784
+ }
2785
+ // 4. Check remote accessible
2786
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_remote_check", message: "checking remote accessibility", meta: { remote: command.remote } });
2787
+ try {
2788
+ (0, child_process_1.execFileSync)("git", ["ls-remote", "--exit-code", command.remote], { stdio: "pipe", timeout: 15000 });
2789
+ }
2790
+ catch {
2791
+ const message = `could not reach remote: ${command.remote}\nCheck the URL and your network connection.`;
2792
+ deps.writeStdout(message);
2793
+ return message;
2794
+ }
2795
+ // 5. Clone
2796
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_git_clone", message: "cloning agent bundle", meta: { remote: command.remote, targetPath } });
2797
+ (0, child_process_1.execFileSync)("git", ["clone", command.remote, targetPath], { stdio: "pipe" });
2798
+ // 6. Create machine identity
2799
+ (0, machine_identity_1.loadOrCreateMachineIdentity)();
2800
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_identity_created", message: "machine identity created", meta: {} });
2801
+ // 7. Enable sync in agent.json
2802
+ const agentJsonPath = path.join(targetPath, "agent.json");
2803
+ if (fs.existsSync(agentJsonPath)) {
2804
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
2805
+ const config = JSON.parse(raw);
2806
+ config.sync = { enabled: true, remote: "origin" };
2807
+ fs.writeFileSync(agentJsonPath, JSON.stringify(config, null, 2) + "\n");
2808
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_sync_enabled", message: "sync enabled in agent.json", meta: { agentName } });
2809
+ }
2810
+ // 8. Output success message
2811
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_complete", message: "clone complete", meta: { agentName, targetPath } });
2812
+ const message = `cloned ${agentName} to ${targetPath}\nsync enabled (remote: origin)\nnext steps:\n ouro auth run --agent ${agentName}`;
2813
+ deps.writeStdout(message);
2814
+ return message;
2815
+ }
2619
2816
  const daemonCommand = toDaemonCommand(command);
2620
2817
  let response;
2621
2818
  try {
@@ -67,6 +67,12 @@ exports.COMMAND_REGISTRY = {
67
67
  usage: "ouro versions",
68
68
  example: "ouro versions",
69
69
  },
70
+ clone: {
71
+ category: "Lifecycle",
72
+ description: "Clone an existing agent bundle from a git remote onto this machine",
73
+ usage: "ouro clone <remote> [--agent <name>]",
74
+ example: "ouro clone https://github.com/user/myagent.ouro.git",
75
+ },
70
76
  doctor: {
71
77
  category: "Lifecycle",
72
78
  description: "Run diagnostic checks on the ouro installation",
@@ -11,6 +11,7 @@ exports.extractFacingFlag = extractFacingFlag;
11
11
  exports.facingToProviderLane = facingToProviderLane;
12
12
  exports.isAgentProvider = isAgentProvider;
13
13
  exports.usage = usage;
14
+ exports.inferAgentNameFromRemote = inferAgentNameFromRemote;
14
15
  exports.parseMcpServeCommand = parseMcpServeCommand;
15
16
  exports.parseOuroCommand = parseOuroCommand;
16
17
  const types_1 = require("../../mind/friends/types");
@@ -100,6 +101,7 @@ function usage() {
100
101
  " ouro mcp call <server> <tool> [--args '{...}']",
101
102
  " ouro rollback [<version>]",
102
103
  " ouro versions",
104
+ " ouro clone <remote> [--agent <name>]",
103
105
  " ouro doctor",
104
106
  ].join("\n");
105
107
  }
@@ -674,6 +676,41 @@ function parseMcpCommand(args) {
674
676
  }
675
677
  throw new Error(`Usage\n${usage()}`);
676
678
  }
679
+ function inferAgentNameFromRemote(remote) {
680
+ // Remove trailing slash
681
+ let name = remote.replace(/\/+$/, "");
682
+ // Handle SSH URLs (git@host:user/repo) — extract after last / or :
683
+ const lastSlash = name.lastIndexOf("/");
684
+ const lastColon = name.lastIndexOf(":");
685
+ const lastSep = Math.max(lastSlash, lastColon);
686
+ if (lastSep !== -1) {
687
+ name = name.slice(lastSep + 1);
688
+ }
689
+ // Strip .git suffix
690
+ name = name.replace(/\.git$/, "");
691
+ // Strip .ouro suffix
692
+ name = name.replace(/\.ouro$/, "");
693
+ return name;
694
+ }
695
+ function parseCloneCommand(args) {
696
+ let remote;
697
+ let agent;
698
+ for (let i = 0; i < args.length; i++) {
699
+ if (args[i] === "--agent" && args[i + 1]) {
700
+ agent = args[++i];
701
+ continue;
702
+ }
703
+ if (!remote) {
704
+ remote = args[i];
705
+ }
706
+ }
707
+ if (!remote) {
708
+ throw new Error("clone requires a remote URL.\nUsage: ouro clone <remote> [--agent <name>]");
709
+ }
710
+ return agent
711
+ ? { kind: "clone", remote, agent }
712
+ : { kind: "clone", remote };
713
+ }
677
714
  function parseMcpServeCommand(args) {
678
715
  let agent;
679
716
  let friendId;
@@ -903,6 +940,8 @@ function parseOuroCommand(args) {
903
940
  return parseMcpServeCommand(args.slice(1));
904
941
  if (head === "setup")
905
942
  return parseSetupCommand(args.slice(1));
943
+ if (head === "clone")
944
+ return parseCloneCommand(args.slice(1));
906
945
  if (head === "doctor")
907
946
  return { kind: "doctor" };
908
947
  if (head === "bluebubbles")
@@ -0,0 +1,81 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.detectPlatform = detectPlatform;
37
+ const fs = __importStar(require("fs"));
38
+ const runtime_1 = require("../nerves/runtime");
39
+ function detectPlatform(deps = {}) {
40
+ const platform = deps.platform ?? process.platform;
41
+ const env = deps.env ?? process.env;
42
+ const readFile = deps.readFileSync ?? ((p) => fs.readFileSync(p, "utf-8"));
43
+ let result;
44
+ if (platform === "darwin") {
45
+ result = "macos";
46
+ }
47
+ else if (platform === "win32") {
48
+ result = "windows-native";
49
+ }
50
+ else if (platform === "linux") {
51
+ result = detectLinuxOrWsl(env, readFile);
52
+ }
53
+ else {
54
+ // Unknown platform — treat as linux
55
+ result = "linux";
56
+ }
57
+ (0, runtime_1.emitNervesEvent)({
58
+ component: "daemon",
59
+ event: "daemon.platform_detected",
60
+ message: "detected platform",
61
+ meta: { platform, result },
62
+ });
63
+ return result;
64
+ }
65
+ function detectLinuxOrWsl(env, readFile) {
66
+ // Primary: WSL_DISTRO_NAME env var
67
+ if (env.WSL_DISTRO_NAME && env.WSL_DISTRO_NAME.length > 0) {
68
+ return "wsl";
69
+ }
70
+ // Fallback: /proc/version containing "microsoft" (case-insensitive)
71
+ try {
72
+ const procVersion = readFile("/proc/version");
73
+ if (/microsoft/i.test(procVersion)) {
74
+ return "wsl";
75
+ }
76
+ }
77
+ catch {
78
+ // /proc/version not readable — not WSL
79
+ }
80
+ return "linux";
81
+ }
@@ -251,12 +251,28 @@ async function streamAnthropicMessages(client, model, request) {
251
251
  // prompt when using OAuth setup tokens (sk-ant-oat01). Without it, Opus/Sonnet
252
252
  // 4.6 requests are rejected with 400. This is the API's validation that the
253
253
  // token is being used by a Claude Code client.
254
- const claudeCodePreamble = { type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." };
255
- if (system) {
256
- params.system = [claudeCodePreamble, { type: "text", text: system }];
254
+ const preambleText = "You are Claude Code, Anthropic's official CLI for Claude.";
255
+ if (request.systemPrompt) {
256
+ // Structured SystemPrompt: merge preamble + stable prefix into one cached block,
257
+ // volatile suffix as a separate uncached block.
258
+ const stableBlock = {
259
+ type: "text",
260
+ text: preambleText + "\n\n" + request.systemPrompt.stable,
261
+ cache_control: { type: "ephemeral" },
262
+ };
263
+ if (request.systemPrompt.volatile) {
264
+ params.system = [stableBlock, { type: "text", text: request.systemPrompt.volatile }];
265
+ }
266
+ else {
267
+ params.system = [stableBlock];
268
+ }
269
+ }
270
+ else if (system) {
271
+ // Fallback: no structured prompt, extract from messages (legacy path)
272
+ params.system = [{ type: "text", text: preambleText }, { type: "text", text: system }];
257
273
  }
258
274
  else {
259
- params.system = [claudeCodePreamble];
275
+ params.system = [{ type: "text", text: preambleText }];
260
276
  }
261
277
  if (anthropicTools.length > 0)
262
278
  params.tools = anthropicTools;
@@ -5,11 +5,12 @@ const prompt_1 = require("./prompt");
5
5
  const runtime_1 = require("../nerves/runtime");
6
6
  async function refreshSystemPrompt(messages, channel, options, context) {
7
7
  const newSystem = await (0, prompt_1.buildSystem)(channel, options, context);
8
+ const flattened = (0, prompt_1.flattenSystemPrompt)(newSystem);
8
9
  if (messages.length > 0 && messages[0].role === "system") {
9
- messages[0] = { role: "system", content: newSystem };
10
+ messages[0] = { role: "system", content: flattened };
10
11
  }
11
12
  else {
12
- messages.unshift({ role: "system", content: newSystem });
13
+ messages.unshift({ role: "system", content: flattened });
13
14
  }
14
15
  (0, runtime_1.emitNervesEvent)({
15
16
  event: "mind.system_prompt_refreshed",
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.flattenSystemPrompt = flattenSystemPrompt;
36
37
  exports.resetPsycheCache = resetPsycheCache;
37
38
  exports.buildSessionSummary = buildSessionSummary;
38
39
  exports.bodyMapSection = bodyMapSection;
@@ -79,6 +80,10 @@ const daemon_health_1 = require("../heart/daemon/daemon-health");
79
80
  const scrutiny_1 = require("./scrutiny");
80
81
  const pulse_1 = require("../heart/daemon/pulse");
81
82
  const provider_visibility_1 = require("../heart/provider-visibility");
83
+ function flattenSystemPrompt(sp) {
84
+ const parts = [sp.stable, sp.volatile].filter(Boolean);
85
+ return parts.join("\n\n");
86
+ }
82
87
  // Lazy-loaded psyche text cache
83
88
  let _psycheCache = null;
84
89
  let _senseStatusLinesCache = null;
@@ -377,6 +382,7 @@ function runtimeInfoSection(channel, options) {
377
382
  lines.push(`process type: ${processTypeLabel(channel)}`);
378
383
  lines.push(`daemon: ${daemonStatus(options?.daemonRunning)}`);
379
384
  lines.push(`mcp serve: i can expose my tools to dev tools via \`ouro mcp-serve\`. see the configure-dev-tools skill for setup.`);
385
+ lines.push(`harness docs: the harness repo has docs/ and skills/ with guides for setup, operations, and capabilities. docs/ does NOT ship in the npm package — in production, fetch from https://github.com/ouroborosbot/ouroboros/tree/main/docs instead. in dev mode, read from ${sourceRoot}/docs/. when someone asks about setup, installation, cross-machine cloning, deployment, testing, auth, or how i work — consult the docs before guessing.`);
380
386
  if (channel === "cli") {
381
387
  lines.push("i introduce myself on boot with a fun random greeting.");
382
388
  }
@@ -1256,7 +1262,7 @@ async function buildSystem(channel = "cli", options, context) {
1256
1262
  });
1257
1263
  // Backfill bundle-meta.json for existing agents that don't have one
1258
1264
  (0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
1259
- const system = [
1265
+ const stableParts = [
1260
1266
  // Group 1: who i am
1261
1267
  "# who i am",
1262
1268
  soulSection(),
@@ -1264,14 +1270,12 @@ async function buildSystem(channel = "cli", options, context) {
1264
1270
  loreSection(),
1265
1271
  tacitKnowledgeSection(),
1266
1272
  aspirationsSection(),
1267
- // Group 2: my body & environment
1273
+ // Group 2: my body & environment (minus dateSection and rhythmStatusSection)
1268
1274
  "# my body & environment",
1269
1275
  bodyMapSection((0, identity_1.getAgentName)(), channel),
1270
1276
  runtimeInfoSection(channel, options),
1271
- rhythmStatusSection(options?.daemonHealth),
1272
1277
  channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
1273
1278
  providerSection(channel, options),
1274
- dateSection(),
1275
1279
  // Group 3: my tools & capabilities
1276
1280
  "# my tools & capabilities",
1277
1281
  toolsSection(channel, options, context),
@@ -1302,6 +1306,11 @@ async function buildSystem(channel = "cli", options, context) {
1302
1306
  groupChatParticipationSection(context),
1303
1307
  feedbackSignalSection(context),
1304
1308
  ] : []),
1309
+ ];
1310
+ const volatileParts = [
1311
+ // Volatile sections from Group 2 (date and rhythm change every turn)
1312
+ dateSection(),
1313
+ rhythmStatusSection(options?.daemonHealth),
1305
1314
  // Group 7: dynamic state for this turn
1306
1315
  "# dynamic state for this turn",
1307
1316
  startOfTurnPacketSection(options),
@@ -1331,14 +1340,16 @@ async function buildSystem(channel = "cli", options, context) {
1331
1340
  // Group 9: task context
1332
1341
  "# task context",
1333
1342
  taskBoardSection(),
1334
- ]
1335
- .filter(Boolean)
1336
- .join("\n\n");
1343
+ ];
1344
+ const result = {
1345
+ stable: stableParts.filter(Boolean).join("\n\n"),
1346
+ volatile: volatileParts.filter(Boolean).join("\n\n"),
1347
+ };
1337
1348
  (0, runtime_1.emitNervesEvent)({
1338
1349
  event: "mind.step_end",
1339
1350
  component: "mind",
1340
1351
  message: "buildSystem completed",
1341
1352
  meta: { channel },
1342
1353
  });
1343
- return system;
1354
+ return result;
1344
1355
  }
@@ -646,7 +646,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
646
646
  const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
647
647
  const sessionMessages = existing?.messages && existing.messages.length > 0
648
648
  ? existing.messages
649
- : [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", {}, context) }];
649
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
650
650
  if (event.kind === "message") {
651
651
  const agentName = resolvedDeps.getAgentName();
652
652
  if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
@@ -511,7 +511,7 @@ async function runCliSession(options) {
511
511
  (0, commands_1.registerDefaultCommands)(registry);
512
512
  }
513
513
  const messages = options.messages
514
- ?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
514
+ ?? [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli")) }];
515
515
  // ─── Rendering: TUI (Ink + Static) for TTY, imperative for tests/pipes ───
516
516
  const useTui = !options._testInputSource && process.stdin.isTTY === true;
517
517
  let currentAbort = null;
@@ -976,7 +976,7 @@ async function main(agentName, options) {
976
976
  const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
977
977
  const sessionMessages = existing?.messages && existing.messages.length > 0
978
978
  ? existing.messages
979
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", {}, resolvedContext) }];
979
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli", {}, resolvedContext)) }];
980
980
  // Repair any orphaned tool calls from a crash mid-turn
981
981
  (0, core_1.repairOrphanedToolCalls)(sessionMessages);
982
982
  // Per-turn pipeline input: CLI capabilities and pending dir
@@ -672,7 +672,7 @@ async function runInnerDialogTurn(options) {
672
672
  // Fresh session: build system prompt
673
673
  const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true });
674
674
  return {
675
- messages: [{ role: "system", content: systemPrompt }],
675
+ messages: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(systemPrompt) }],
676
676
  sessionPath: sessionFilePath,
677
677
  };
678
678
  },
@@ -112,7 +112,7 @@ async function runSenseTurn(options) {
112
112
  let persistPromise;
113
113
  const sessionMessages = existing?.messages && existing.messages.length > 0
114
114
  ? existing.messages
115
- : [{ role: "system", content: await (0, prompt_1.buildSystem)(channel, {}, undefined) }];
115
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, {}, undefined)) }];
116
116
  // Pending dir
117
117
  const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
118
118
  // Accumulate response text via callbacks
@@ -655,7 +655,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
655
655
  const existing = (0, context_1.loadSession)(sessPath);
656
656
  const messages = existing?.messages && existing.messages.length > 0
657
657
  ? existing.messages
658
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", {}, resolvedContext) }];
658
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("teams", {}, resolvedContext)) }];
659
659
  (0, core_1.repairOrphanedToolCalls)(messages);
660
660
  return {
661
661
  messages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.361",
3
+ "version": "0.1.0-alpha.363",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -1,6 +1,6 @@
1
1
  # Configure Dev Tools for MCP Agent Bridge
2
2
 
3
- Set up your development tools (Claude Code, Codex) to communicate with Ouroboros agents via MCP. One command does everything.
3
+ Set up your development tools (Claude Code, Codex) to communicate with Ouroboros agents via MCP. One command does everything — including cross-platform WSL2 bridging on Windows.
4
4
 
5
5
  ## Setup
6
6
 
@@ -15,6 +15,16 @@ This command:
15
15
  2. Configures lifecycle hooks (SessionStart, Stop, PostToolUse) for passive awareness
16
16
  3. Detects dev vs installed mode automatically and uses the correct command path
17
17
 
18
+ **On WSL2 (Windows):** The command automatically detects the WSL environment and:
19
+ - Calls `claude.exe` (the Windows binary) instead of `claude`
20
+ - Prefixes MCP serve and hook commands with `wsl` so Windows-side Claude Code spawns them through WSL
21
+ - Resolves the Windows-side home directory and writes config to the Windows-side `~/.claude/`
22
+ - After setup, open Claude Code in PowerShell — the agent is there
23
+
24
+ **On native Windows (no WSL):** Not yet supported. The command prints a message directing you to install WSL2.
25
+
26
+ For the full cross-machine setup flow (including cloning an agent to a new machine), see `docs/cross-machine-setup.md` in the harness repo.
27
+
18
28
  ### Codex
19
29
 
20
30
  ```bash