@bahulam/code 2.6.12 → 2.6.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bahulam/code",
3
- "version": "2.6.12",
3
+ "version": "2.6.14",
4
4
  "description": "Bahulam Code \u2014 abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,8 +49,8 @@
49
49
  "web-tree-sitter": "^0.26.9"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@bahulam/runtime-darwin-arm64": "2.6.12",
53
- "@bahulam/runtime-linux-x64": "2.6.12",
54
- "@bahulam/runtime-win32-x64": "2.6.12"
52
+ "@bahulam/runtime-darwin-arm64": "2.6.14",
53
+ "@bahulam/runtime-linux-x64": "2.6.14",
54
+ "@bahulam/runtime-win32-x64": "2.6.14"
55
55
  }
56
56
  }
@@ -204,6 +204,7 @@ export async function syncAgentsToBackend({
204
204
  headers: {
205
205
  Authorization: `Bearer ${token}`,
206
206
  'Content-Type': 'application/json',
207
+ 'X-Product': 'bahulam',
207
208
  },
208
209
  body: JSON.stringify(payload),
209
210
  signal: AbortSignal.timeout(timeoutMs),
@@ -59,7 +59,7 @@ function getAuth() {
59
59
  const auth = new TarangAuth();
60
60
  const creds = auth.loadCredentials();
61
61
  if (!creds.token) {
62
- process.stderr.write(`${RED}✗ Not authenticated. Run `bahulam-code login` first.${RESET}\n`);
62
+ process.stderr.write(`${RED}✗ Not authenticated. Run bahulam-code login first.${RESET}\n`);
63
63
  process.exit(1);
64
64
  }
65
65
  return creds;
@@ -76,13 +76,14 @@ async function apiFetch(path, options = {}) {
76
76
  const headers = {
77
77
  'Authorization': `Bearer ${creds.token}`,
78
78
  'Content-Type': 'application/json',
79
+ 'X-Product': 'bahulam',
79
80
  ...options.headers,
80
81
  };
81
82
  const url = `${baseUrl}${path}`;
82
83
  const response = await fetch(url, { ...options, headers });
83
84
 
84
85
  if (response.status === 401) {
85
- process.stderr.write(`${RED}✗ Authentication failed. Run `bahulam-code login` to re-authenticate.${RESET}\n`);
86
+ process.stderr.write(`${RED}✗ Authentication failed. Run bahulam-code login to re-authenticate.${RESET}\n`);
86
87
  process.exit(1);
87
88
  }
88
89
 
@@ -2,6 +2,9 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
 
4
4
  const REDACTED = 'REDACTED';
5
+ const MAX_ARG_LOG_CHARS = 4000;
6
+ const MAX_PROMPT_SUBJECT_CHARS = 4000;
7
+ const MAX_PROMPT_REASON_CHARS = 1200;
5
8
  const SENSITIVE_KEY_RE = /^(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)$|(?:^|[-_])(?:api[-_]?key|apikey|token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)(?:$|[-_])/i;
6
9
  const SENSITIVE_ASSIGNMENT_KEY = String.raw`[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*`;
7
10
  const SENSITIVE_JSON_KEY = String.raw`(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key|[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*)`;
@@ -48,10 +51,10 @@ function sanitizeForLog(value, depth = 0) {
48
51
  }
49
52
 
50
53
  function safeArgs(args) {
51
- if (args?.command) return redactSensitive(String(args.command)).slice(0, 500);
52
- if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0, 500);
53
- try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, 500); }
54
- catch { return redactSensitive(String(args || '')).slice(0, 500); }
54
+ if (args?.command) return redactSensitive(String(args.command)).slice(0, MAX_ARG_LOG_CHARS);
55
+ if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0, MAX_ARG_LOG_CHARS);
56
+ try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, MAX_ARG_LOG_CHARS); }
57
+ catch { return redactSensitive(String(args || '')).slice(0, MAX_ARG_LOG_CHARS); }
55
58
  }
56
59
 
57
60
  function safeText(value, max = 500) {
@@ -59,6 +62,19 @@ function safeText(value, max = 500) {
59
62
  return redactSensitive(String(value)).slice(0, max);
60
63
  }
61
64
 
65
+ function safePrompt(prompt) {
66
+ if (!prompt || typeof prompt !== 'object') return undefined;
67
+ const out = {
68
+ title: safeText(prompt.title, 200),
69
+ subject: safeText(prompt.subject, MAX_PROMPT_SUBJECT_CHARS),
70
+ reason: safeText(prompt.reason, MAX_PROMPT_REASON_CHARS),
71
+ };
72
+ for (const key of Object.keys(out)) {
73
+ if (out[key] === undefined) delete out[key];
74
+ }
75
+ return Object.keys(out).length ? out : undefined;
76
+ }
77
+
62
78
  export class ApprovalLog {
63
79
  constructor({ cwd = process.cwd() } = {}) {
64
80
  this.cwd = cwd;
@@ -68,6 +84,7 @@ export class ApprovalLog {
68
84
  append(entry) {
69
85
  try {
70
86
  fs.mkdirSync(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
87
+ const prompt = safePrompt(entry.prompt);
71
88
  const line = JSON.stringify({
72
89
  ts: new Date().toISOString(),
73
90
  tier: entry.tier,
@@ -77,6 +94,7 @@ export class ApprovalLog {
77
94
  scope: entry.scope || 'once',
78
95
  rule_id: entry.rule_id || null,
79
96
  reason: safeText(entry.reason),
97
+ ...(prompt ? { prompt } : {}),
80
98
  });
81
99
  fs.appendFileSync(this.filePath, line + '\n', { mode: 0o600 });
82
100
  try { fs.chmodSync(this.filePath, 0o600); } catch {}
@@ -10,7 +10,7 @@
10
10
  * Shell commands are risk-assessed (safe/medium/high).
11
11
  */
12
12
 
13
- import { shellCommandDisplay, toolDisplaySummary } from '../terminal/tool-display.mjs';
13
+ import { shellCommandDisplay, shellCommandProfile, toolDisplaySummary } from '../terminal/tool-display.mjs';
14
14
  import {
15
15
  classify as classifyTier,
16
16
  TIERS,
@@ -20,10 +20,11 @@ import {
20
20
  } from './risk-tier.mjs';
21
21
  import {
22
22
  renderApprovalPrompt,
23
+ renderApprovalDockPrompt,
23
24
  renderTrustedApproval,
24
25
  defaultOptions as approvalOptions,
25
26
  } from '../ui/approval.mjs';
26
- import { isInputDockMounted, moveToContent } from '../ui/input-dock.mjs';
27
+ import { clearInputPrompt, isInputDockMounted, moveToContent, renderDockOverlay } from '../ui/input-dock.mjs';
27
28
  import { validateShellCommand } from './safety.mjs';
28
29
  import { classifyCommand } from '../permissions/command-classifier.mjs';
29
30
  import { ApprovalLog } from './approval-log.mjs';
@@ -72,12 +73,22 @@ function shellHardBlockReason(tool, args = {}) {
72
73
  if (tool !== 'shell') return '';
73
74
  const command = args.command || args.cmd || '';
74
75
  const safety = validateShellCommand(command);
75
- if (!safety.safe) return safety.reason || 'Blocked by shell safety policy';
76
+ if (!safety.safe) return withShellRetryHint(safety.reason || 'Blocked by shell safety policy');
76
77
  const classification = classifyCommand(command);
77
- if (classification.classification === 'blocked') return classification.reason || 'Blocked by shell safety policy';
78
+ if (classification.classification === 'blocked') {
79
+ return withShellRetryHint(classification.reason || 'Blocked by shell safety policy');
80
+ }
78
81
  return '';
79
82
  }
80
83
 
84
+ function withShellRetryHint(reason) {
85
+ const text = String(reason || '').trim();
86
+ if (/command substitution|backticks|\$\(\)/i.test(text)) {
87
+ return `${text}. Retry with separate simple shell commands instead of backticks or $().`;
88
+ }
89
+ return text;
90
+ }
91
+
81
92
  // ── ANSI helpers ──
82
93
 
83
94
  const RST = '\x1b[0m';
@@ -111,9 +122,10 @@ export class ApprovalManager {
111
122
 
112
123
  setReadline(rl) { this._rl = rl; }
113
124
 
114
- setExecutionHooks({ onPause, onResume } = {}) {
125
+ setExecutionHooks({ onPause, onResume, onApprovalPromptEnd } = {}) {
115
126
  this._execPause = onPause || null;
116
127
  this._execResume = onResume || null;
128
+ this._approvalPromptEnd = onApprovalPromptEnd || null;
117
129
  }
118
130
 
119
131
  revoke() {
@@ -143,7 +155,8 @@ export class ApprovalManager {
143
155
  const reason = `Blocked by safety policy: ${hardBlock}`;
144
156
  this.history.push({ tool: toolName, decision: 'safety-block', tier, time: Date.now(), reason });
145
157
  this.approvalLog.append({ tool: toolName, args, tier, decision: 'safety_block', scope: 'none', reason });
146
- write(` ${RED}✗${RST} ${DIM}${reason}${RST}\n\n`);
158
+ if (isInputDockMounted()) moveToContent();
159
+ writeSafetyBlock(reason);
147
160
  return { approved: false, tier, reason, blocked: true, code: 'safety_block' };
148
161
  }
149
162
 
@@ -203,25 +216,48 @@ export class ApprovalManager {
203
216
  async _prompt(toolName, args, context = {}) {
204
217
  const tier = context.tier || classifyTier(toolName, args);
205
218
  const why = context.reason || context.why || defaultWhy(tier, toolName, args);
206
- const options = this._optionsFor(tier);
219
+ const options = this._optionsFor(tier, toolName, args);
207
220
 
208
221
  let selected = 0; // arrow-driven cursor
209
222
  let printedHeight = 0;
223
+ let showDetails = false;
224
+
225
+ const isInteractive = process.stdin.isTTY;
210
226
 
211
- // Approval must render ABOVE the fixed input dock (in the scrollable
212
- // content area) so it doesn't collide with the "+ add instruction"
213
- // prompt. Move cursor into content once; the in-place redraw below
214
- // stays within that region because printedHeight tracks the anchor.
215
- if (isInputDockMounted()) moveToContent();
227
+ // In rich TTY mode approval lives in the fixed input dock, replacing
228
+ // "+ add instruction" until the user decides. Fallback/plain mode
229
+ // still renders in the transcript.
230
+ const useDockPrompt = isInteractive && isInputDockMounted();
231
+ if (!useDockPrompt && isInputDockMounted()) moveToContent();
216
232
 
217
233
  // For TTYs we redraw in place on every arrow key so the prompt feels
218
234
  // live. For non-TTYs / pipes we just print once and read a line.
219
- const isInteractive = process.stdin.isTTY;
220
235
  if (!isInteractive) {
221
- write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options }) + '\n');
236
+ write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails }) + '\n');
222
237
  }
223
238
 
224
239
  const drawPrompt = () => {
240
+ if (useDockPrompt) {
241
+ const dock = renderApprovalDockPrompt({
242
+ tool: toolName,
243
+ args,
244
+ tier,
245
+ why,
246
+ selected,
247
+ options,
248
+ showDetails,
249
+ width: process.stderr.columns || process.stdout.columns || 96,
250
+ });
251
+ renderDockOverlay({
252
+ context: dock.context,
253
+ lines: dock.lines || [`${dock.prefix || ''}${dock.value || ''}`],
254
+ meta: dock.meta,
255
+ tips: dock.tips,
256
+ maxRows: dock.maxRows,
257
+ });
258
+ printedHeight = 0;
259
+ return;
260
+ }
225
261
  // Move up over the previous render before re-printing. Use a
226
262
  // bounded per-line clear instead of \x1b[J — a full clear-to-end
227
263
  // would wipe the input dock's bottom rule and tips row that live
@@ -234,7 +270,7 @@ export class ApprovalManager {
234
270
  }
235
271
  write(`\x1b[${printedHeight}F`); // back to the anchor row
236
272
  }
237
- const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options });
273
+ const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails });
238
274
  write(block + '\n');
239
275
  printedHeight = block.split('\n').length;
240
276
  };
@@ -266,6 +302,11 @@ export class ApprovalManager {
266
302
  // Letter shortcut: match against option.key
267
303
  if (typeof k === 'string' && k.length === 1) {
268
304
  const lower = k.toLowerCase();
305
+ if (lower === 'd' && toolName === 'shell') {
306
+ showDetails = !showDetails;
307
+ if (isInteractive) drawPrompt();
308
+ continue;
309
+ }
269
310
  const idx = options.findIndex(o => o.key === lower);
270
311
  if (idx >= 0) {
271
312
  selected = idx;
@@ -278,11 +319,18 @@ export class ApprovalManager {
278
319
  };
279
320
 
280
321
  const value = await choose();
322
+ if (useDockPrompt) {
323
+ clearInputPrompt();
324
+ if (this._approvalPromptEnd) this._approvalPromptEnd();
325
+ moveToContent();
326
+ }
327
+ const prompt = promptForLog(toolName, args, tier, why);
281
328
 
282
329
  switch (value) {
283
330
  case 'approve':
284
331
  this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
285
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once' });
332
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once', prompt });
333
+ writeApprovalConfirmation(toolName, args, 'approved once');
286
334
  return { approved: true, tier };
287
335
 
288
336
  case 'allow-session': {
@@ -290,7 +338,7 @@ export class ApprovalManager {
290
338
  const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'SESSION' });
291
339
  write(` ${GREEN}✓${RST} ${DIM}trusted for this session: ${rule.pattern}${RST}\n\n`);
292
340
  this.history.push({ tool: toolName, decision: 'session-trust', tier, time: Date.now(), rule_id: rule.id });
293
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id });
341
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id, prompt });
294
342
  return { approved: true, tier, scope: 'SESSION', rule_id: rule.id };
295
343
  }
296
344
 
@@ -299,7 +347,7 @@ export class ApprovalManager {
299
347
  const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'PROJECT' });
300
348
  write(` ${GREEN}✓${RST} ${DIM}trusted for this project: ${rule.pattern}${RST}\n\n`);
301
349
  this.history.push({ tool: toolName, decision: 'project-trust', tier, time: Date.now(), rule_id: rule.id });
302
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id });
350
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id, prompt });
303
351
  return { approved: true, tier, scope: 'PROJECT', rule_id: rule.id };
304
352
  }
305
353
 
@@ -308,7 +356,7 @@ export class ApprovalManager {
308
356
  const reason = 'User stopped the command';
309
357
  write(` ${RED}✗${RST} ${DIM}stopped${RST}\n\n`);
310
358
  this.history.push({ tool: toolName, decision: 'no', tier, time: Date.now(), reason });
311
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason });
359
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason, prompt });
312
360
  this._rememberRejection({ tool: toolName, args, tier, decision: 'reject', reason, note: '' });
313
361
  return { approved: false, tier, reason };
314
362
  }
@@ -318,14 +366,15 @@ export class ApprovalManager {
318
366
  this.approveAll = true;
319
367
  write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
320
368
  this.history.push({ tool: toolName, decision: 'approve-all', tier, time: Date.now() });
321
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session' });
369
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session', prompt });
322
370
  return { approved: true, tier };
323
371
 
324
372
  case 'allow-type':
325
373
  if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
326
374
  this.approvedToolTypes.add(toolName);
327
375
  this.history.push({ tool: toolName, decision: 'type-approve', tier, time: Date.now() });
328
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session' });
376
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session', prompt });
377
+ writeApprovalConfirmation(toolName, args, 'always allow');
329
378
  return { approved: true, tier };
330
379
 
331
380
  case 'why':
@@ -340,7 +389,7 @@ export class ApprovalManager {
340
389
  const reason = note ? `User asked to re-plan: ${note}` : 'User asked to re-plan';
341
390
  write(` ${YELLOW}↩${RST} ${DIM}${note ? `re-plan — ${truncateNote(note)}` : 'reject with hint — rework the plan'}${RST}\n\n`);
342
391
  this.history.push({ tool: toolName, decision: 'replan', tier, time: Date.now(), reason });
343
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason });
392
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason, prompt });
344
393
  this._rememberRejection({ tool: toolName, args, tier, decision: 'replan', reason, note });
345
394
  return { approved: false, tier, reason };
346
395
  }
@@ -439,8 +488,8 @@ export class ApprovalManager {
439
488
  });
440
489
  }
441
490
 
442
- _optionsFor(tier) {
443
- return approvalOptions(tier);
491
+ _optionsFor(tier, toolName, args) {
492
+ return approvalOptions(tier, { tool: toolName, args });
444
493
  }
445
494
 
446
495
  getSummary() {
@@ -474,3 +523,102 @@ function truncateNote(note) {
474
523
  const text = String(note || '').trim();
475
524
  return text.length <= 120 ? text : text.slice(0, 119) + '…';
476
525
  }
526
+
527
+ function promptForLog(tool, args, tier, why) {
528
+ return {
529
+ title: `${approvalTitleForLog(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
530
+ subject: approvalSummary(tool, args),
531
+ reason: why || '',
532
+ };
533
+ }
534
+
535
+ function approvalTitleForLog(tier) {
536
+ switch (tier) {
537
+ case TIERS.SENSITIVE_READ:
538
+ case TIERS.SHELL_DANGEROUS:
539
+ case TIERS.DESTRUCTIVE:
540
+ return 'EXPLICIT APPROVAL';
541
+ default:
542
+ return 'APPROVAL';
543
+ }
544
+ }
545
+
546
+ function writeApprovalConfirmation(tool, args, label) {
547
+ if (tool === 'shell') {
548
+ write(` ${GREEN}✓${RST} ${DIM}${label} · shell ${shellApprovalSubject(args)}${RST}\n`);
549
+ return;
550
+ }
551
+ const subject = approvalSummary(tool, args);
552
+ const suffix = subject ? ` · ${truncateNote(subject)}` : '';
553
+ write(` ${GREEN}✓${RST} ${DIM}${label}${suffix}${RST}\n\n`);
554
+ }
555
+
556
+ function shellApprovalSubject(args = {}) {
557
+ const profile = shellCommandProfile(args.command || args.cmd || '');
558
+ const subject = profile.compact ? profile.summary : profile.command;
559
+ return `$ ${truncateNote(subject)}`;
560
+ }
561
+
562
+ function writeSafetyBlock(reason) {
563
+ const cols = Math.max(40, Math.min(process.stderr.columns || process.stdout.columns || 80, 140));
564
+ const firstPrefix = ` ${RED}✗${RST} ${DIM}`;
565
+ const nextPrefix = ` ${DIM}`;
566
+ const firstWidth = Math.max(24, cols - 5);
567
+ const nextWidth = Math.max(24, cols - 5);
568
+ const lines = wrapSafetyText(reason, firstWidth, nextWidth);
569
+
570
+ write(`${firstPrefix}${lines[0] || ''}${RST}\n`);
571
+ for (const line of lines.slice(1)) {
572
+ write(`${nextPrefix}${line}${RST}\n`);
573
+ }
574
+ write('\n');
575
+ }
576
+
577
+ function wrapSafetyText(text, firstWidth, nextWidth) {
578
+ const words = String(text || '').replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
579
+ if (!words.length) return [''];
580
+
581
+ const lines = [];
582
+ let line = '';
583
+ let width = firstWidth;
584
+
585
+ for (const word of words) {
586
+ if (!line) {
587
+ if (word.length <= width) {
588
+ line = word;
589
+ } else {
590
+ const chunks = splitWord(word, width);
591
+ lines.push(...chunks.slice(0, -1));
592
+ line = chunks[chunks.length - 1] || '';
593
+ }
594
+ continue;
595
+ }
596
+
597
+ if (line.length + 1 + word.length <= width) {
598
+ line += ` ${word}`;
599
+ continue;
600
+ }
601
+
602
+ lines.push(line);
603
+ width = nextWidth;
604
+ if (word.length <= width) {
605
+ line = word;
606
+ } else {
607
+ const chunks = splitWord(word, width);
608
+ lines.push(...chunks.slice(0, -1));
609
+ line = chunks[chunks.length - 1] || '';
610
+ }
611
+ }
612
+
613
+ if (line) lines.push(line);
614
+ return lines;
615
+ }
616
+
617
+ function splitWord(word, width) {
618
+ const size = Math.max(8, width);
619
+ const chunks = [];
620
+ for (let i = 0; i < word.length; i += size) {
621
+ chunks.push(word.slice(i, i + size));
622
+ }
623
+ return chunks;
624
+ }
@@ -272,7 +272,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
272
272
  tool_calls: data?.tool_calls || 0,
273
273
  success: data?.success !== false,
274
274
  });
275
- emit({ type: 'sub_agent', ...data });
275
+ emit({ ...data, type: 'sub_agent', agent_type: data?.type || '' });
276
276
  log(`SubAgent done: ${data?.type} (${data?.tool_calls} tools, ${data?.duration_s}s)`);
277
277
  }
278
278
 
@@ -360,8 +360,19 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
360
360
  const subAgentToolBreakdown = countBreakdown(toolCalls.filter(t => t.internal));
361
361
 
362
362
  const subAgentReportedToolCount = subAgents.reduce((sum, sa) => sum + (sa.tool_calls || 0), 0);
363
- const subAgentToolCount = backendSubAgentToolCount ?? Math.max(subAgentReportedToolCount, subAgentForwardedToolCount);
364
- const totalToolCount = backendToolCount ?? (primaryToolCount + subAgentToolCount);
363
+ const observedSubAgentToolCount = Math.max(
364
+ subAgentReportedToolCount,
365
+ subAgentForwardedToolCount,
366
+ );
367
+ const subAgentToolCount = (
368
+ backendSubAgentToolCount && backendSubAgentToolCount > 0
369
+ )
370
+ ? backendSubAgentToolCount
371
+ : observedSubAgentToolCount;
372
+ const computedToolCount = primaryToolCount + subAgentToolCount;
373
+ const totalToolCount = backendToolCount != null
374
+ ? Math.max(backendToolCount, computedToolCount)
375
+ : computedToolCount;
365
376
  const primaryToolTotal = backendPrimaryToolCount ?? (
366
377
  backendToolCount != null
367
378
  ? Math.max(0, totalToolCount - subAgentToolCount)
@@ -21,11 +21,11 @@ const MAX_ITERATIONS = 50;
21
21
  const TOOL_SCHEMAS = [
22
22
  {
23
23
  name: 'shell',
24
- description: 'Run a shell command and return stdout/stderr. Use for: installing packages, running tests, builds, git operations, or any terminal command.',
24
+ description: 'Run one non-interactive shell command and return stdout/stderr. Do not use command substitution, backticks, or $(); run separate simple shell calls instead.',
25
25
  input_schema: {
26
26
  type: 'object',
27
27
  properties: {
28
- command: { type: 'string', description: 'The shell command to execute' },
28
+ command: { type: 'string', description: 'The command to execute. Avoid backticks and $(); split dependent checks into separate commands.' },
29
29
  timeout: { type: 'number', description: 'Timeout in milliseconds (default: 120000)' },
30
30
  },
31
31
  required: ['command'],
@@ -478,6 +478,7 @@ export class LocalAgent {
478
478
  'You are Bahulam Code, Bahulam\'s AI coding agent running in local mode.',
479
479
  'You have access to tools for reading, writing, and executing code.',
480
480
  'Use tools to accomplish the user\'s request. Be concise and direct.',
481
+ 'For shell tools, never use command substitution, backticks, or $(). Run separate simple commands and carry values forward in text.',
481
482
  ];
482
483
  if (context.cwd) parts.push(`Working directory: ${context.cwd}`);
483
484
  if (context.gitBranch) parts.push(`Git branch: ${context.gitBranch}`);
@@ -74,6 +74,14 @@ export function createToolExecutor({
74
74
  return path.resolve(cwd, value);
75
75
  }
76
76
 
77
+ function blockedShellOutput(reason) {
78
+ const text = String(reason || 'Blocked by shell safety policy').trim();
79
+ const hint = /command substitution|backticks|\$\(\)/i.test(text)
80
+ ? 'Retry with separate simple shell commands instead of backticks or $().'
81
+ : 'Work only inside a registered project root.';
82
+ return `BLOCKED: ${text}. ${hint}`;
83
+ }
84
+
77
85
  function longRunningObservationTimeoutMs() {
78
86
  const configured = Number(process.env.KEPLER_LONG_RUNNING_TIMEOUT_MS);
79
87
  return Number.isFinite(configured) && configured > 0 ? configured : 15_000;
@@ -578,7 +586,7 @@ export function createToolExecutor({
578
586
  if (!shellCheck.safe) {
579
587
  return {
580
588
  success: false,
581
- output: `BLOCKED: ${shellCheck.reason}. Work only inside a registered project root.`,
589
+ output: blockedShellOutput(shellCheck.reason),
582
590
  _tool: 'shell', _blocked: true,
583
591
  };
584
592
  }
@@ -588,7 +596,7 @@ export function createToolExecutor({
588
596
  if (classification.classification === 'blocked') {
589
597
  return {
590
598
  success: false,
591
- output: `BLOCKED: ${classification.reason}`,
599
+ output: blockedShellOutput(classification.reason),
592
600
  _tool: 'shell', _blocked: true,
593
601
  };
594
602
  }
@@ -1513,7 +1521,9 @@ print('OK: replaced')
1513
1521
  const created = listLocalAgents(process.cwd()).find(agent => agent.slug === result.slug);
1514
1522
  const payload = {
1515
1523
  ...result,
1516
- agent: created ? compactAgentMetadata(created) : null,
1524
+ agent: created
1525
+ ? { ...compactAgentMetadata(created), spec: created.spec }
1526
+ : null,
1517
1527
  next_actions: [
1518
1528
  `Edit ${result.filePath}`,
1519
1529
  `Run /agents sync ${result.slug} when ready`,
package/src/index.mjs CHANGED
@@ -35,7 +35,7 @@ import { printBanner, printProjectInfo, printHints, printAuthStatus, printStyled
35
35
  import { ContextRetriever } from './context/retriever.mjs';
36
36
  import { loadSettings } from './config/settings.mjs';
37
37
 
38
- const VERSION = '2.6.12';
38
+ const VERSION = '2.6.14';
39
39
 
40
40
  // ── Arg Parsing (consolidated from index.mjs + cli-args.mjs) ──
41
41