@cydm/pie 2.7.6 → 2.7.8

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.
@@ -6,10 +6,10 @@ import {
6
6
  createSharedWebSearchTool,
7
7
  interpretShellExit,
8
8
  requestInteraction
9
- } from "./chunk-YWWLRZSG.js";
9
+ } from "./chunk-P22SYVGO.js";
10
10
  import {
11
11
  getLogger
12
- } from "./chunk-ZF5FHTES.js";
12
+ } from "./chunk-2FEDB5GR.js";
13
13
  import {
14
14
  Type
15
15
  } from "./chunk-XXGYUYXI.js";
@@ -188,7 +188,7 @@ function createTraceTransportSink(noteRuntimeEvent) {
188
188
  }
189
189
 
190
190
  // src/capabilities/bash/index.ts
191
- import { spawn } from "child_process";
191
+ import { spawn, spawnSync } from "child_process";
192
192
  import { closeSync, createWriteStream, existsSync as existsSync2, openSync, statSync, unlinkSync, writeFileSync } from "fs";
193
193
  import { tmpdir } from "os";
194
194
  import { join as join2, resolve as resolve2 } from "path";
@@ -316,6 +316,30 @@ function truncateStringToBytesFromEnd(str, maxBytes) {
316
316
  return buf.slice(start).toString("utf-8");
317
317
  }
318
318
 
319
+ // src/capabilities/bash/contract.ts
320
+ var WINDOWS_DIALECT_BLOCK = `On this host shell:auto runs Windows PowerShell 5.1 (powershell.exe). Write PowerShell syntax: separate statements with ';' (PS 5.1 has no && or || \u2014 chain with "if ($?) { ... }" or check $LASTEXITCODE after native commands). Nested quotes in inline "node -e" or "-Command" strings are fragile; write a temp script file with write_file and execute it instead. Heredocs do not exist; multi-line commands work, but long scripts run more reliably from a file. Judge outcomes by the exit code; stderr may carry PowerShell-generated records such as CLIXML. shell:"bash" runs the first real bash on PATH (Git Bash when installed) and fails with an actionable error when only the WSL stub exists; shell:"cmd" runs cmd.exe.`;
321
+ var POSIX_DIALECT_BLOCK = 'On this host shell:auto runs bash (bash -c). POSIX syntax applies: && and || chaining, heredocs, and standard quoting rules are available. shell:"powershell" uses pwsh when installed; shell:"cmd" is Windows-only.';
322
+ function buildShellToolContract(platform = process.platform) {
323
+ if (platform === "win32") {
324
+ return {
325
+ dialect: "powershell51",
326
+ defaultShell: "powershell",
327
+ name: "shell",
328
+ label: "powershell",
329
+ dialectBlock: WINDOWS_DIALECT_BLOCK,
330
+ commandParamDescription: "The PowerShell command to execute by default (Windows PowerShell 5.1). If shell is overridden, use that shell's syntax."
331
+ };
332
+ }
333
+ return {
334
+ dialect: "bash",
335
+ defaultShell: "bash",
336
+ name: "shell",
337
+ label: "bash",
338
+ dialectBlock: POSIX_DIALECT_BLOCK,
339
+ commandParamDescription: "The bash command to execute by default. If shell is overridden, use that shell's syntax."
340
+ };
341
+ }
342
+
319
343
  // src/capabilities/bash/artifact-sink.ts
320
344
  function createArtifactSinkGate(sink, isActive) {
321
345
  const sources = /* @__PURE__ */ new Set();
@@ -353,40 +377,198 @@ function createArtifactSinkGate(sink, isActive) {
353
377
  };
354
378
  }
355
379
 
380
+ // src/capabilities/bash/powershell-display.ts
381
+ import { StringDecoder } from "node:string_decoder";
382
+ var MARKER = "#< CLIXML";
383
+ var MAX_CLIXML_CHARS = 64 * 1024;
384
+ function xmlText(text) {
385
+ if (/[<>]|\]\]>/.test(text)) return void 0;
386
+ let valid = true;
387
+ const decoded = text.replace(/&([^;]{0,10});|&/g, (whole, entity) => {
388
+ const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
389
+ if (entity && Object.hasOwn(named, entity)) return named[entity];
390
+ const n = entity && /^#(?:[0-9]+|x[0-9a-fA-F]+)$/.test(entity) ? Number.parseInt(entity.slice(entity[1] === "x" ? 2 : 1), entity[1] === "x" ? 16 : 10) : NaN;
391
+ if (!Number.isInteger(n) || !(n === 9 || n === 10 || n === 13 || n >= 32 && n <= 55295 || n >= 57344 && n <= 65533 || n >= 65536 && n <= 1114111)) {
392
+ valid = false;
393
+ return whole;
394
+ }
395
+ return String.fromCodePoint(n);
396
+ });
397
+ if (!valid || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\ufffe\uffff]/.test(decoded)) return void 0;
398
+ return decoded.replace(/_x([0-9a-fA-F]{4})_/g, (whole, hex) => {
399
+ const n = Number.parseInt(hex, 16);
400
+ return n === 9 || n === 10 || n === 13 || n >= 32 && n < 127 || n >= 160 && n < 55296 || n >= 57344 && n < 65534 ? String.fromCharCode(n) : whole;
401
+ });
402
+ }
403
+ function unpack(xml) {
404
+ let pos = 0;
405
+ const take = (pattern) => {
406
+ pattern.lastIndex = pos;
407
+ const match = pattern.exec(xml);
408
+ if (match) pos = pattern.lastIndex;
409
+ return match;
410
+ };
411
+ if (!take(/\s*<Objs/y)) return void 0;
412
+ const attrs = /* @__PURE__ */ new Set();
413
+ while (true) {
414
+ const attr = take(/\s+(Version|xmlns)=(?:"([^"<>]*)"|'([^'<>]*)')/y);
415
+ if (!attr) break;
416
+ if (attrs.has(attr[1])) return void 0;
417
+ attrs.add(attr[1]);
418
+ const value = attr[2] ?? attr[3];
419
+ if (attr[1] === "Version" ? value !== "1.1.0.1" : value !== "http://schemas.microsoft.com/powershell/2004/04") return void 0;
420
+ }
421
+ if (!take(/\s*>/y)) return void 0;
422
+ const records = [];
423
+ while (true) {
424
+ if (take(/\s*<\/Objs>/y)) return pos === xml.length && records.length > 0 ? records.join("") : void 0;
425
+ const record = take(/\s*<S\s+S=(?:"(Error|Warning|Verbose|Debug|Information|Output|Progress)"|'(Error|Warning|Verbose|Debug|Information|Output|Progress)')\s*>([^<]*)<\/S>/y);
426
+ if (!record) return void 0;
427
+ const text = xmlText(record[3]);
428
+ if (text === void 0) return void 0;
429
+ records.push(text);
430
+ }
431
+ }
432
+ var PowerShellStderrNormalizer = class {
433
+ constructor(emit) {
434
+ this.emit = emit;
435
+ }
436
+ emit;
437
+ decoder = new StringDecoder("utf8");
438
+ pending = "";
439
+ block = false;
440
+ endTagTail = "";
441
+ lineStart = true;
442
+ disabled = false;
443
+ normalizedBlocks = 0;
444
+ write(data) {
445
+ this.accept(this.decoder.write(data));
446
+ }
447
+ end() {
448
+ this.accept(this.decoder.end());
449
+ this.emit(this.pending);
450
+ this.pending = "";
451
+ }
452
+ accept(text) {
453
+ let plain = "";
454
+ const flush = () => {
455
+ if (plain) this.emit(plain);
456
+ plain = "";
457
+ };
458
+ for (const char of text) {
459
+ if (this.disabled) {
460
+ plain += char;
461
+ continue;
462
+ }
463
+ if (this.block) {
464
+ this.pending += char;
465
+ this.endTagTail = (this.endTagTail + char).slice(-7);
466
+ if (this.pending.length > MAX_CLIXML_CHARS) {
467
+ plain += this.pending;
468
+ this.pending = "";
469
+ this.block = false;
470
+ this.disabled = true;
471
+ } else if (this.endTagTail === "</Objs>") {
472
+ const newline = this.pending.indexOf("\n");
473
+ const decoded = unpack(this.pending.slice(newline + 1));
474
+ flush();
475
+ if (decoded !== void 0) {
476
+ this.normalizedBlocks++;
477
+ this.emit(decoded);
478
+ } else this.emit(this.pending);
479
+ this.pending = "";
480
+ this.block = false;
481
+ this.endTagTail = "";
482
+ this.lineStart = false;
483
+ }
484
+ continue;
485
+ }
486
+ if (this.lineStart) {
487
+ this.pending += char;
488
+ if (this.pending === MARKER + "\n" || this.pending === MARKER + "\r\n") {
489
+ flush();
490
+ this.block = true;
491
+ this.endTagTail = "";
492
+ continue;
493
+ }
494
+ if ((MARKER + "\n").startsWith(this.pending) || (MARKER + "\r\n").startsWith(this.pending)) continue;
495
+ plain += this.pending;
496
+ this.pending = "";
497
+ this.lineStart = char === "\n";
498
+ } else {
499
+ plain += char;
500
+ this.lineStart = char === "\n";
501
+ }
502
+ }
503
+ flush();
504
+ }
505
+ };
506
+ var PowerShellDisplay = class {
507
+ head = "";
508
+ tail = "";
509
+ total = 0;
510
+ totalBytes = 0;
511
+ newlines = 0;
512
+ stdout = new StringDecoder("utf8");
513
+ stderr = new PowerShellStderrNormalizer((text) => this.append(text));
514
+ append(text) {
515
+ const cap = DEFAULT_MAX_BYTES;
516
+ this.total += text.length;
517
+ this.totalBytes += Buffer.byteLength(text, "utf8");
518
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) this.newlines++;
519
+ if (this.head.length < cap) this.head += text.slice(0, cap - this.head.length);
520
+ this.tail = (this.tail + text).slice(-cap);
521
+ }
522
+ writeStdout(data) {
523
+ this.append(this.stdout.write(data));
524
+ }
525
+ end() {
526
+ this.append(this.stdout.end());
527
+ this.stderr.end();
528
+ }
529
+ preview() {
530
+ const result = truncateHeadTail(this.total <= this.head.length ? this.head : this.total <= this.head.length + this.tail.length ? this.head + this.tail.slice(this.head.length + this.tail.length - this.total) : this.head + "\n... [stream middle omitted] ...\n" + this.tail);
531
+ if (this.totalBytes > result.totalBytes) {
532
+ result.truncated = true;
533
+ result.totalBytes = this.totalBytes;
534
+ result.totalLines = this.newlines + 1;
535
+ result.truncatedBy = result.totalLines > DEFAULT_MAX_LINES ? "lines" : "bytes";
536
+ }
537
+ return result;
538
+ }
539
+ };
540
+
356
541
  // src/capabilities/bash/index.ts
357
- var bashSchema = Type.Object({
358
- command: Type.String({ description: "The bash command to execute" }),
359
- timeout: Type.Optional(
360
- Type.Number({
361
- description: "Timeout in seconds. Default: 120s (2 minutes). Longer timeouts are honored for installs, builds, and tests; generic custom scripts and polling commands are capped to protect the turn budget."
362
- })
363
- ),
364
- cwd: Type.Optional(Type.String({ description: "Working directory for the command" })),
365
- shell: Type.Optional(Type.Union([
366
- Type.Literal("auto"),
367
- Type.Literal("bash"),
368
- Type.Literal("powershell"),
369
- Type.Literal("cmd")
370
- ], { description: "Shell to use. Defaults to bash on macOS/Linux and PowerShell on Windows. Use this only for platform-specific commands." }))
371
- });
542
+ function buildBashSchema(commandDescription) {
543
+ return Type.Object({
544
+ command: Type.String({ description: commandDescription }),
545
+ timeout: Type.Optional(
546
+ Type.Number({
547
+ exclusiveMinimum: 0,
548
+ maximum: 2147483,
549
+ description: "Timeout in seconds. Default: 120s (2 minutes). The requested value is honored as-is for every command; set it to what the task needs (installs, builds, and tests often need more)."
550
+ })
551
+ ),
552
+ cwd: Type.Optional(Type.String({ description: "Working directory for the command" })),
553
+ shell: Type.Optional(Type.Union([
554
+ Type.Literal("auto"),
555
+ Type.Literal("bash"),
556
+ Type.Literal("powershell"),
557
+ Type.Literal("cmd")
558
+ ], { description: "Shell to use. Defaults to bash on macOS/Linux and PowerShell on Windows. Use this only for platform-specific commands." }))
559
+ });
560
+ }
372
561
  var DEFAULT_TIMEOUT_SECONDS = 120;
373
- var MAX_GENERIC_TIMEOUT_SECONDS = 120;
374
562
  var OUTPUT_DRAIN_TIMEOUT_MS = 1e3;
375
563
  var MAX_SHELL_EXIT_DRAIN_MS = 1e4;
376
- function resolveTimeoutPolicy(requestedTimeout, commandClass) {
564
+ function resolveTimeoutPolicy(requestedTimeout) {
377
565
  const requestedSeconds = requestedTimeout ?? DEFAULT_TIMEOUT_SECONDS;
378
- if (requestedSeconds > MAX_GENERIC_TIMEOUT_SECONDS && commandClass !== "package_manager" && commandClass !== "test_or_build") {
379
- return {
380
- requestedSeconds,
381
- effectiveSeconds: MAX_GENERIC_TIMEOUT_SECONDS,
382
- capped: true,
383
- reason: `long timeouts are capped at ${MAX_GENERIC_TIMEOUT_SECONDS}s for non-build commands`
384
- };
566
+ if (!Number.isFinite(requestedSeconds) || requestedSeconds <= 0 || requestedSeconds > 2147483) {
567
+ throw new Error("Invalid shell timeout: use a finite positive number of seconds within the supported timer range (2147483s).");
385
568
  }
386
569
  return {
387
570
  requestedSeconds,
388
- effectiveSeconds: requestedSeconds,
389
- capped: false
571
+ effectiveSeconds: requestedSeconds
390
572
  };
391
573
  }
392
574
  function validateBashCommand(command) {
@@ -396,10 +578,10 @@ function validateBashCommand(command) {
396
578
  throw new Error("Command cannot be empty. Pass a real shell command in the command argument.");
397
579
  }
398
580
  if (/^:\s/.test(trimmed) || /^:\s*(?:\d+[\.)]?)?\s*$/.test(trimmed) || /^:\s*\d+[\.)]/.test(trimmed)) {
399
- throw new Error(`Invalid bash command "${value}". Do not pass a no-op, numbered plan item, or plan fragment; pass the actual shell command to run.`);
581
+ throw new Error(`Invalid shell command "${value}". Do not pass a no-op, numbered plan item, or plan fragment; pass the actual shell command to run.`);
400
582
  }
401
583
  if (trimmed.includes("<|tool")) {
402
- throw new Error("Invalid bash command. The command argument must be only a real shell command, not tool-call markup.");
584
+ throw new Error("Invalid shell command. The command argument must be only a real shell command, not tool-call markup.");
403
585
  }
404
586
  }
405
587
  function buildEffectiveCommand(command, options, shell) {
@@ -413,21 +595,80 @@ function buildEffectiveCommand(command, options, shell) {
413
595
  }
414
596
  return `${prefix} && ${command}`;
415
597
  }
416
- function resolveShell(shell) {
598
+ function resolveShell(shell, contract) {
417
599
  if (!shell || shell === "auto") {
418
- return process.platform === "win32" ? "powershell" : "bash";
600
+ return contract.defaultShell;
419
601
  }
420
602
  return shell;
421
603
  }
604
+ function buildBashDescription(contract) {
605
+ return [
606
+ "Execute a shell command in the current working directory.",
607
+ contract.dialectBlock,
608
+ `Returns stdout and stderr as a bounded head/tail preview of at most ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. If truncated, the result includes a private full-output artifact path. Default timeout: 120s. The timeout parameter is the budget: pass a larger value when a task legitimately needs it, and keep long waits focused rather than polling loops.`
609
+ ].join(" ");
610
+ }
422
611
  function buildSpawnCommand(shell, command) {
423
612
  if (shell === "powershell") {
424
- const utf8Command = `[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding; $OutputEncoding = [Console]::OutputEncoding; ${command}`;
425
- return { file: process.platform === "win32" ? "powershell.exe" : "pwsh", args: ["-NoProfile", "-NonInteractive", "-Command", utf8Command], shell: false };
613
+ const childScript = [
614
+ "$ProgressPreference = 'SilentlyContinue'",
615
+ "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding; $OutputEncoding = [Console]::OutputEncoding",
616
+ command,
617
+ "if (($null -ne $LASTEXITCODE) -and ($LASTEXITCODE -ne 0)) { exit $LASTEXITCODE }"
618
+ ].join("\n");
619
+ const script = [
620
+ "$ProgressPreference = 'SilentlyContinue'",
621
+ `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(childScript, "utf16le").toString("base64")}`,
622
+ "$__pieCode = $LASTEXITCODE; $__pieOk = $?",
623
+ "if (-not $__pieOk -and (($null -eq $__pieCode) -or ($__pieCode -eq 0))) { exit 1 }",
624
+ "if (($null -ne $__pieCode) -and ($__pieCode -ne 0)) { exit $__pieCode }",
625
+ "exit 0"
626
+ ].join("\n");
627
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
628
+ return { file: process.platform === "win32" ? "powershell.exe" : "pwsh", args: ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], shell: false };
426
629
  }
427
630
  if (shell === "cmd") {
428
631
  return { file: "cmd.exe", args: ["/d", "/s", "/c", command], shell: false };
429
632
  }
430
- return { file: process.platform === "win32" ? "bash.exe" : "bash", args: ["-c", command], shell: false };
633
+ if (process.platform === "win32") {
634
+ const bashPath = resolveWindowsBashPath();
635
+ if (!bashPath) {
636
+ throw new Error(
637
+ "No usable bash found on Windows: PATH only offers the WSL store stub (WindowsApps\\bash.exe). Install Git Bash (https://git-scm.com) or WSL, or use shell:auto (PowerShell)."
638
+ );
639
+ }
640
+ return { file: bashPath, args: ["-c", command], shell: false };
641
+ }
642
+ return { file: "bash", args: ["-c", command], shell: false };
643
+ }
644
+ var windowsBashPathCache;
645
+ function resolveWindowsBashPath() {
646
+ if (windowsBashPathCache !== void 0) return windowsBashPathCache;
647
+ let resolved = null;
648
+ try {
649
+ const result = spawnSync("cmd.exe", ["/d", "/c", "where", "bash"], { windowsHide: true, encoding: "utf8", timeout: 5e3 });
650
+ const lines = (result.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
651
+ const real = lines.find((line) => !isWindowsAppsPath(line));
652
+ resolved = real ?? null;
653
+ } catch {
654
+ resolved = null;
655
+ }
656
+ if (!resolved) {
657
+ const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]].filter((value) => Boolean(value));
658
+ for (const root of programFiles) {
659
+ const candidate = join2(root, "Git", "bin", "bash.exe");
660
+ if (existsSync2(candidate)) {
661
+ resolved = candidate;
662
+ break;
663
+ }
664
+ }
665
+ }
666
+ windowsBashPathCache = resolved;
667
+ return resolved;
668
+ }
669
+ function isWindowsAppsPath(p) {
670
+ const normalized = p.replace(/\\/g, "/").toLowerCase();
671
+ return normalized.includes("/windowsapps/");
431
672
  }
432
673
  async function requestHighRiskConfirmation(request) {
433
674
  const response = await requestInteraction({
@@ -633,6 +874,7 @@ function killProcessTree(pid, leaderExited) {
633
874
  }
634
875
  }
635
876
  function createBashTool(defaultCwd, options) {
877
+ const contract = buildShellToolContract(options?.platform);
636
878
  const recentCommands = /* @__PURE__ */ new Map();
637
879
  const rememberCommandResult = (key, fingerprint) => {
638
880
  const previous = recentCommands.get(key);
@@ -646,13 +888,28 @@ function createBashTool(defaultCwd, options) {
646
888
  return { count, sameResult };
647
889
  };
648
890
  return {
649
- name: "bash",
650
- label: "bash",
651
- description: `Execute a shell command in the current working directory. The public tool name remains bash for compatibility; on Windows shell:auto uses PowerShell, otherwise bash. Returns stdout and stderr as a bounded head/tail preview of at most ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. If truncated, the result includes a private full-output artifact path. Default timeout: 120s. Long timeouts are reserved for installs, builds, and tests; custom scripts, searches, and polling commands are capped at ${MAX_GENERIC_TIMEOUT_SECONDS}s and should be split into bounded steps with progress output.`,
652
- parameters: bashSchema,
891
+ name: contract.name,
892
+ label: contract.label,
893
+ description: buildBashDescription(contract),
894
+ parameters: buildBashSchema(contract.commandParamDescription),
895
+ // Policy identity travels with the tool (single source of truth, so a
896
+ // host may rename it without touching this layer's registries); the
897
+ // shared BUILTIN_TOOL_CAPABILITY_METADATA["bash"] entry stays as the
898
+ // name-keyed fallback for legacy sessions and name-only test doubles.
899
+ capabilityMetadata: {
900
+ riskClass: "shell",
901
+ concurrencySafe: false,
902
+ requiresPermission: true,
903
+ permissionScope: "shell",
904
+ executesShell: true,
905
+ availableInPlanMode: true,
906
+ allowedForSubagentByDefault: true,
907
+ highRisk: true,
908
+ maxOutputChars: 1e5
909
+ },
653
910
  execute: async (_toolCallId, { command, timeout, cwd, shell }, signal, onUpdate) => {
654
911
  validateBashCommand(command);
655
- const resolvedShell = resolveShell(shell);
912
+ const resolvedShell = resolveShell(shell, contract);
656
913
  const effectiveCommand = buildEffectiveCommand(command, options, resolvedShell);
657
914
  const workingDir = normalizeWorkingDirectory(cwd, defaultCwd);
658
915
  validateWorkingDirectory(workingDir);
@@ -717,7 +974,7 @@ function createBashTool(defaultCwd, options) {
717
974
  }
718
975
  });
719
976
  }
720
- const timeoutPolicy = resolveTimeoutPolicy(timeout, risk.commandClass);
977
+ const timeoutPolicy = resolveTimeoutPolicy(timeout);
721
978
  const timeoutMs = timeoutPolicy.effectiveSeconds * 1e3;
722
979
  return new Promise((resolve3, reject) => {
723
980
  if (signal?.aborted) {
@@ -729,6 +986,7 @@ function createBashTool(defaultCwd, options) {
729
986
  let tempFileSink;
730
987
  let settled = false;
731
988
  let totalBytes = 0;
989
+ const display = resolvedShell === "powershell" ? new PowerShellDisplay() : void 0;
732
990
  const chunks = [];
733
991
  let chunksBytes = 0;
734
992
  const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
@@ -854,10 +1112,9 @@ function createBashTool(defaultCwd, options) {
854
1112
  }
855
1113
  }
856
1114
  appendRollingChunk(data);
1115
+ display?.writeStdout(data);
857
1116
  if (onUpdate) {
858
- const fullBuffer = Buffer.concat(chunks);
859
- const fullText = fullBuffer.toString("utf-8");
860
- const truncation = truncateHeadTail(fullText);
1117
+ const truncation = display?.preview() ?? truncateHeadTail(Buffer.concat(chunks).toString("utf-8"));
861
1118
  onUpdate({
862
1119
  content: [{ type: "text", text: truncation.content || "" }],
863
1120
  details: {
@@ -908,6 +1165,15 @@ function createBashTool(defaultCwd, options) {
908
1165
  }
909
1166
  }
910
1167
  appendRollingChunk(data);
1168
+ display?.stderr.write(data);
1169
+ if (display?.stderr.normalizedBlocks && !tempFilePath) {
1170
+ try {
1171
+ const sink = ensureOutputArtifact();
1172
+ for (const chunk of chunks) sink.write(chunk);
1173
+ } catch (error) {
1174
+ failExecution(error instanceof Error ? error : new Error(String(error)));
1175
+ }
1176
+ }
911
1177
  });
912
1178
  const finishExecution = async (code, sig, stdioIncomplete = false) => {
913
1179
  if (settled) return;
@@ -924,8 +1190,9 @@ function createBashTool(defaultCwd, options) {
924
1190
  ... [stream middle omitted] ...
925
1191
  ${fullOutput}`;
926
1192
  }
927
- const truncation = truncateHeadTail(fullOutput);
928
- if (totalBytes > truncation.totalBytes) {
1193
+ display?.end();
1194
+ const truncation = display?.preview() ?? truncateHeadTail(fullOutput);
1195
+ if (!display && totalBytes > truncation.totalBytes) {
929
1196
  truncation.truncated = true;
930
1197
  truncation.totalBytes = totalBytes;
931
1198
  truncation.totalLines = totalNewlines + 1;
@@ -938,7 +1205,7 @@ ${fullOutput}`;
938
1205
  tempFilePath = path3;
939
1206
  retainedOutputArtifacts.add(path3);
940
1207
  try {
941
- writeFileSync(fd, fullOutput, "utf-8");
1208
+ writeFileSync(fd, fullBuffer);
942
1209
  } finally {
943
1210
  closeSync(fd);
944
1211
  }
@@ -998,20 +1265,22 @@ ${commandNonzeroHint}`;
998
1265
  [Showing a bounded head/tail preview of ${truncation.totalLines} lines (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
999
1266
  }
1000
1267
  }
1268
+ if (display?.stderr.normalizedBlocks) {
1269
+ details.outputNormalization = { kind: "powershell-clixml-text", blocks: display.stderr.normalizedBlocks, rawEvidence: true };
1270
+ details.fullOutputPath = tempFilePath;
1271
+ outputText += `
1272
+
1273
+ [PowerShell CLIXML text records unpacked for display (limited support: complex/unknown records remain raw). Raw stdout/stderr evidence in observed capture order: ${tempFilePath}]`;
1274
+ }
1001
1275
  if (timedOut) {
1002
1276
  outputText += `
1003
1277
 
1004
1278
  [Command timed out after ${timeoutPolicy.effectiveSeconds} seconds]`;
1005
- outputText += "\n[Timeout hint: split custom searches or service polls into smaller bounded steps with progress output, inspect partial results/logs, then make a decision. Avoid repeating long waits. Reserve long timeouts for installs, builds, and tests.]";
1279
+ outputText += "\n[Timeout hint: the command ran longer than its requested budget. Inspect partial results/logs, then either raise the timeout to what the task needs or split long waits into smaller focused steps. Avoid repeating identical long waits.]";
1006
1280
  }
1007
1281
  if (stdioIncomplete && !timedOut) {
1008
1282
  outputText += "\n\n[Output capture ended after the command exited because a descendant kept stdio open or the output artifact was still draining (bounded wait). Captured output may be incomplete.]";
1009
1283
  }
1010
- if (timeoutPolicy.capped) {
1011
- outputText += `
1012
-
1013
- [Timeout policy: requested ${timeoutPolicy.requestedSeconds} seconds, capped to ${timeoutPolicy.effectiveSeconds} seconds because ${timeoutPolicy.reason}.]`;
1014
- }
1015
1284
  const recoveryHint = buildBashRecoveryHint(command, outputText);
1016
1285
  if (recoveryHint) {
1017
1286
  outputText += `
@@ -1046,7 +1315,7 @@ ${commandNonzeroHint}`;
1046
1315
  void finishExecution(code, sig);
1047
1316
  });
1048
1317
  child.on("error", (err) => {
1049
- failExecution(new Error(`Failed to start bash command in ${workingDir}: ${err.message}. Check that the command is executable and cwd exists.`));
1318
+ failExecution(new Error(`Failed to start shell command in ${workingDir}: ${err.message}. Check that the command is executable and cwd exists.`));
1050
1319
  });
1051
1320
  });
1052
1321
  }
@@ -1292,7 +1561,7 @@ function createCliHostCapabilities(cwd, options = {}) {
1292
1561
  });
1293
1562
  const capabilities = [
1294
1563
  {
1295
- id: "bash",
1564
+ id: "shell",
1296
1565
  description: "Node/TUI host capability for executing shell commands in the local workspace.",
1297
1566
  tools: [bashTool]
1298
1567
  },