@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 +14 -0
- package/dist/heart/safe-workspace.js +44 -3
- package/dist/mind/prompt.js +11 -0
- package/dist/repertoire/tools-base.js +31 -1
- package/dist/repertoire/tools.js +12 -1
- package/package.json +1 -1
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
|
|
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
|
+
}
|
package/dist/mind/prompt.js
CHANGED
|
@@ -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) =>
|
|
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: {
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -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
|
|
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",
|