@ouro.bot/cli 0.1.0-alpha.175 → 0.1.0-alpha.176
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 +1 -1
- package/dist/heart/active-work.js +2 -2
- package/dist/heart/core.js +1 -1
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/prompt.js +119 -60
- package/dist/mind/scrutiny.js +173 -0
- package/dist/nerves/coverage/audit.js +1 -1
- package/dist/repertoire/ado-semantic.js +5 -5
- package/dist/repertoire/coding/tools.js +35 -5
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/tools-base.js +145 -16
- package/dist/repertoire/tools.js +37 -0
- package/dist/senses/cli/adapter.js +307 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +68 -0
- package/dist/senses/cli/message-list.js +84 -0
- package/dist/senses/cli/ouro-tui.js +296 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +64 -0
- package/dist/senses/cli/streaming-markdown.js +486 -0
- package/dist/senses/cli/tool-display.js +77 -0
- package/dist/senses/cli/tool-render.js +89 -0
- package/dist/senses/cli/tui-store.js +198 -0
- package/dist/senses/cli/virtual-list.js +68 -0
- package/dist/senses/cli.js +231 -157
- package/dist/senses/pipeline.js +53 -10
- package/package.json +1 -1
- package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
- package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
- package/dist/outlook-ui/index.html +0 -15
|
@@ -92,7 +92,7 @@ function scanSourceFiles(sourceRoot) {
|
|
|
92
92
|
continue;
|
|
93
93
|
walkDir(full);
|
|
94
94
|
}
|
|
95
|
-
else if (entry.name.endsWith(".ts")) {
|
|
95
|
+
else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
96
96
|
const content = (0, fs_1.readFileSync)(full, "utf8");
|
|
97
97
|
const relPath = full.slice(root.length - "src".length);
|
|
98
98
|
fileContents.set(relPath, content);
|
|
@@ -221,7 +221,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
221
221
|
type: "function",
|
|
222
222
|
function: {
|
|
223
223
|
name: "ado_backlog_list",
|
|
224
|
-
description: "Query the backlog and return enriched work items with hierarchy, type, parent, assignee, area path, and iteration. Supports filtering. Use this instead of raw WIQL queries.",
|
|
224
|
+
description: "Query the backlog and return enriched work items with hierarchy, type, parent, assignee, area path, and iteration. Supports filtering. Use this instead of raw WIQL queries. Provides enriched results with hierarchy.",
|
|
225
225
|
parameters: {
|
|
226
226
|
type: "object",
|
|
227
227
|
properties: {
|
|
@@ -287,7 +287,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
287
287
|
type: "function",
|
|
288
288
|
function: {
|
|
289
289
|
name: "ado_create_epic",
|
|
290
|
-
description: "Create an epic in Azure DevOps with title, description, area path, and optional parent.",
|
|
290
|
+
description: "Create an epic in Azure DevOps with title, description, area path, and optional parent. Use ado_preview_changes first to dry-run the operation.",
|
|
291
291
|
parameters: {
|
|
292
292
|
type: "object",
|
|
293
293
|
properties: {
|
|
@@ -327,7 +327,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
327
327
|
type: "function",
|
|
328
328
|
function: {
|
|
329
329
|
name: "ado_create_issue",
|
|
330
|
-
description: "Create an issue or user story in Azure DevOps with title, description, area path, and parent epic.",
|
|
330
|
+
description: "Create an issue or user story in Azure DevOps with title, description, area path, and parent epic. Use ado_preview_changes first to dry-run the operation.",
|
|
331
331
|
parameters: {
|
|
332
332
|
type: "object",
|
|
333
333
|
properties: {
|
|
@@ -369,7 +369,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
369
369
|
type: "function",
|
|
370
370
|
function: {
|
|
371
371
|
name: "ado_move_items",
|
|
372
|
-
description: "Reparent work items -- move them to a new parent epic or feature.",
|
|
372
|
+
description: "Reparent work items -- move them to a new parent epic or feature. Use ado_preview_changes first. These mutations affect real work items.",
|
|
373
373
|
parameters: {
|
|
374
374
|
type: "object",
|
|
375
375
|
properties: {
|
|
@@ -424,7 +424,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
424
424
|
type: "function",
|
|
425
425
|
function: {
|
|
426
426
|
name: "ado_restructure_backlog",
|
|
427
|
-
description: "Bulk restructure: reparent multiple work items in a single logical operation.",
|
|
427
|
+
description: "Bulk restructure: reparent multiple work items in a single logical operation. Use ado_preview_changes first. These mutations affect real work items.",
|
|
428
428
|
parameters: {
|
|
429
429
|
type: "object",
|
|
430
430
|
properties: {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.codingToolDefinitions = void 0;
|
|
4
|
+
exports.countFilesInSessionOutput = countFilesInSessionOutput;
|
|
4
5
|
const index_1 = require("./index");
|
|
5
6
|
const context_pack_1 = require("./context-pack");
|
|
6
7
|
const identity_1 = require("../../heart/identity");
|
|
7
8
|
const obligations_1 = require("../../heart/obligations");
|
|
8
9
|
const runtime_1 = require("../../nerves/runtime");
|
|
10
|
+
const scrutiny_1 = require("../../mind/scrutiny");
|
|
9
11
|
const RUNNERS = ["claude", "codex"];
|
|
10
12
|
function requireArg(args, key) {
|
|
11
13
|
const value = args[key];
|
|
@@ -32,6 +34,34 @@ function emitCodingToolEvent(toolName) {
|
|
|
32
34
|
meta: { toolName },
|
|
33
35
|
});
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Count distinct file paths mentioned in a coding session's stdout output.
|
|
39
|
+
* Looks for path-like tokens (containing / and a file extension).
|
|
40
|
+
* Returns the count of unique paths found.
|
|
41
|
+
*/
|
|
42
|
+
function countFilesInSessionOutput(session) {
|
|
43
|
+
const text = `${session.stdoutTail}\n${session.stderrTail}`;
|
|
44
|
+
// Match path-like tokens: contain at least one / and a file extension
|
|
45
|
+
const pathPattern = /(?:^|\s)((?:\/|\.\/|\.\.\/)?(?:[\w.@-]+\/)+[\w.-]+\.[\w]+)/gm;
|
|
46
|
+
const paths = new Set();
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = pathPattern.exec(text)) !== null) {
|
|
49
|
+
paths.add(match[1]);
|
|
50
|
+
}
|
|
51
|
+
return paths.size;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* If a coding session is completed, append scrutiny to the result.
|
|
55
|
+
* Returns the original result with scrutiny appended, or unchanged if
|
|
56
|
+
* the session is not completed or has no file changes.
|
|
57
|
+
*/
|
|
58
|
+
function appendCompletionScrutiny(result, session) {
|
|
59
|
+
if (session.status !== "completed")
|
|
60
|
+
return result;
|
|
61
|
+
const fileCount = countFilesInSessionOutput(session);
|
|
62
|
+
const scrutiny = (0, scrutiny_1.getCodingCompletionScrutiny)(fileCount);
|
|
63
|
+
return scrutiny ? `${result}\n\n${scrutiny}` : result;
|
|
64
|
+
}
|
|
35
65
|
function sameOriginSession(left, right) {
|
|
36
66
|
if (!left && !right)
|
|
37
67
|
return true;
|
|
@@ -109,7 +139,7 @@ const codingSpawnTool = {
|
|
|
109
139
|
type: "function",
|
|
110
140
|
function: {
|
|
111
141
|
name: "coding_spawn",
|
|
112
|
-
description: "
|
|
142
|
+
description: "Spawn a coding session using claude or codex with task-threaded guidance. The coding session runs as a separate process with its own context. Give it a COMPLETE, SELF-CONTAINED task description -- it cannot see this conversation, doesn't know what you've tried, doesn't understand the broader context. Include: what to do, why, what files are involved, what 'done' looks like. Never delegate understanding -- don't write 'based on the conversation, fix the bug.' Write the specific file paths, line numbers, and what to change. Include any required verification steps or tests in the task description so the coding session knows how to prove the work is done.",
|
|
113
143
|
parameters: {
|
|
114
144
|
type: "object",
|
|
115
145
|
properties: {
|
|
@@ -128,7 +158,7 @@ const codingStatusTool = {
|
|
|
128
158
|
type: "function",
|
|
129
159
|
function: {
|
|
130
160
|
name: "coding_status",
|
|
131
|
-
description: "
|
|
161
|
+
description: "Inspect coding sessions. Omit sessionId to list all active/known sessions with their status. Use this to check progress before asking the human for a status update.",
|
|
132
162
|
parameters: {
|
|
133
163
|
type: "object",
|
|
134
164
|
properties: {
|
|
@@ -141,7 +171,7 @@ const codingTailTool = {
|
|
|
141
171
|
type: "function",
|
|
142
172
|
function: {
|
|
143
173
|
name: "coding_tail",
|
|
144
|
-
description: "
|
|
174
|
+
description: "Show recent stdout/stderr output from a coding session. Use this to understand what the session is doing or why it might be stuck. Read the actual output before reporting status -- don't guess.",
|
|
145
175
|
parameters: {
|
|
146
176
|
type: "object",
|
|
147
177
|
properties: {
|
|
@@ -293,7 +323,7 @@ exports.codingToolDefinitions = [
|
|
|
293
323
|
const session = manager.getSession(sessionId);
|
|
294
324
|
if (!session)
|
|
295
325
|
return `session not found: ${sessionId}`;
|
|
296
|
-
return JSON.stringify(session);
|
|
326
|
+
return appendCompletionScrutiny(JSON.stringify(session), session);
|
|
297
327
|
},
|
|
298
328
|
summaryKeys: ["sessionId"],
|
|
299
329
|
},
|
|
@@ -307,7 +337,7 @@ exports.codingToolDefinitions = [
|
|
|
307
337
|
const session = (0, index_1.getCodingSessionManager)().getSession(sessionId);
|
|
308
338
|
if (!session)
|
|
309
339
|
return `session not found: ${sessionId}`;
|
|
310
|
-
return (0, index_1.formatCodingTail)(session);
|
|
340
|
+
return appendCompletionScrutiny((0, index_1.formatCodingTail)(session), session);
|
|
311
341
|
},
|
|
312
342
|
summaryKeys: ["sessionId"],
|
|
313
343
|
},
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spawnBackgroundShell = spawnBackgroundShell;
|
|
4
|
+
exports.getShellSession = getShellSession;
|
|
5
|
+
exports.listShellSessions = listShellSessions;
|
|
6
|
+
exports.tailShellSession = tailShellSession;
|
|
7
|
+
exports.resetShellSessions = resetShellSessions;
|
|
8
|
+
exports.detectDestructivePatterns = detectDestructivePatterns;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const runtime_1 = require("../nerves/runtime");
|
|
12
|
+
const sessions = new Map();
|
|
13
|
+
const MAX_OUTPUT_LINES = 200;
|
|
14
|
+
function spawnBackgroundShell(command) {
|
|
15
|
+
const id = (0, crypto_1.randomUUID)();
|
|
16
|
+
const proc = (0, child_process_1.spawn)("sh", ["-c", command], {
|
|
17
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
18
|
+
});
|
|
19
|
+
const session = {
|
|
20
|
+
process: proc,
|
|
21
|
+
info: {
|
|
22
|
+
id,
|
|
23
|
+
command,
|
|
24
|
+
status: "running",
|
|
25
|
+
exitCode: null,
|
|
26
|
+
pid: proc.pid,
|
|
27
|
+
startedAt: Date.now(),
|
|
28
|
+
output: [],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const appendOutput = (data) => {
|
|
32
|
+
const lines = data.toString().split("\n");
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (line.length > 0 || session.info.output.length > 0) {
|
|
35
|
+
session.info.output.push(line);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Keep only the last MAX_OUTPUT_LINES
|
|
39
|
+
if (session.info.output.length > MAX_OUTPUT_LINES) {
|
|
40
|
+
session.info.output = session.info.output.slice(-MAX_OUTPUT_LINES);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
proc.stdout?.on("data", appendOutput);
|
|
44
|
+
proc.stderr?.on("data", appendOutput);
|
|
45
|
+
proc.on("close", (code) => {
|
|
46
|
+
session.info.status = "exited";
|
|
47
|
+
session.info.exitCode = code;
|
|
48
|
+
(0, runtime_1.emitNervesEvent)({
|
|
49
|
+
component: "repertoire",
|
|
50
|
+
event: "repertoire.shell.process_exit",
|
|
51
|
+
message: "background shell process exited",
|
|
52
|
+
meta: { id, command, exitCode: code, pid: proc.pid },
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
sessions.set(id, session);
|
|
56
|
+
(0, runtime_1.emitNervesEvent)({
|
|
57
|
+
component: "repertoire",
|
|
58
|
+
event: "repertoire.shell.spawn",
|
|
59
|
+
message: "spawned background shell session",
|
|
60
|
+
meta: { id, command, pid: proc.pid },
|
|
61
|
+
});
|
|
62
|
+
return { ...session.info };
|
|
63
|
+
}
|
|
64
|
+
function getShellSession(id) {
|
|
65
|
+
const session = sessions.get(id);
|
|
66
|
+
if (!session)
|
|
67
|
+
return undefined;
|
|
68
|
+
(0, runtime_1.emitNervesEvent)({
|
|
69
|
+
component: "repertoire",
|
|
70
|
+
event: "repertoire.shell.status_check",
|
|
71
|
+
message: "checked shell session status",
|
|
72
|
+
meta: { id, status: session.info.status, exitCode: session.info.exitCode },
|
|
73
|
+
});
|
|
74
|
+
return { ...session.info };
|
|
75
|
+
}
|
|
76
|
+
function listShellSessions() {
|
|
77
|
+
return Array.from(sessions.values()).map((s) => ({
|
|
78
|
+
id: s.info.id,
|
|
79
|
+
command: s.info.command,
|
|
80
|
+
status: s.info.status,
|
|
81
|
+
exitCode: s.info.exitCode,
|
|
82
|
+
pid: s.info.pid,
|
|
83
|
+
startedAt: s.info.startedAt,
|
|
84
|
+
output: [], // Don't include full output in listing
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
function tailShellSession(id, lines = 50) {
|
|
88
|
+
const session = sessions.get(id);
|
|
89
|
+
if (!session)
|
|
90
|
+
return undefined;
|
|
91
|
+
const tail = session.info.output.slice(-lines);
|
|
92
|
+
(0, runtime_1.emitNervesEvent)({
|
|
93
|
+
component: "repertoire",
|
|
94
|
+
event: "repertoire.shell.tail",
|
|
95
|
+
message: "tailed shell session output",
|
|
96
|
+
meta: { id, requestedLines: lines, returnedLines: tail.length },
|
|
97
|
+
});
|
|
98
|
+
return tail.join("\n");
|
|
99
|
+
}
|
|
100
|
+
/** Reset all sessions (for testing) */
|
|
101
|
+
function resetShellSessions() {
|
|
102
|
+
for (const session of sessions.values()) {
|
|
103
|
+
if (session.info.status === "running") {
|
|
104
|
+
session.process.kill();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
sessions.clear();
|
|
108
|
+
}
|
|
109
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
110
|
+
{ name: "rm -rf root/home", regex: /rm\s+-rf\s+[/~]/ },
|
|
111
|
+
{ name: "git push --force", regex: /git\s+push\s+--force/ },
|
|
112
|
+
{ name: "git reset --hard", regex: /git\s+reset\s+--hard/ },
|
|
113
|
+
{ name: "git clean -f", regex: /git\s+clean\s+-f/ },
|
|
114
|
+
{ name: "git branch -D", regex: /git\s+branch\s+-D/ },
|
|
115
|
+
{ name: "fork bomb", regex: /:\(\)\s*\{/ },
|
|
116
|
+
{ name: "write to raw device", regex: />\s*\/dev\/sd/ },
|
|
117
|
+
{ name: "git checkout .", regex: /git\s+checkout\s+\./ },
|
|
118
|
+
{ name: "git stash drop", regex: /git\s+stash\s+drop/ },
|
|
119
|
+
];
|
|
120
|
+
/**
|
|
121
|
+
* Detect destructive patterns in a shell command.
|
|
122
|
+
* Returns list of matched pattern names. Empty array = safe.
|
|
123
|
+
* This is a friction layer, not a hard block.
|
|
124
|
+
*/
|
|
125
|
+
function detectDestructivePatterns(command) {
|
|
126
|
+
const matched = [];
|
|
127
|
+
for (const pattern of DESTRUCTIVE_PATTERNS) {
|
|
128
|
+
if (pattern.regex.test(command)) {
|
|
129
|
+
matched.push(pattern.name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return matched;
|
|
133
|
+
}
|
|
@@ -40,8 +40,11 @@ const fg = __importStar(require("fast-glob"));
|
|
|
40
40
|
const child_process_1 = require("child_process");
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const skills_1 = require("./skills");
|
|
43
|
+
const shell_sessions_1 = require("./shell-sessions");
|
|
43
44
|
const config_1 = require("../heart/config");
|
|
44
45
|
const runtime_1 = require("../nerves/runtime");
|
|
46
|
+
const file_state_1 = require("../mind/file-state");
|
|
47
|
+
const scrutiny_1 = require("../mind/scrutiny");
|
|
45
48
|
const identity_1 = require("../heart/identity");
|
|
46
49
|
const identity_2 = require("../heart/identity");
|
|
47
50
|
const socket_client_1 = require("../heart/daemon/socket-client");
|
|
@@ -337,7 +340,7 @@ exports.baseToolDefinitions = [
|
|
|
337
340
|
type: "function",
|
|
338
341
|
function: {
|
|
339
342
|
name: "read_file",
|
|
340
|
-
description: "read file
|
|
343
|
+
description: "Read file contents. Results include line numbers. Use offset/limit for large files -- don't read the whole thing if you only need a section. Use this tool before editing any file. When reading code, read enough context to understand the surrounding logic, not just the target line.",
|
|
341
344
|
parameters: {
|
|
342
345
|
type: "object",
|
|
343
346
|
properties: {
|
|
@@ -355,6 +358,17 @@ exports.baseToolDefinitions = [
|
|
|
355
358
|
exports.editFileReadTracker.add(resolvedPath);
|
|
356
359
|
const offset = a.offset ? parseInt(a.offset, 10) : undefined;
|
|
357
360
|
const limit = a.limit ? parseInt(a.limit, 10) : undefined;
|
|
361
|
+
// Record in file state cache for staleness detection
|
|
362
|
+
try {
|
|
363
|
+
const mtime = fs.statSync(resolvedPath).mtimeMs;
|
|
364
|
+
const readContent = (offset === undefined && limit === undefined)
|
|
365
|
+
? content
|
|
366
|
+
: content.split("\n").slice(offset ? offset - 1 : 0, limit !== undefined ? (offset ? offset - 1 : 0) + limit : undefined).join("\n");
|
|
367
|
+
file_state_1.fileStateCache.record(resolvedPath, readContent, mtime, offset, limit);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// stat failed -- skip cache recording
|
|
371
|
+
}
|
|
358
372
|
if (offset === undefined && limit === undefined)
|
|
359
373
|
return content;
|
|
360
374
|
const lines = content.split("\n");
|
|
@@ -369,7 +383,7 @@ exports.baseToolDefinitions = [
|
|
|
369
383
|
type: "function",
|
|
370
384
|
function: {
|
|
371
385
|
name: "write_file",
|
|
372
|
-
description: "
|
|
386
|
+
description: "Prefer this tool for creating new files or fully replacing existing ones. You MUST read an existing file with read_file before overwriting it. Prefer edit_file for modifying existing files -- it only sends the diff. Do not create documentation files (*.md, README) by default; only do so when explicitly asked or when documentation is clearly part of the requested change.",
|
|
373
387
|
parameters: {
|
|
374
388
|
type: "object",
|
|
375
389
|
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
@@ -381,7 +395,10 @@ exports.baseToolDefinitions = [
|
|
|
381
395
|
const resolvedPath = resolveLocalToolPath(a.path);
|
|
382
396
|
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
383
397
|
fs.writeFileSync(resolvedPath, a.content, "utf-8");
|
|
384
|
-
|
|
398
|
+
(0, scrutiny_1.trackModifiedFile)(resolvedPath);
|
|
399
|
+
const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
|
|
400
|
+
/* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
|
|
401
|
+
return scrutiny ? `ok\n\n${scrutiny}` : "ok";
|
|
385
402
|
},
|
|
386
403
|
summaryKeys: ["path"],
|
|
387
404
|
},
|
|
@@ -390,7 +407,7 @@ exports.baseToolDefinitions = [
|
|
|
390
407
|
type: "function",
|
|
391
408
|
function: {
|
|
392
409
|
name: "edit_file",
|
|
393
|
-
description: "
|
|
410
|
+
description: "Surgically edit a file by replacing an exact string. The file MUST have been read via read_file first -- this tool will reject the call otherwise. old_string must match EXACTLY ONE location in the file -- if it matches zero or multiple, the edit fails. To fix: provide more surrounding context to make the match unique. Preserve exact indentation (tabs/spaces) from the file. Prefer this over write_file for modifications -- it only sends the diff.",
|
|
394
411
|
parameters: {
|
|
395
412
|
type: "object",
|
|
396
413
|
properties: {
|
|
@@ -407,6 +424,8 @@ exports.baseToolDefinitions = [
|
|
|
407
424
|
if (!exports.editFileReadTracker.has(resolvedPath)) {
|
|
408
425
|
return `error: you must read the file with read_file before editing it. call read_file on ${a.path} first.`;
|
|
409
426
|
}
|
|
427
|
+
// Check staleness before editing
|
|
428
|
+
const stalenessCheck = file_state_1.fileStateCache.isStale(resolvedPath);
|
|
410
429
|
let content;
|
|
411
430
|
try {
|
|
412
431
|
content = fs.readFileSync(resolvedPath, "utf-8");
|
|
@@ -434,13 +453,33 @@ exports.baseToolDefinitions = [
|
|
|
434
453
|
const idx = occurrences[0];
|
|
435
454
|
const updated = content.slice(0, idx) + a.new_string + content.slice(idx + a.old_string.length);
|
|
436
455
|
fs.writeFileSync(resolvedPath, updated, "utf-8");
|
|
456
|
+
// Update file state cache with new content
|
|
457
|
+
try {
|
|
458
|
+
const newMtime = fs.statSync(resolvedPath).mtimeMs;
|
|
459
|
+
file_state_1.fileStateCache.record(resolvedPath, updated, newMtime);
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// stat failed -- skip cache update
|
|
463
|
+
}
|
|
437
464
|
// Build contextual diff
|
|
438
465
|
const lines = updated.split("\n");
|
|
439
466
|
const prefixLines = content.slice(0, idx).split("\n");
|
|
440
467
|
const changeStartLine = prefixLines.length - 1;
|
|
441
468
|
const newStringLines = a.new_string.split("\n");
|
|
442
469
|
const changeEndLine = changeStartLine + newStringLines.length;
|
|
443
|
-
|
|
470
|
+
const diffResult = buildContextDiff(lines, changeStartLine, changeEndLine);
|
|
471
|
+
// Track modified file and compute scrutiny appendix
|
|
472
|
+
(0, scrutiny_1.trackModifiedFile)(resolvedPath);
|
|
473
|
+
const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
|
|
474
|
+
// Append staleness warning if detected (do not block -- TTFA)
|
|
475
|
+
/* v8 ignore start -- staleness+diff+scrutiny combo not exercised in integration tests @preserve */
|
|
476
|
+
if (stalenessCheck.stale) {
|
|
477
|
+
const base = `${diffResult}\n\n⚠️ warning: file changed externally since last read -- re-read recommended`;
|
|
478
|
+
return scrutiny ? `${base}\n\n${scrutiny}` : base;
|
|
479
|
+
}
|
|
480
|
+
/* v8 ignore stop */
|
|
481
|
+
/* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
|
|
482
|
+
return scrutiny ? `${diffResult}\n\n${scrutiny}` : diffResult;
|
|
444
483
|
},
|
|
445
484
|
summaryKeys: ["path"],
|
|
446
485
|
},
|
|
@@ -449,7 +488,7 @@ exports.baseToolDefinitions = [
|
|
|
449
488
|
type: "function",
|
|
450
489
|
function: {
|
|
451
490
|
name: "glob",
|
|
452
|
-
description: "
|
|
491
|
+
description: "Find files matching a glob pattern, sorted alphabetically. Use this instead of shell commands like `find` or `ls`. For broad exploratory searches that would require multiple rounds of globbing and grepping, consider using claude or coding_spawn.",
|
|
453
492
|
parameters: {
|
|
454
493
|
type: "object",
|
|
455
494
|
properties: {
|
|
@@ -472,7 +511,7 @@ exports.baseToolDefinitions = [
|
|
|
472
511
|
type: "function",
|
|
473
512
|
function: {
|
|
474
513
|
name: "grep",
|
|
475
|
-
description: "
|
|
514
|
+
description: "Search file contents for lines matching a regex pattern. Searches recursively in directories. Use this instead of shell commands like `grep` or `rg`. Returns matching lines with file path and line numbers. Use context_lines for surrounding context. Use include to filter file types (e.g., '*.ts').",
|
|
476
515
|
parameters: {
|
|
477
516
|
type: "object",
|
|
478
517
|
properties: {
|
|
@@ -581,22 +620,112 @@ exports.baseToolDefinitions = [
|
|
|
581
620
|
type: "function",
|
|
582
621
|
function: {
|
|
583
622
|
name: "shell",
|
|
584
|
-
description: "
|
|
623
|
+
description: "Run a shell command and return stdout/stderr. Working directory persists between calls. Use dedicated tools instead of shell when available: read_file instead of cat, edit_file instead of sed, glob instead of find, grep instead of grep/rg. Reserve shell for operations that genuinely need the shell: installing packages, running builds/tests, git operations, process management. Be careful with destructive commands -- consider reversibility before running. If a command fails, read the error output before retrying with a different approach.",
|
|
585
624
|
parameters: {
|
|
586
625
|
type: "object",
|
|
587
|
-
properties: {
|
|
626
|
+
properties: {
|
|
627
|
+
command: { type: "string" },
|
|
628
|
+
timeout_ms: {
|
|
629
|
+
type: "number",
|
|
630
|
+
description: "Timeout in milliseconds. Default: 30000. Max: 600000.",
|
|
631
|
+
},
|
|
632
|
+
background: {
|
|
633
|
+
type: "boolean",
|
|
634
|
+
description: "Run in background. Returns immediately with a process ID. Use shell_status/shell_tail to monitor.",
|
|
635
|
+
},
|
|
636
|
+
},
|
|
588
637
|
required: ["command"],
|
|
589
638
|
},
|
|
590
639
|
},
|
|
591
640
|
},
|
|
592
641
|
handler: (a) => {
|
|
593
|
-
|
|
642
|
+
// Destructive pattern detection (friction, not a block)
|
|
643
|
+
const destructivePatterns = (0, shell_sessions_1.detectDestructivePatterns)(a.command);
|
|
644
|
+
if (destructivePatterns.length > 0) {
|
|
645
|
+
(0, runtime_1.emitNervesEvent)({
|
|
646
|
+
level: "warn",
|
|
647
|
+
event: "tool.shell.destructive_detected",
|
|
648
|
+
component: "tools",
|
|
649
|
+
message: `destructive pattern detected: ${destructivePatterns.join(", ")}`,
|
|
650
|
+
meta: { command: a.command, patterns: destructivePatterns },
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// Background mode: spawn and return immediately
|
|
654
|
+
if (a.background === "true") {
|
|
655
|
+
const session = (0, shell_sessions_1.spawnBackgroundShell)(a.command);
|
|
656
|
+
return JSON.stringify({ id: session.id, command: session.command, status: session.status });
|
|
657
|
+
}
|
|
658
|
+
const MAX_TIMEOUT = 600000;
|
|
659
|
+
const requestedTimeout = Number(a.timeout_ms) || 0;
|
|
660
|
+
let configDefault = 30000;
|
|
661
|
+
try {
|
|
662
|
+
configDefault = (0, identity_1.loadAgentConfig)().shell?.defaultTimeout ?? 30000;
|
|
663
|
+
}
|
|
664
|
+
catch { /* test env: no --agent flag */ }
|
|
665
|
+
const baseTimeout = requestedTimeout > 0 ? requestedTimeout : configDefault;
|
|
666
|
+
const timeout = Math.min(baseTimeout, MAX_TIMEOUT);
|
|
667
|
+
const output = (0, child_process_1.execSync)(a.command, {
|
|
594
668
|
encoding: "utf-8",
|
|
595
|
-
timeout
|
|
669
|
+
timeout,
|
|
596
670
|
});
|
|
671
|
+
if (destructivePatterns.length > 0) {
|
|
672
|
+
return `${output}\n\n--- destructive pattern detected: ${destructivePatterns.join(", ")} ---`;
|
|
673
|
+
}
|
|
674
|
+
return output;
|
|
597
675
|
},
|
|
598
676
|
summaryKeys: ["command"],
|
|
599
677
|
},
|
|
678
|
+
{
|
|
679
|
+
tool: {
|
|
680
|
+
type: "function",
|
|
681
|
+
function: {
|
|
682
|
+
name: "shell_status",
|
|
683
|
+
description: "Check status of background shell processes. Omit id to list all.",
|
|
684
|
+
parameters: {
|
|
685
|
+
type: "object",
|
|
686
|
+
properties: {
|
|
687
|
+
id: { type: "string", description: "Background shell process ID" },
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
handler: (a) => {
|
|
693
|
+
if (!a.id) {
|
|
694
|
+
return JSON.stringify((0, shell_sessions_1.listShellSessions)());
|
|
695
|
+
}
|
|
696
|
+
const session = (0, shell_sessions_1.getShellSession)(a.id);
|
|
697
|
+
if (!session)
|
|
698
|
+
return `process not found: ${a.id}`;
|
|
699
|
+
return JSON.stringify(session);
|
|
700
|
+
},
|
|
701
|
+
summaryKeys: ["id"],
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
tool: {
|
|
705
|
+
type: "function",
|
|
706
|
+
function: {
|
|
707
|
+
name: "shell_tail",
|
|
708
|
+
description: "Show recent output from a background shell process.",
|
|
709
|
+
parameters: {
|
|
710
|
+
type: "object",
|
|
711
|
+
properties: {
|
|
712
|
+
id: { type: "string", description: "Background shell process ID" },
|
|
713
|
+
},
|
|
714
|
+
required: ["id"],
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
handler: (a) => {
|
|
719
|
+
/* v8 ignore next -- schema requires id, defensive guard @preserve */
|
|
720
|
+
if (!a.id)
|
|
721
|
+
return "id is required";
|
|
722
|
+
const output = (0, shell_sessions_1.tailShellSession)(a.id);
|
|
723
|
+
if (output === undefined)
|
|
724
|
+
return `process not found: ${a.id}`;
|
|
725
|
+
return output || "(no output yet)";
|
|
726
|
+
},
|
|
727
|
+
summaryKeys: ["id"],
|
|
728
|
+
},
|
|
600
729
|
{
|
|
601
730
|
tool: {
|
|
602
731
|
type: "function",
|
|
@@ -636,7 +765,7 @@ exports.baseToolDefinitions = [
|
|
|
636
765
|
type: "function",
|
|
637
766
|
function: {
|
|
638
767
|
name: "claude",
|
|
639
|
-
description: "use claude code to query this codebase or get an outside perspective.
|
|
768
|
+
description: "use claude code to query this codebase or get an outside perspective. Use for code review, second opinions, or questions that benefit from a fresh perspective outside this conversation's context.",
|
|
640
769
|
parameters: {
|
|
641
770
|
type: "object",
|
|
642
771
|
properties: { prompt: { type: "string" } },
|
|
@@ -709,7 +838,7 @@ exports.baseToolDefinitions = [
|
|
|
709
838
|
type: "function",
|
|
710
839
|
function: {
|
|
711
840
|
name: "recall",
|
|
712
|
-
description: "
|
|
841
|
+
description: "Search my diary and journal for facts, thoughts, and working notes matching a query. Uses semantic similarity -- phrasing matters. Try different angles if the first query doesn't find what you're looking for. Check recall before asking the human something you might already know.",
|
|
713
842
|
parameters: {
|
|
714
843
|
type: "object",
|
|
715
844
|
properties: { query: { type: "string" } },
|
|
@@ -762,7 +891,7 @@ exports.baseToolDefinitions = [
|
|
|
762
891
|
type: "function",
|
|
763
892
|
function: {
|
|
764
893
|
name: "diary_write",
|
|
765
|
-
description: "
|
|
894
|
+
description: "Write an entry in my diary -- something I learned, noticed, or concluded that I want to recall later. Use 'about' to tag the entry to a person, topic, or context. Write for my future self: include enough context that the entry makes sense without the surrounding conversation. Prefer durable conclusions over passing noise. Don't duplicate what already belongs in friend notes.",
|
|
766
895
|
parameters: {
|
|
767
896
|
type: "object",
|
|
768
897
|
properties: {
|
|
@@ -1376,7 +1505,7 @@ exports.ponderTool = {
|
|
|
1376
1505
|
type: "function",
|
|
1377
1506
|
function: {
|
|
1378
1507
|
name: "ponder",
|
|
1379
|
-
description: "i need to sit with this. from a conversation, takes the thread inward with a thought and a parting word. from inner dialog, keeps the wheel turning for another pass. must be the only tool call in the turn.",
|
|
1508
|
+
description: "i need to sit with this. from a conversation, takes the thread inward with a thought and a parting word. from inner dialog, keeps the wheel turning for another pass. must be the only tool call in the turn. Use when a question deserves more thought than this turn allows. Don't ponder trivial questions.",
|
|
1380
1509
|
parameters: {
|
|
1381
1510
|
type: "object",
|
|
1382
1511
|
properties: {
|
|
@@ -1409,7 +1538,7 @@ exports.settleTool = {
|
|
|
1409
1538
|
type: "function",
|
|
1410
1539
|
function: {
|
|
1411
1540
|
name: "settle",
|
|
1412
|
-
description: "respond to the user with your message. call this tool when you are ready to deliver your response.",
|
|
1541
|
+
description: "respond to the user with your message. call this tool when you are ready to deliver your response. Only call when you have a substantive response. If you're settling with 'I'll look into that,' you probably should be using a tool instead.",
|
|
1413
1542
|
parameters: {
|
|
1414
1543
|
type: "object",
|
|
1415
1544
|
properties: {
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.getToolsForChannel = getToolsForChannel;
|
|
|
38
38
|
exports.isConfirmationRequired = isConfirmationRequired;
|
|
39
39
|
exports.execTool = execTool;
|
|
40
40
|
exports.summarizeArgs = summarizeArgs;
|
|
41
|
+
exports.buildToolResultSummary = buildToolResultSummary;
|
|
41
42
|
const tools_base_1 = require("./tools-base");
|
|
42
43
|
const tools_teams_1 = require("./tools-teams");
|
|
43
44
|
const tools_bluebubbles_1 = require("./tools-bluebubbles");
|
|
@@ -387,3 +388,39 @@ function summarizeArgs(name, args) {
|
|
|
387
388
|
}
|
|
388
389
|
return summarizeUnknownArgs(args);
|
|
389
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Build an enriched tool result summary for display on remote channels.
|
|
393
|
+
* For recognized tools, includes result-derived info (diff stats, exit codes, etc.).
|
|
394
|
+
* Falls back to arg-based summary for unrecognized tools.
|
|
395
|
+
*/
|
|
396
|
+
function buildToolResultSummary(name, args, _result, success) {
|
|
397
|
+
switch (name) {
|
|
398
|
+
case "edit_file": {
|
|
399
|
+
if (!success)
|
|
400
|
+
return summarizeArgs(name, args);
|
|
401
|
+
const addedLines = (args.new_string ?? "").split("\n").length;
|
|
402
|
+
const removedLines = (args.old_string ?? "").split("\n").length;
|
|
403
|
+
return `+${addedLines} -${removedLines} lines in ${args.path ?? "unknown"}`;
|
|
404
|
+
}
|
|
405
|
+
case "shell": {
|
|
406
|
+
const cmd = args.command ?? "?";
|
|
407
|
+
const exitCode = success ? 0 : 1;
|
|
408
|
+
return `$ ${cmd} (exit ${exitCode})`;
|
|
409
|
+
}
|
|
410
|
+
case "read_file":
|
|
411
|
+
return `path=${args.path ?? "unknown"}`;
|
|
412
|
+
case "write_file":
|
|
413
|
+
return `path=${args.path ?? "unknown"}`;
|
|
414
|
+
case "glob":
|
|
415
|
+
return `pattern=${args.pattern ?? "?"} ${args.cwd ? `cwd=${args.cwd}` : ""}`.trim();
|
|
416
|
+
case "grep":
|
|
417
|
+
return `pattern=${args.pattern ?? "?"} ${args.path ? `path=${args.path}` : ""}`.trim();
|
|
418
|
+
case "coding_spawn": {
|
|
419
|
+
const taskRef = args.taskRef ?? "unknown";
|
|
420
|
+
const status = success ? "spawned" : "failed";
|
|
421
|
+
return `${taskRef} -> ${status}`;
|
|
422
|
+
}
|
|
423
|
+
default:
|
|
424
|
+
return summarizeArgs(name, args);
|
|
425
|
+
}
|
|
426
|
+
}
|