@ouro.bot/cli 0.1.0-alpha.175 → 0.1.0-alpha.177

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.
@@ -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: "spawn a coding session using claude/codex and explicit task-threaded guidance",
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: "inspect coding sessions; omit sessionId to list all active/known sessions",
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: "show recent stdout/stderr tail for a coding session in a readable format",
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 contents",
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: "write content to file",
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
- return "ok";
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: "surgically edit a file by replacing an exact string. the file must have been read via read_file first. old_string must match exactly one location in the file.",
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
- return buildContextDiff(lines, changeStartLine, changeEndLine);
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: "find files matching a glob pattern. returns matching paths sorted alphabetically, one per line.",
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: "search file contents for lines matching a regex pattern. searches recursively when given a directory. returns matching lines with file path and line numbers.",
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: "run shell command",
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: { command: { type: "string" } },
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
- return (0, child_process_1.execSync)(a.command, {
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: 30000,
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. useful for code review, second opinions, and asking questions about your own source.",
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: "recall what i know — search my diary and journal for facts, thoughts, and working notes that match a query",
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: "write an entry in my diary — something i learned, noticed, or concluded that i want to recall later. optional 'about' tags the entry to a person, topic, or context.",
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: {
@@ -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
+ }