@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.85

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 CHANGED
@@ -1,6 +1,20 @@
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.85",
6
+ "changes": [
7
+ "Relative repo paths now route through the chosen safe workspace too, so read/edit/write file tools actually operate in the dedicated clone or worktree instead of silently hitting the wrong checkout.",
8
+ "File-edit guardrails now normalize paths the same way file reads do, which means a `read_file` followed by `edit_file` on the same relative path no longer self-blocks during safe-workspace routing."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.84",
13
+ "changes": [
14
+ "Safe workspace routing now covers repo-local shell commands too, so agents that discover a harness friction through `shell` land in the dedicated worktree instead of wandering in the shared checkout.",
15
+ "The new `safe_workspace` tool and prompt guidance make the chosen workspace path, branch, and first concrete repo action explicit before the first edit, tightening the visible OODA loop."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.83",
6
20
  "changes": [
@@ -37,6 +37,7 @@ exports.resetSafeWorkspaceSelection = resetSafeWorkspaceSelection;
37
37
  exports.getActiveSafeWorkspaceSelection = getActiveSafeWorkspaceSelection;
38
38
  exports.ensureSafeRepoWorkspace = ensureSafeRepoWorkspace;
39
39
  exports.resolveSafeRepoPath = resolveSafeRepoPath;
40
+ exports.resolveSafeShellExecution = resolveSafeShellExecution;
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const child_process_1 = require("child_process");
@@ -96,7 +97,7 @@ function createDedicatedWorktree(repoRoot, workspaceRoot, branchSuffix, existsSy
96
97
  rmSync(workspaceRoot, { recursive: true, force: true });
97
98
  }
98
99
  assertGitOk(runGit(repoRoot, ["worktree", "add", "-B", branchName, workspaceRoot, "origin/main"], spawnSync), "git worktree add");
99
- return { workspaceRoot, created: true };
100
+ return { workspaceRoot, created: true, branchName };
100
101
  }
101
102
  function createScratchClone(workspaceRoot, cloneUrl, existsSync, mkdirSync, rmSync, spawnSync) {
102
103
  mkdirSync(path.dirname(workspaceRoot), { recursive: true });
@@ -107,7 +108,11 @@ function createScratchClone(workspaceRoot, cloneUrl, existsSync, mkdirSync, rmSy
107
108
  stdio: ["ignore", "pipe", "pipe"],
108
109
  });
109
110
  assertGitOk(result, "git clone");
110
- return { workspaceRoot, created: true };
111
+ return { workspaceRoot, created: true, branchName: "main" };
112
+ }
113
+ const REPO_LOCAL_SHELL_COMMAND = /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(git|npm|npx|node|pnpm|yarn|bun|rg|sed|cat|ls|find|grep|vitest|tsc|eslint)\b/;
114
+ function looksRepoLocalShellCommand(command) {
115
+ return REPO_LOCAL_SHELL_COMMAND.test(command.trim());
111
116
  }
112
117
  function registerCleanupHook(options) {
113
118
  if (cleanupHookRegistered)
@@ -160,6 +165,7 @@ function ensureSafeRepoWorkspace(options = {}) {
160
165
  runtimeKind: "clone-main",
161
166
  repoRoot,
162
167
  workspaceRoot: created.workspaceRoot,
168
+ workspaceBranch: created.branchName,
163
169
  sourceBranch: branch,
164
170
  sourceCloneUrl: canonicalRepoUrl,
165
171
  cleanupAfterMerge: false,
@@ -174,6 +180,7 @@ function ensureSafeRepoWorkspace(options = {}) {
174
180
  runtimeKind: "clone-non-main",
175
181
  repoRoot,
176
182
  workspaceRoot: created.workspaceRoot,
183
+ workspaceBranch: created.branchName,
177
184
  sourceBranch: branch,
178
185
  sourceCloneUrl: canonicalRepoUrl,
179
186
  cleanupAfterMerge: false,
@@ -189,6 +196,7 @@ function ensureSafeRepoWorkspace(options = {}) {
189
196
  runtimeKind: "installed-runtime",
190
197
  repoRoot,
191
198
  workspaceRoot: created.workspaceRoot,
199
+ workspaceBranch: created.branchName,
192
200
  sourceBranch: null,
193
201
  sourceCloneUrl: canonicalRepoUrl,
194
202
  cleanupAfterMerge: true,
@@ -205,6 +213,7 @@ function ensureSafeRepoWorkspace(options = {}) {
205
213
  runtimeKind: selection.runtimeKind,
206
214
  repoRoot: selection.repoRoot,
207
215
  workspaceRoot: selection.workspaceRoot,
216
+ workspaceBranch: selection.workspaceBranch,
208
217
  sourceBranch: selection.sourceBranch,
209
218
  sourceCloneUrl: selection.sourceCloneUrl,
210
219
  cleanupAfterMerge: selection.cleanupAfterMerge,
@@ -213,8 +222,16 @@ function ensureSafeRepoWorkspace(options = {}) {
213
222
  return selection;
214
223
  }
215
224
  function resolveSafeRepoPath(options) {
216
- const requestedPath = path.resolve(options.requestedPath);
225
+ const rawRequestedPath = options.requestedPath;
217
226
  const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
227
+ if (!path.isAbsolute(rawRequestedPath) && !rawRequestedPath.startsWith("~")) {
228
+ const selection = activeSelection ?? ensureSafeRepoWorkspace(options);
229
+ return {
230
+ selection,
231
+ resolvedPath: path.resolve(selection.workspaceRoot, rawRequestedPath),
232
+ };
233
+ }
234
+ const requestedPath = path.resolve(rawRequestedPath);
218
235
  if (activeSelection && requestedPath.startsWith(activeSelection.workspaceRoot + path.sep)) {
219
236
  return { selection: activeSelection, resolvedPath: requestedPath };
220
237
  }
@@ -226,3 +243,27 @@ function resolveSafeRepoPath(options) {
226
243
  const resolvedPath = relativePath ? path.join(selection.workspaceRoot, relativePath) : selection.workspaceRoot;
227
244
  return { selection, resolvedPath };
228
245
  }
246
+ function resolveSafeShellExecution(command, options = {}) {
247
+ const trimmed = command.trim();
248
+ if (!trimmed) {
249
+ return { selection: activeSelection, command };
250
+ }
251
+ if (activeSelection && command.includes(activeSelection.workspaceRoot)) {
252
+ return { selection: activeSelection, command, cwd: activeSelection.workspaceRoot };
253
+ }
254
+ const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
255
+ const mentionsRepoRoot = command.includes(repoRoot);
256
+ const shouldRoute = mentionsRepoRoot || looksRepoLocalShellCommand(trimmed);
257
+ if (!shouldRoute) {
258
+ return { selection: activeSelection, command };
259
+ }
260
+ const selection = ensureSafeRepoWorkspace(options);
261
+ const rewrittenCommand = mentionsRepoRoot
262
+ ? command.split(repoRoot).join(selection.workspaceRoot)
263
+ : command;
264
+ return {
265
+ selection,
266
+ command: rewrittenCommand,
267
+ cwd: selection.workspaceRoot,
268
+ };
269
+ }
@@ -565,6 +565,16 @@ tool_choice is set to "required" -- i must call a tool on every turn.
565
565
  \`final_answer\` must be the ONLY tool call in that turn. do not combine it with other tool calls.
566
566
  do NOT call no-op tools just before \`final_answer\`. if i am done, i call \`final_answer\` directly.`;
567
567
  }
568
+ function workspaceDisciplineSection() {
569
+ return `## repo workspace discipline
570
+ when a shared-harness or local code fix needs repo work, i get the real workspace first with \`safe_workspace\`.
571
+ \`read_file\`, \`write_file\`, and \`edit_file\` already map repo paths into that workspace. shell commands that target the harness run there too.
572
+
573
+ before the first repo edit, i tell the user in 1-2 short lines:
574
+ - the friction i'm fixing
575
+ - the workspace path/branch i'm using
576
+ - the first concrete action i'm taking`;
577
+ }
568
578
  function contextSection(context, options) {
569
579
  if (!context)
570
580
  return "";
@@ -699,6 +709,7 @@ async function buildSystem(channel = "cli", options, context) {
699
709
  toolsSection(channel, options, context),
700
710
  mcpToolsSection(options?.mcpManager),
701
711
  reasoningEffortSection(options),
712
+ workspaceDisciplineSection(),
702
713
  toolRestrictionSection(context),
703
714
  trustContextSection(context),
704
715
  mixedTrustGroupSection(context),
@@ -404,6 +404,29 @@ exports.baseToolDefinitions = [
404
404
  return allResults.join("\n");
405
405
  },
406
406
  },
407
+ {
408
+ tool: {
409
+ type: "function",
410
+ function: {
411
+ name: "safe_workspace",
412
+ description: "acquire or inspect the safe harness repo workspace for local edits. returns the real workspace path, branch, and why it was chosen.",
413
+ parameters: {
414
+ type: "object",
415
+ properties: {},
416
+ },
417
+ },
418
+ },
419
+ handler: () => {
420
+ const selection = (0, safe_workspace_1.ensureSafeRepoWorkspace)();
421
+ return [
422
+ `workspace: ${selection.workspaceRoot}`,
423
+ `branch: ${selection.workspaceBranch}`,
424
+ `runtime: ${selection.runtimeKind}`,
425
+ `cleanup_after_merge: ${selection.cleanupAfterMerge ? "yes" : "no"}`,
426
+ `note: ${selection.note}`,
427
+ ].join("\n");
428
+ },
429
+ },
407
430
  {
408
431
  tool: {
409
432
  type: "function",
@@ -417,7 +440,14 @@ exports.baseToolDefinitions = [
417
440
  },
418
441
  },
419
442
  },
420
- handler: (a) => (0, child_process_1.execSync)(a.command, { encoding: "utf-8", timeout: 30000 }),
443
+ handler: (a) => {
444
+ const prepared = (0, safe_workspace_1.resolveSafeShellExecution)(a.command);
445
+ return (0, child_process_1.execSync)(prepared.command, {
446
+ encoding: "utf-8",
447
+ timeout: 30000,
448
+ ...(prepared.cwd ? { cwd: prepared.cwd } : {}),
449
+ });
450
+ },
421
451
  },
422
452
  {
423
453
  tool: {
@@ -13,6 +13,7 @@ const tools_github_1 = require("./tools-github");
13
13
  const runtime_1 = require("../nerves/runtime");
14
14
  const guardrails_1 = require("./guardrails");
15
15
  const identity_1 = require("../heart/identity");
16
+ const safe_workspace_1 = require("../heart/safe-workspace");
16
17
  function safeGetAgentRoot() {
17
18
  try {
18
19
  return (0, identity_1.getAgentRoot)();
@@ -99,6 +100,15 @@ function isConfirmationRequired(toolName) {
99
100
  const def = allDefinitions.find((d) => d.tool.function.name === toolName);
100
101
  return def?.confirmationRequired === true;
101
102
  }
103
+ function normalizeGuardArgs(name, args) {
104
+ if ((name === "read_file" || name === "write_file" || name === "edit_file") && args.path) {
105
+ return {
106
+ ...args,
107
+ path: (0, safe_workspace_1.resolveSafeRepoPath)({ requestedPath: args.path }).resolvedPath,
108
+ };
109
+ }
110
+ return args;
111
+ }
102
112
  async function execTool(name, args, ctx) {
103
113
  (0, runtime_1.emitNervesEvent)({
104
114
  event: "tool.start",
@@ -124,7 +134,8 @@ async function execTool(name, args, ctx) {
124
134
  trustLevel: ctx?.context?.friend?.trustLevel,
125
135
  agentRoot: safeGetAgentRoot(),
126
136
  };
127
- const guardResult = (0, guardrails_1.guardInvocation)(name, args, guardContext);
137
+ const guardArgs = normalizeGuardArgs(name, args);
138
+ const guardResult = (0, guardrails_1.guardInvocation)(name, guardArgs, guardContext);
128
139
  if (!guardResult.allowed) {
129
140
  (0, runtime_1.emitNervesEvent)({
130
141
  level: "warn",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.83",
3
+ "version": "0.1.0-alpha.85",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",