steward-cli 0.13.0 → 0.15.0

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.
Files changed (3) hide show
  1. package/README.md +1 -14
  2. package/dist/cli.js +317 -219
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Steward
2
2
 
3
- [![Version](https://img.shields.io/badge/version-v0.13.0-D77757.svg)](https://github.com/sapirrior/steward/releases)
3
+ [![Version](https://img.shields.io/badge/version-v0.15.0-D77757.svg)](https://github.com/sapirrior/steward/releases)
4
4
  [![npm](https://img.shields.io/npm/v/steward-cli.svg?color=373737)](https://www.npmjs.com/package/steward-cli)
5
5
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
  [![Runtime](https://img.shields.io/badge/runtime-Bun-fbf0df.svg?logo=bun)](https://bun.sh)
@@ -10,19 +10,6 @@ Steward is an interactive AI engineering assistant for the terminal. Built with
10
10
 
11
11
  ---
12
12
 
13
- ```text
14
- ▄▄▄▄▄ Steward
15
- ▀▙███▟▀ AI engineering assistant for your terminal
16
- ▘▘ ▝▝
17
-
18
- ────────────────────────────────────────────────────────────────────────────────
19
- > Refactor the authentication service to use JWT refresh token rotation
20
- ────────────────────────────────────────────────────────────────────────────────
21
- ? for shortcuts claude-3-7-sonnet
22
- ```
23
-
24
- ---
25
-
26
13
  ## Get Started
27
14
 
28
15
  ### Installation
package/dist/cli.js CHANGED
@@ -42842,7 +42842,7 @@ function getTheme() {
42842
42842
  return darkTheme;
42843
42843
  }
42844
42844
  // src/theme/logo.ts
42845
- var LOGO_LINES = [" ▄▄▄▄▄ ", "▀▙███▟▀", " ▘▘ ▝▝ "];
42845
+ var LOGO_LINES = [" ▄▄▄▄▄ ", "▀▙███▟▀", " ▝ ▘ "];
42846
42846
  var LOGO_WIDTH = Math.max(...LOGO_LINES.map((line) => stringWidth(line)));
42847
42847
  // node_modules/marked/lib/marked.esm.js
42848
42848
  function A() {
@@ -52973,7 +52973,7 @@ class TerminalEngine {
52973
52973
  }
52974
52974
 
52975
52975
  // src/engine/agent-session.ts
52976
- import { randomUUID as randomUUID8 } from "node:crypto";
52976
+ import { randomUUID as randomUUID7 } from "node:crypto";
52977
52977
  // src/session/helpers.ts
52978
52978
  function formatToolOutputSummary(res, isError = false) {
52979
52979
  if (isError || res === undefined || res === null) {
@@ -70955,7 +70955,7 @@ class ToolCatalog {
70955
70955
  for (const [name, def] of this.tools.entries()) {
70956
70956
  aiTools[name] = tool({
70957
70957
  description: def.description,
70958
- parameters: def.parameters,
70958
+ inputSchema: def.parameters,
70959
70959
  execute: async (args) => {
70960
70960
  return def.execute(args, context);
70961
70961
  }
@@ -70967,13 +70967,52 @@ class ToolCatalog {
70967
70967
  var defaultToolCatalog = new ToolCatalog;
70968
70968
 
70969
70969
  // src/tools/read-file/index.ts
70970
- import { existsSync as existsSync9, lstatSync as lstatSync3, readFileSync as readFileSync8 } from "node:fs";
70971
- import { isAbsolute as isAbsolute2, resolve as resolve5 } from "node:path";
70970
+ import { existsSync as existsSync9, lstatSync as lstatSync3, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
70971
+ import { isAbsolute as isAbsolute2, resolve as resolve5, sep as sep3 } from "node:path";
70972
70972
  var readFileInputSchema = object({
70973
70973
  path: string2().describe("The path of the file to read (relative to current working directory or absolute)."),
70974
70974
  offset: number2().int().min(1).optional().describe("The line number to start reading from (1-indexed). Defaults to 1."),
70975
70975
  limit: number2().int().min(1).max(2000).optional().describe("The maximum number of lines to read. Defaults to 200.")
70976
70976
  });
70977
+ function isPathInside2(targetPath, basePath) {
70978
+ const normTarget = resolve5(targetPath);
70979
+ const normBase = resolve5(basePath);
70980
+ if (normTarget === normBase) {
70981
+ return true;
70982
+ }
70983
+ const prefix = normBase.endsWith(sep3) ? normBase : `${normBase}${sep3}`;
70984
+ return normTarget.startsWith(prefix);
70985
+ }
70986
+ function resolveSafeReadPath(cwd, filePath) {
70987
+ if (!filePath || !filePath.trim()) {
70988
+ throw new Error("File path cannot be empty.");
70989
+ }
70990
+ const trimmed = filePath.trim();
70991
+ const resolved = isAbsolute2(trimmed) ? resolve5(trimmed) : resolve5(cwd, trimmed);
70992
+ if (!isPathInside2(resolved, cwd)) {
70993
+ throw new Error(`Access denied: path "${filePath}" resolves outside the trusted workspace root ("${cwd}").`);
70994
+ }
70995
+ if (!existsSync9(resolved)) {
70996
+ throw new Error(`File not found: ${filePath}`);
70997
+ }
70998
+ let realTarget;
70999
+ try {
71000
+ realTarget = realpathSync3(resolved);
71001
+ } catch {
71002
+ realTarget = resolved;
71003
+ }
71004
+ if (!isPathInside2(realTarget, cwd)) {
71005
+ throw new Error(`Access denied: symlink "${filePath}" points outside the trusted workspace root.`);
71006
+ }
71007
+ const stat = lstatSync3(resolved);
71008
+ if (stat.isDirectory()) {
71009
+ throw new Error(`Cannot read path because it is a directory: ${filePath}`);
71010
+ }
71011
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
71012
+ throw new Error(`Cannot read special or non-regular file: ${filePath}`);
71013
+ }
71014
+ return realTarget;
71015
+ }
70977
71016
  function isBinaryBuffer(buffer) {
70978
71017
  const checkLength = Math.min(buffer.length, 1024);
70979
71018
  for (let i = 0;i < checkLength; i++) {
@@ -70986,7 +71025,7 @@ function isBinaryBuffer(buffer) {
70986
71025
  var readFileTool = {
70987
71026
  name: "read_file",
70988
71027
  displayName: "Read",
70989
- description: "Reads the contents of a file with 1-indexed line numbers. Supports reading specific line ranges via offset and limit. Read is read-only and does not mutate or checkpoint.",
71028
+ description: "Reads the contents of a file. Supports reading specific line ranges via offset and limit. Read is read-only and does not mutate or checkpoint.",
70990
71029
  parameters: readFileInputSchema,
70991
71030
  confirmationPolicy: "never",
70992
71031
  summarize: (_args, result) => {
@@ -70994,10 +71033,7 @@ var readFileTool = {
70994
71033
  return `Read ${lines} line${lines === 1 ? "" : "s"}`;
70995
71034
  },
70996
71035
  execute: async (args, context) => {
70997
- const targetPath = isAbsolute2(args.path) ? args.path : resolve5(context.cwd, args.path);
70998
- if (!existsSync9(targetPath)) {
70999
- throw new Error(`File not found: ${args.path}`);
71000
- }
71036
+ const targetPath = resolveSafeReadPath(context.cwd, args.path);
71001
71037
  const stat = lstatSync3(targetPath);
71002
71038
  if (stat.isDirectory()) {
71003
71039
  throw new Error(`Cannot read path because it is a directory: ${args.path}`);
@@ -71033,18 +71069,13 @@ var readFileTool = {
71033
71069
  const startIndex = Math.max(0, startLine - 1);
71034
71070
  const endIndex = Math.min(totalLines, startIndex + limit);
71035
71071
  const slice = allLines.slice(startIndex, endIndex);
71036
- const padWidth = String(endIndex).length;
71037
- const formattedContent = slice.map((line, idx) => {
71038
- const lineNum = String(startIndex + idx + 1).padStart(padWidth, " ");
71039
- return `${lineNum} | ${line}`;
71040
- }).join(`
71041
- `);
71042
71072
  return {
71043
71073
  path: args.path,
71044
71074
  totalLines,
71045
71075
  startLine,
71046
71076
  endLine: endIndex,
71047
- content: formattedContent,
71077
+ content: slice.join(`
71078
+ `),
71048
71079
  isTruncated: endIndex < totalLines,
71049
71080
  linesRead: slice.length
71050
71081
  };
@@ -71052,6 +71083,9 @@ var readFileTool = {
71052
71083
  };
71053
71084
 
71054
71085
  // src/tools/write-file/index.ts
71086
+ import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync3 } from "node:fs";
71087
+
71088
+ // src/utils/atomic-write.ts
71055
71089
  import { randomUUID as randomUUID5 } from "node:crypto";
71056
71090
  import {
71057
71091
  chmodSync as chmodSync2,
@@ -71060,13 +71094,46 @@ import {
71060
71094
  fsyncSync as fsyncSync5,
71061
71095
  mkdirSync as mkdirSync7,
71062
71096
  openSync as openSync6,
71063
- readFileSync as readFileSync9,
71064
71097
  renameSync as renameSync5,
71065
- statSync as statSync3,
71066
71098
  unlinkSync as unlinkSync5,
71067
71099
  writeSync as writeSync5
71068
71100
  } from "node:fs";
71069
71101
  import { dirname as dirname5, join as join7 } from "node:path";
71102
+ function atomicWriteFileSync(targetPath, content, options = {}) {
71103
+ const buffer = typeof content === "string" ? Buffer.from(content, options.encoding ?? "utf-8") : content;
71104
+ const dir = dirname5(targetPath);
71105
+ if (!existsSync10(dir)) {
71106
+ mkdirSync7(dir, { recursive: true });
71107
+ }
71108
+ const mode = options.mode ?? 420;
71109
+ const tmpPath = join7(dir, `.tmp-write-${randomUUID5().slice(0, 8)}`);
71110
+ let fd = null;
71111
+ try {
71112
+ fd = openSync6(tmpPath, "w", mode);
71113
+ writeSync5(fd, buffer, 0, buffer.length);
71114
+ fsyncSync5(fd);
71115
+ closeSync6(fd);
71116
+ fd = null;
71117
+ try {
71118
+ chmodSync2(tmpPath, mode);
71119
+ } catch {}
71120
+ renameSync5(tmpPath, targetPath);
71121
+ } catch (err) {
71122
+ if (fd !== null) {
71123
+ try {
71124
+ closeSync6(fd);
71125
+ } catch {}
71126
+ }
71127
+ if (existsSync10(tmpPath)) {
71128
+ try {
71129
+ unlinkSync5(tmpPath);
71130
+ } catch {}
71131
+ }
71132
+ throw err;
71133
+ }
71134
+ }
71135
+
71136
+ // src/tools/write-file/index.ts
71070
71137
  var writeFileInputSchema = object({
71071
71138
  file_path: string2().describe("The path of the file to write (relative to current working directory or absolute inside trusted workspace)."),
71072
71139
  content: string2().describe("The full text content to write to the file.")
@@ -71098,7 +71165,7 @@ ${detail}${overflow}`;
71098
71165
  },
71099
71166
  execute: async (args, context) => {
71100
71167
  const { absolutePath: targetPath, relativePath } = resolveDirectMutationPath(context.cwd, args.file_path);
71101
- const isNew = !existsSync10(targetPath);
71168
+ const isNew = !existsSync11(targetPath);
71102
71169
  const beforeContent = isNew ? null : readFileSync9(targetPath, "utf-8");
71103
71170
  const afterContent = args.content;
71104
71171
  const linesWritten = afterContent.length === 0 ? 0 : afterContent.split(/\r?\n/).length;
@@ -71134,11 +71201,11 @@ ${detail}${overflow}`;
71134
71201
  throw new Error("File write permission denied by user.");
71135
71202
  }
71136
71203
  if (isNew) {
71137
- if (existsSync10(targetPath)) {
71204
+ if (existsSync11(targetPath)) {
71138
71205
  throw new Error(`File was created externally during approval review: "${relativePath}". Mutation aborted to prevent data loss.`);
71139
71206
  }
71140
71207
  } else {
71141
- if (!existsSync10(targetPath)) {
71208
+ if (!existsSync11(targetPath)) {
71142
71209
  throw new Error(`File was deleted externally during approval review: "${relativePath}". Mutation aborted.`);
71143
71210
  }
71144
71211
  const freshContent = readFileSync9(targetPath, "utf-8");
@@ -71146,42 +71213,14 @@ ${detail}${overflow}`;
71146
71213
  throw new Error(`File was modified externally during approval review: "${relativePath}". Mutation aborted to prevent data loss.`);
71147
71214
  }
71148
71215
  }
71149
- const newBuffer = Buffer.from(afterContent, "utf-8");
71150
- const dir = dirname5(targetPath);
71151
- if (!existsSync10(dir)) {
71152
- mkdirSync7(dir, { recursive: true });
71153
- }
71154
71216
  let existingMode = 420;
71155
71217
  if (!isNew) {
71156
71218
  try {
71157
71219
  existingMode = statSync3(targetPath).mode;
71158
71220
  } catch {}
71159
71221
  }
71160
- const tmpPath = join7(dir, `.tmp-write-${randomUUID5().slice(0, 8)}`);
71161
- let fd = null;
71162
- try {
71163
- fd = openSync6(tmpPath, "w", existingMode);
71164
- writeSync5(fd, newBuffer, 0, newBuffer.length);
71165
- fsyncSync5(fd);
71166
- closeSync6(fd);
71167
- fd = null;
71168
- try {
71169
- chmodSync2(tmpPath, existingMode);
71170
- } catch {}
71171
- renameSync5(tmpPath, targetPath);
71172
- } catch (err) {
71173
- if (fd !== null) {
71174
- try {
71175
- closeSync6(fd);
71176
- } catch {}
71177
- }
71178
- if (existsSync10(tmpPath)) {
71179
- try {
71180
- unlinkSync5(tmpPath);
71181
- } catch {}
71182
- }
71183
- throw err;
71184
- }
71222
+ const newBuffer = Buffer.from(afterContent, "utf-8");
71223
+ atomicWriteFileSync(targetPath, newBuffer, { mode: existingMode });
71185
71224
  if (context.checkpointTracker) {
71186
71225
  await context.checkpointTracker.completeMutation(targetPath);
71187
71226
  }
@@ -71201,20 +71240,7 @@ ${detail}${overflow}`;
71201
71240
  };
71202
71241
 
71203
71242
  // src/tools/edit-file/index.ts
71204
- import { randomUUID as randomUUID6 } from "node:crypto";
71205
- import {
71206
- chmodSync as chmodSync3,
71207
- closeSync as closeSync7,
71208
- existsSync as existsSync11,
71209
- fsyncSync as fsyncSync6,
71210
- openSync as openSync7,
71211
- readFileSync as readFileSync10,
71212
- renameSync as renameSync6,
71213
- statSync as statSync4,
71214
- unlinkSync as unlinkSync6,
71215
- writeSync as writeSync6
71216
- } from "node:fs";
71217
- import { dirname as dirname6, join as join8 } from "node:path";
71243
+ import { existsSync as existsSync12, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
71218
71244
 
71219
71245
  // src/utils/diff.ts
71220
71246
  function tokenizeLines(content) {
@@ -71524,7 +71550,7 @@ ${diffLines.join(`
71524
71550
  },
71525
71551
  execute: async (args, context) => {
71526
71552
  const { absolutePath: targetPath, relativePath } = resolveDirectMutationPath(context.cwd, args.file_path);
71527
- if (!existsSync11(targetPath)) {
71553
+ if (!existsSync12(targetPath)) {
71528
71554
  throw new Error(`Cannot edit file because it does not exist: "${args.file_path}". Use write_file to create new files.`);
71529
71555
  }
71530
71556
  const currentContent = readFileSync10(targetPath, "utf-8");
@@ -71575,65 +71601,30 @@ ${diffLines.join(`
71575
71601
  if (!perm.allowed) {
71576
71602
  throw new Error("File edit permission denied by user.");
71577
71603
  }
71578
- if (!existsSync11(targetPath)) {
71604
+ if (!existsSync12(targetPath)) {
71579
71605
  throw new Error(`File was deleted externally during approval review: "${relativePath}". Mutation aborted.`);
71580
71606
  }
71581
71607
  const freshContent = readFileSync10(targetPath, "utf-8");
71582
71608
  if (freshContent !== currentContent) {
71583
71609
  throw new Error(`File was modified externally during approval review: "${relativePath}". Mutation aborted to prevent data loss.`);
71584
71610
  }
71585
- const beforeLines = currentContent.split(/\r?\n/);
71586
- const afterLines = updatedContent.split(/\r?\n/);
71587
- const lineDelta = afterLines.length - beforeLines.length;
71611
+ const diff = buildUnifiedDiff(currentContent, updatedContent, 0);
71588
71612
  let addedLines = 0;
71589
71613
  let removedLines = 0;
71590
- const oldLinesInMatch = args.old_string.split(/\r?\n/).length;
71591
- const newLinesInMatch = args.new_string.split(/\r?\n/).length;
71592
- const totalReplacedOccurrences = args.replace_all ? matchCount : 1;
71593
- if (oldLinesInMatch === newLinesInMatch && !args.old_string.includes(`
71594
- `) && !args.new_string.includes(`
71595
- `)) {
71596
- addedLines = totalReplacedOccurrences;
71597
- removedLines = totalReplacedOccurrences;
71598
- } else {
71599
- removedLines = (oldLinesInMatch - 1) * totalReplacedOccurrences + (lineDelta < 0 ? Math.abs(lineDelta) : 0);
71600
- addedLines = (newLinesInMatch - 1) * totalReplacedOccurrences + (lineDelta > 0 ? lineDelta : 0);
71601
- if (addedLines === 0 && removedLines === 0 && args.old_string !== args.new_string) {
71602
- addedLines = totalReplacedOccurrences;
71603
- removedLines = totalReplacedOccurrences;
71614
+ for (const hunk of diff.hunks) {
71615
+ for (const line of hunk.lines) {
71616
+ if (line.kind === "addition")
71617
+ addedLines++;
71618
+ if (line.kind === "deletion")
71619
+ removedLines++;
71604
71620
  }
71605
71621
  }
71606
- const newBuffer = Buffer.from(updatedContent, "utf-8");
71607
- const dir = dirname6(targetPath);
71608
71622
  let existingMode = 420;
71609
71623
  try {
71610
71624
  existingMode = statSync4(targetPath).mode;
71611
71625
  } catch {}
71612
- const tmpPath = join8(dir, `.tmp-edit-${randomUUID6().slice(0, 8)}`);
71613
- let fd = null;
71614
- try {
71615
- fd = openSync7(tmpPath, "w", existingMode);
71616
- writeSync6(fd, newBuffer, 0, newBuffer.length);
71617
- fsyncSync6(fd);
71618
- closeSync7(fd);
71619
- fd = null;
71620
- try {
71621
- chmodSync3(tmpPath, existingMode);
71622
- } catch {}
71623
- renameSync6(tmpPath, targetPath);
71624
- } catch (err) {
71625
- if (fd !== null) {
71626
- try {
71627
- closeSync7(fd);
71628
- } catch {}
71629
- }
71630
- if (existsSync11(tmpPath)) {
71631
- try {
71632
- unlinkSync6(tmpPath);
71633
- } catch {}
71634
- }
71635
- throw err;
71636
- }
71626
+ const newBuffer = Buffer.from(updatedContent, "utf-8");
71627
+ atomicWriteFileSync(targetPath, newBuffer, { mode: existingMode });
71637
71628
  if (context.checkpointTracker) {
71638
71629
  await context.checkpointTracker.completeMutation(targetPath);
71639
71630
  }
@@ -71654,7 +71645,7 @@ ${diffLines.join(`
71654
71645
 
71655
71646
  // src/tools/glob/index.ts
71656
71647
  import { readdirSync as readdirSync2 } from "node:fs";
71657
- import { isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve6 } from "node:path";
71648
+ import { isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve6 } from "node:path";
71658
71649
  var globInputSchema = object({
71659
71650
  pattern: string2().describe('Glob pattern to match files against (e.g. "**/*.ts", "src/*.json").'),
71660
71651
  path: string2().optional().describe("Starting directory path. Defaults to workspace root."),
@@ -71683,10 +71674,10 @@ function walkDir(dir, baseDir, matcher, results, limit) {
71683
71674
  break;
71684
71675
  if (entry.isDirectory()) {
71685
71676
  if (!IGNORED_DIRS.has(entry.name)) {
71686
- walkDir(join9(dir, entry.name), baseDir, matcher, results, limit);
71677
+ walkDir(join8(dir, entry.name), baseDir, matcher, results, limit);
71687
71678
  }
71688
71679
  } else if (entry.isFile()) {
71689
- const rel = relative2(baseDir, join9(dir, entry.name));
71680
+ const rel = relative2(baseDir, join8(dir, entry.name));
71690
71681
  if (matcher(rel, entry.name)) {
71691
71682
  results.push(rel);
71692
71683
  }
@@ -71706,7 +71697,13 @@ var globTool = {
71706
71697
  },
71707
71698
  execute: async (args, context) => {
71708
71699
  const startTime = Date.now();
71709
- const searchRoot = args.path ? isAbsolute3(args.path) ? args.path : resolve6(context.cwd, args.path) : context.cwd;
71700
+ const searchRoot = args.path ? isAbsolute3(args.path) ? resolve6(args.path) : resolve6(context.cwd, args.path) : context.cwd;
71701
+ const normCwd = resolve6(context.cwd);
71702
+ const normTarget = resolve6(searchRoot);
71703
+ const prefix = normCwd.endsWith("/") ? normCwd : `${normCwd}/`;
71704
+ if (normTarget !== normCwd && !normTarget.startsWith(prefix)) {
71705
+ throw new Error(`Access denied: path "${args.path}" resolves outside the trusted workspace root ("${context.cwd}").`);
71706
+ }
71710
71707
  const limit = args.limit ?? 100;
71711
71708
  const pattern = args.pattern.trim();
71712
71709
  const regex = globToRegex(pattern);
@@ -71729,7 +71726,7 @@ var globTool = {
71729
71726
 
71730
71727
  // src/tools/grep/index.ts
71731
71728
  import { readFileSync as readFileSync11, readdirSync as readdirSync3 } from "node:fs";
71732
- import { isAbsolute as isAbsolute4, join as join10, relative as relative3, resolve as resolve7 } from "node:path";
71729
+ import { isAbsolute as isAbsolute4, join as join9, relative as relative3, resolve as resolve7 } from "node:path";
71733
71730
  var grepInputSchema = object({
71734
71731
  pattern: string2().describe("The regex or text pattern to search for across files."),
71735
71732
  path: string2().optional().describe("Target directory or file path to search. Defaults to current directory."),
@@ -71758,7 +71755,7 @@ function searchInDir(dir, baseDir, regex, fileFilter, results, filesWithMatches,
71758
71755
  for (const entry of entries) {
71759
71756
  if (results.length >= limit)
71760
71757
  break;
71761
- const fullPath = join10(dir, entry.name);
71758
+ const fullPath = join9(dir, entry.name);
71762
71759
  if (entry.isDirectory()) {
71763
71760
  if (!IGNORED_DIRS2.has(entry.name)) {
71764
71761
  searchInDir(fullPath, baseDir, regex, fileFilter, results, filesWithMatches, limit);
@@ -71804,7 +71801,13 @@ var grepTool = {
71804
71801
  },
71805
71802
  execute: async (args, context) => {
71806
71803
  const startTime = Date.now();
71807
- const searchRoot = args.path ? isAbsolute4(args.path) ? args.path : resolve7(context.cwd, args.path) : context.cwd;
71804
+ const searchRoot = args.path ? isAbsolute4(args.path) ? resolve7(args.path) : resolve7(context.cwd, args.path) : context.cwd;
71805
+ const normCwd = resolve7(context.cwd);
71806
+ const normTarget = resolve7(searchRoot);
71807
+ const prefix = normCwd.endsWith("/") ? normCwd : `${normCwd}/`;
71808
+ if (normTarget !== normCwd && !normTarget.startsWith(prefix)) {
71809
+ throw new Error(`Access denied: path "${args.path}" resolves outside the trusted workspace root ("${context.cwd}").`);
71810
+ }
71808
71811
  const limit = args.limit ?? 100;
71809
71812
  const isCaseSensitive = args.case_sensitive ?? false;
71810
71813
  const flags = isCaseSensitive ? "" : "i";
@@ -71859,7 +71862,7 @@ var grepTool = {
71859
71862
 
71860
71863
  // src/tools/list-dir/index.ts
71861
71864
  import { readdirSync as readdirSync4, statSync as statSync5 } from "node:fs";
71862
- import { isAbsolute as isAbsolute5, join as join11, resolve as resolve8 } from "node:path";
71865
+ import { isAbsolute as isAbsolute5, join as join10, resolve as resolve8 } from "node:path";
71863
71866
  var listDirInputSchema = object({
71864
71867
  path: string2().optional().describe("Directory path to list. Defaults to current working directory."),
71865
71868
  recursive: boolean2().optional().describe("If true, lists files recursively (depth 2). Defaults to false.")
@@ -71876,7 +71879,13 @@ var listDirTool = {
71876
71879
  return `Listed ${files} file${files === 1 ? "" : "s"}, ${dirs} director${dirs === 1 ? "y" : "ies"}`;
71877
71880
  },
71878
71881
  execute: async (args, context) => {
71879
- const targetPath = args.path ? isAbsolute5(args.path) ? args.path : resolve8(context.cwd, args.path) : context.cwd;
71882
+ const targetPath = args.path ? isAbsolute5(args.path) ? resolve8(args.path) : resolve8(context.cwd, args.path) : context.cwd;
71883
+ const normCwd = resolve8(context.cwd);
71884
+ const normTarget = resolve8(targetPath);
71885
+ const prefix = normCwd.endsWith("/") ? normCwd : `${normCwd}/`;
71886
+ if (normTarget !== normCwd && !normTarget.startsWith(prefix)) {
71887
+ throw new Error(`Access denied: path "${args.path}" resolves outside the trusted workspace root ("${context.cwd}").`);
71888
+ }
71880
71889
  const rawEntries = readdirSync4(targetPath, { withFileTypes: true });
71881
71890
  const entries = [];
71882
71891
  let fileCount = 0;
@@ -71893,7 +71902,7 @@ var listDirTool = {
71893
71902
  type = "file";
71894
71903
  fileCount++;
71895
71904
  try {
71896
- sizeBytes = statSync5(join11(targetPath, entry.name)).size;
71905
+ sizeBytes = statSync5(join10(targetPath, entry.name)).size;
71897
71906
  } catch {}
71898
71907
  } else {
71899
71908
  fileCount++;
@@ -71961,7 +71970,7 @@ import { rmSync as rmSync3 } from "node:fs";
71961
71970
  // src/services/tasks/process.ts
71962
71971
  import { spawn } from "node:child_process";
71963
71972
  import { createWriteStream, mkdirSync as mkdirSync8 } from "node:fs";
71964
- import { join as join12 } from "node:path";
71973
+ import { join as join11 } from "node:path";
71965
71974
  import { tmpdir, homedir as homedir4 } from "node:os";
71966
71975
 
71967
71976
  // src/services/tasks/shell.ts
@@ -72009,16 +72018,16 @@ class ShellExecution {
72009
72018
  this.command = options.command;
72010
72019
  this.cwd = options.cwd;
72011
72020
  this.startedAt = new Date().toISOString();
72012
- const taskDir = join12(homedir4(), ".steward", "tasks", options.sessionId || "default");
72021
+ const taskDir = join11(homedir4(), ".steward", "tasks", options.sessionId || "default");
72013
72022
  try {
72014
72023
  mkdirSync8(taskDir, { recursive: true });
72015
72024
  } catch {
72016
- const fallbackDir = join12(tmpdir(), "steward-tasks");
72025
+ const fallbackDir = join11(tmpdir(), "steward-tasks");
72017
72026
  mkdirSync8(fallbackDir, { recursive: true });
72018
- this.outputPath = join12(fallbackDir, `${this.taskId}.log`);
72027
+ this.outputPath = join11(fallbackDir, `${this.taskId}.log`);
72019
72028
  return;
72020
72029
  }
72021
- this.outputPath = join12(taskDir, `${this.taskId}.log`);
72030
+ this.outputPath = join11(taskDir, `${this.taskId}.log`);
72022
72031
  }
72023
72032
  get status() {
72024
72033
  switch (this.state) {
@@ -72556,7 +72565,7 @@ var bashTool = {
72556
72565
  if (result && "status" in result && result.status === "backgrounded") {
72557
72566
  return `Moved to background · task id: ${result.taskId}`;
72558
72567
  }
72559
- const code = result?.exitCode ?? 0;
72568
+ const code = result && "exitCode" in result ? result.exitCode : 0;
72560
72569
  return `Ran successfully · exit code: ${code}`;
72561
72570
  },
72562
72571
  execute: async (args, context) => {
@@ -72688,10 +72697,34 @@ var taskKillTool = {
72688
72697
  return context.shellTasks.kill(args.task_id);
72689
72698
  }
72690
72699
  };
72700
+
72701
+ // src/tools/task-list/index.ts
72702
+ var taskListInputSchema = object({});
72703
+ var taskListTool = {
72704
+ name: "task_list",
72705
+ displayName: "Task List",
72706
+ description: "Lists all currently tracked background shell tasks along with their IDs, statuses, commands, and exit codes.",
72707
+ parameters: taskListInputSchema,
72708
+ confirmationPolicy: "never",
72709
+ summarize: (_args, result) => {
72710
+ const count = result?.total ?? 0;
72711
+ return `Listed ${count} background task${count === 1 ? "" : "s"}`;
72712
+ },
72713
+ execute: async (_args, context) => {
72714
+ if (!context.shellTasks) {
72715
+ throw new Error("No shell tasks manager available in execution context.");
72716
+ }
72717
+ const tasks = context.shellTasks.list();
72718
+ return {
72719
+ tasks,
72720
+ total: tasks.length
72721
+ };
72722
+ }
72723
+ };
72691
72724
  // package.json
72692
72725
  var package_default = {
72693
72726
  name: "steward-cli",
72694
- version: "0.13.0",
72727
+ version: "0.15.0",
72695
72728
  description: "Interactive AI engineering assistant for the terminal with human-in-the-loop control across local and frontier models.",
72696
72729
  author: "sapirrior",
72697
72730
  license: "MIT",
@@ -72910,18 +72943,18 @@ var webSearchTool = {
72910
72943
  };
72911
72944
  // src/services/todos/store.ts
72912
72945
  import {
72913
- existsSync as existsSync12,
72946
+ existsSync as existsSync13,
72914
72947
  mkdirSync as mkdirSync9,
72915
72948
  readFileSync as readFileSync12,
72916
- renameSync as renameSync7,
72917
- unlinkSync as unlinkSync7,
72949
+ renameSync as renameSync6,
72950
+ unlinkSync as unlinkSync6,
72918
72951
  writeFileSync as writeFileSync3,
72919
- openSync as openSync8,
72920
- fsyncSync as fsyncSync7,
72921
- closeSync as closeSync8
72952
+ openSync as openSync7,
72953
+ fsyncSync as fsyncSync6,
72954
+ closeSync as closeSync7
72922
72955
  } from "node:fs";
72923
- import { join as join13 } from "node:path";
72924
- import { randomUUID as randomUUID7 } from "node:crypto";
72956
+ import { join as join12 } from "node:path";
72957
+ import { randomUUID as randomUUID6 } from "node:crypto";
72925
72958
 
72926
72959
  // src/config/env.ts
72927
72960
  var ALL_PROVIDER_NAMES = [
@@ -73044,10 +73077,10 @@ function sanitizeSessionId(sessionId) {
73044
73077
  }
73045
73078
  function getSessionTodosDir(sessionId) {
73046
73079
  const safeId = sanitizeSessionId(sessionId);
73047
- return join13(getSettingsDir(), "todos", safeId);
73080
+ return join12(getSettingsDir(), "todos", safeId);
73048
73081
  }
73049
73082
  function getSessionTodosFilePath(sessionId) {
73050
- return join13(getSessionTodosDir(sessionId), "todos.json");
73083
+ return join12(getSessionTodosDir(sessionId), "todos.json");
73051
73084
  }
73052
73085
  function validateTodoList(todos) {
73053
73086
  if (!Array.isArray(todos)) {
@@ -73095,7 +73128,7 @@ function validateTodoList(todos) {
73095
73128
  }
73096
73129
  async function readTodoList(sessionId) {
73097
73130
  const filePath = getSessionTodosFilePath(sessionId);
73098
- if (!existsSync12(filePath)) {
73131
+ if (!existsSync13(filePath)) {
73099
73132
  return null;
73100
73133
  }
73101
73134
  const raw = readFileSync12(filePath, "utf-8");
@@ -73128,7 +73161,7 @@ async function writeTodoList(sessionId, todos) {
73128
73161
  const validated = validateTodoList(todos);
73129
73162
  const safeSessionId = sanitizeSessionId(sessionId);
73130
73163
  const dir = getSessionTodosDir(safeSessionId);
73131
- if (!existsSync12(dir)) {
73164
+ if (!existsSync13(dir)) {
73132
73165
  mkdirSync9(dir, { recursive: true, mode: 448 });
73133
73166
  }
73134
73167
  const payload = {
@@ -73138,22 +73171,22 @@ async function writeTodoList(sessionId, todos) {
73138
73171
  todos: validated
73139
73172
  };
73140
73173
  const targetPath = getSessionTodosFilePath(safeSessionId);
73141
- const tempPath = join13(dir, `.todos.${randomUUID7()}.tmp`);
73174
+ const tempPath = join12(dir, `.todos.${randomUUID6()}.tmp`);
73142
73175
  const jsonStr = JSON.stringify(payload, null, 2) + `
73143
73176
  `;
73144
73177
  try {
73145
73178
  writeFileSync3(tempPath, jsonStr, { encoding: "utf-8", mode: 384 });
73146
- const fd = openSync8(tempPath, "r");
73179
+ const fd = openSync7(tempPath, "r");
73147
73180
  try {
73148
- fsyncSync7(fd);
73181
+ fsyncSync6(fd);
73149
73182
  } finally {
73150
- closeSync8(fd);
73183
+ closeSync7(fd);
73151
73184
  }
73152
- renameSync7(tempPath, targetPath);
73185
+ renameSync6(tempPath, targetPath);
73153
73186
  } catch (err) {
73154
73187
  try {
73155
- if (existsSync12(tempPath)) {
73156
- unlinkSync7(tempPath);
73188
+ if (existsSync13(tempPath)) {
73189
+ unlinkSync6(tempPath);
73157
73190
  }
73158
73191
  } catch {}
73159
73192
  throw err;
@@ -73197,7 +73230,7 @@ async function updateTodoItem(sessionId, todoId, updates) {
73197
73230
  const validated = validateTodoList(updatedTodos);
73198
73231
  const safeSessionId = sanitizeSessionId(sessionId);
73199
73232
  const dir = getSessionTodosDir(safeSessionId);
73200
- if (!existsSync12(dir)) {
73233
+ if (!existsSync13(dir)) {
73201
73234
  mkdirSync9(dir, { recursive: true, mode: 448 });
73202
73235
  }
73203
73236
  const payload = {
@@ -73207,22 +73240,22 @@ async function updateTodoItem(sessionId, todoId, updates) {
73207
73240
  todos: validated
73208
73241
  };
73209
73242
  const targetPath = getSessionTodosFilePath(safeSessionId);
73210
- const tempPath = join13(dir, `.todos.${randomUUID7()}.tmp`);
73243
+ const tempPath = join12(dir, `.todos.${randomUUID6()}.tmp`);
73211
73244
  const jsonStr = JSON.stringify(payload, null, 2) + `
73212
73245
  `;
73213
73246
  try {
73214
73247
  writeFileSync3(tempPath, jsonStr, { encoding: "utf-8", mode: 384 });
73215
- const fd = openSync8(tempPath, "r");
73248
+ const fd = openSync7(tempPath, "r");
73216
73249
  try {
73217
- fsyncSync7(fd);
73250
+ fsyncSync6(fd);
73218
73251
  } finally {
73219
- closeSync8(fd);
73252
+ closeSync7(fd);
73220
73253
  }
73221
- renameSync7(tempPath, targetPath);
73254
+ renameSync6(tempPath, targetPath);
73222
73255
  } catch (err) {
73223
73256
  try {
73224
- if (existsSync12(tempPath)) {
73225
- unlinkSync7(tempPath);
73257
+ if (existsSync13(tempPath)) {
73258
+ unlinkSync6(tempPath);
73226
73259
  }
73227
73260
  } catch {}
73228
73261
  throw err;
@@ -73265,7 +73298,7 @@ function formatTodoListSummary(todos, maxItems = 10) {
73265
73298
  `);
73266
73299
  }
73267
73300
  var todoWriteTool = {
73268
- name: "TodoWrite",
73301
+ name: "todo_write",
73269
73302
  displayName: "TodoWrite",
73270
73303
  icon: "\uD83D\uDCDD",
73271
73304
  description: "Initializes or replaces the current session todo list with 2 to 10 items. Use this to establish or replace the multi-step execution plan for the session.",
@@ -73326,7 +73359,7 @@ function formatTodoListSummary2(todos, maxItems = 10) {
73326
73359
  `);
73327
73360
  }
73328
73361
  var todoUpdateTool = {
73329
- name: "TodoUpdate",
73362
+ name: "todo_update",
73330
73363
  displayName: "TodoUpdate",
73331
73364
  icon: "✓",
73332
73365
  description: "Updates a single todo item status or description in the current session. Cannot create new items or update multiple items at once.",
@@ -73391,7 +73424,7 @@ function formatTodoListSummary3(todos, maxItems = 10) {
73391
73424
  `);
73392
73425
  }
73393
73426
  var todoReadTool = {
73394
- name: "TodoRead",
73427
+ name: "todo_read",
73395
73428
  displayName: "TodoRead",
73396
73429
  icon: "\uD83D\uDCCB",
73397
73430
  description: "Reads the current session todo list without modifying it. Returns an empty result if no todos have been established yet.",
@@ -73431,9 +73464,9 @@ ${details}`;
73431
73464
  }
73432
73465
  };
73433
73466
  // src/skills/discovery.ts
73434
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6 } from "node:fs";
73467
+ import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6 } from "node:fs";
73435
73468
  import { homedir as homedir5 } from "node:os";
73436
- import { isAbsolute as isAbsolute6, join as join14, normalize, resolve as resolve9, sep as sep3 } from "node:path";
73469
+ import { isAbsolute as isAbsolute6, join as join13, normalize, resolve as resolve9, sep as sep4 } from "node:path";
73437
73470
  var MAX_SKILL_FILE_SIZE = 64 * 1024;
73438
73471
  function parseSkillFrontmatter(content) {
73439
73472
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -73486,13 +73519,13 @@ function unquote(str) {
73486
73519
  }
73487
73520
  function discoverSkills(cwd = process.cwd()) {
73488
73521
  const targets = [
73489
- { dir: join14(cwd, ".agents", "skills"), source: "workspace" },
73490
- { dir: join14(homedir5(), ".agents", "skills"), source: "user-agents" },
73491
- { dir: join14(homedir5(), ".steward", "skills"), source: "user-steward" }
73522
+ { dir: join13(cwd, ".agents", "skills"), source: "workspace" },
73523
+ { dir: join13(homedir5(), ".agents", "skills"), source: "user-agents" },
73524
+ { dir: join13(homedir5(), ".steward", "skills"), source: "user-steward" }
73492
73525
  ];
73493
73526
  const skillMap = new Map;
73494
73527
  for (const target of targets) {
73495
- if (!existsSync13(target.dir)) {
73528
+ if (!existsSync14(target.dir)) {
73496
73529
  continue;
73497
73530
  }
73498
73531
  try {
@@ -73500,10 +73533,10 @@ function discoverSkills(cwd = process.cwd()) {
73500
73533
  for (const entry of entries) {
73501
73534
  if (!entry.isDirectory())
73502
73535
  continue;
73503
- const skillDir = join14(target.dir, entry.name);
73504
- const skillMdPath = join14(skillDir, "SKILL.md");
73505
- const fallbackMdPath = join14(skillDir, "skill.md");
73506
- const resolvedFile = existsSync13(skillMdPath) ? skillMdPath : existsSync13(fallbackMdPath) ? fallbackMdPath : null;
73536
+ const skillDir = join13(target.dir, entry.name);
73537
+ const skillMdPath = join13(skillDir, "SKILL.md");
73538
+ const fallbackMdPath = join13(skillDir, "skill.md");
73539
+ const resolvedFile = existsSync14(skillMdPath) ? skillMdPath : existsSync14(fallbackMdPath) ? fallbackMdPath : null;
73507
73540
  if (!resolvedFile)
73508
73541
  continue;
73509
73542
  try {
@@ -73545,16 +73578,16 @@ function readSkillResource(name, relativePath, cwd = process.cwd(), maxBytes = M
73545
73578
  throw new Error(`Invalid skill resource path: absolute paths are not permitted (${trimmedPath})`);
73546
73579
  }
73547
73580
  const normalizedRelative = normalize(trimmedPath);
73548
- if (normalizedRelative.startsWith("..") || normalizedRelative.includes(`${sep3}..`)) {
73581
+ if (normalizedRelative.startsWith("..") || normalizedRelative.includes(`${sep4}..`)) {
73549
73582
  throw new Error(`Invalid skill resource path: directory traversal ("..") is forbidden (${trimmedPath})`);
73550
73583
  }
73551
73584
  targetFilePath = resolve9(skill.dirPath, normalizedRelative);
73552
73585
  const resolvedDir = resolve9(skill.dirPath);
73553
- if (!targetFilePath.startsWith(resolvedDir + sep3) && targetFilePath !== resolvedDir) {
73586
+ if (!targetFilePath.startsWith(resolvedDir + sep4) && targetFilePath !== resolvedDir) {
73554
73587
  throw new Error(`Resource "${trimmedPath}" resolves outside skill directory`);
73555
73588
  }
73556
73589
  }
73557
- if (!existsSync13(targetFilePath)) {
73590
+ if (!existsSync14(targetFilePath)) {
73558
73591
  throw new Error(`Skill resource file "${targetFilePath}" does not exist`);
73559
73592
  }
73560
73593
  const stat = statSync6(targetFilePath);
@@ -73576,7 +73609,7 @@ function readSkillResource(name, relativePath, cwd = process.cwd(), maxBytes = M
73576
73609
  // src/tools/skill-list/index.ts
73577
73610
  var skillListParamsSchema = object({});
73578
73611
  var skillListTool = {
73579
- name: "SkillList",
73612
+ name: "skill_list",
73580
73613
  displayName: "SkillList",
73581
73614
  icon: "\uD83D\uDCA1",
73582
73615
  description: "Discovers and lists all available specialized skills and their descriptions. Use this when you need specialized domain knowledge or workflows.",
@@ -73608,7 +73641,7 @@ var skillReadParamsSchema = object({
73608
73641
  path: string2().optional()
73609
73642
  });
73610
73643
  var skillReadTool = {
73611
- name: "SkillRead",
73644
+ name: "skill_read",
73612
73645
  displayName: "SkillRead",
73613
73646
  icon: "\uD83D\uDCD6",
73614
73647
  description: "Loads and reads the instructions (SKILL.md) or a relative resource file of a specialized skill by name. Use this when a task requires specialized guidelines.",
@@ -73640,6 +73673,7 @@ var builtInTools = [
73640
73673
  listDirTool,
73641
73674
  sleepTool,
73642
73675
  bashTool,
73676
+ taskListTool,
73643
73677
  taskReadTool,
73644
73678
  taskSendInputTool,
73645
73679
  taskKillTool,
@@ -73691,6 +73725,7 @@ var MAX_RETAINED_TERMINAL_TASKS = 50;
73691
73725
  class ShellTaskManager {
73692
73726
  tasks = new Map;
73693
73727
  terminalTimestamps = new Map;
73728
+ evictedTaskReasons = new Map;
73694
73729
  isShuttingDown = false;
73695
73730
  generateTaskId() {
73696
73731
  return `task-${randomBytes(4).toString("hex")}`;
@@ -73724,7 +73759,7 @@ class ShellTaskManager {
73724
73759
  this.evictExpiredTasks();
73725
73760
  const exec = this.tasks.get(taskId);
73726
73761
  if (!exec) {
73727
- throw new Error(`Task "${taskId}" not found or expired.`);
73762
+ throw this.getTaskNotFoundError(taskId);
73728
73763
  }
73729
73764
  const rawOutput = exec.getRecentOutput();
73730
73765
  const { preview, truncated } = boundResultText(rawOutput);
@@ -73745,7 +73780,7 @@ class ShellTaskManager {
73745
73780
  this.evictExpiredTasks();
73746
73781
  const exec = this.tasks.get(taskId);
73747
73782
  if (!exec) {
73748
- throw new Error(`Task "${taskId}" not found or expired.`);
73783
+ throw this.getTaskNotFoundError(taskId);
73749
73784
  }
73750
73785
  if (!exec.isRunning) {
73751
73786
  throw new Error(`Task "${taskId}" is not running (status: ${exec.status}).`);
@@ -73766,7 +73801,7 @@ class ShellTaskManager {
73766
73801
  this.evictExpiredTasks();
73767
73802
  const exec = this.tasks.get(taskId);
73768
73803
  if (!exec) {
73769
- throw new Error(`Task "${taskId}" not found or expired.`);
73804
+ throw this.getTaskNotFoundError(taskId);
73770
73805
  }
73771
73806
  if (exec.isRunning) {
73772
73807
  await exec.kill();
@@ -73840,6 +73875,7 @@ class ShellTaskManager {
73840
73875
  }
73841
73876
  this.tasks.delete(id);
73842
73877
  this.terminalTimestamps.delete(id);
73878
+ this.recordEviction(id, "expired due to 10-minute retention TTL");
73843
73879
  }
73844
73880
  }
73845
73881
  const remainingTerminal = Array.from(this.terminalTimestamps.entries()).sort((a, b) => a[1] - b[1]);
@@ -73853,8 +73889,25 @@ class ShellTaskManager {
73853
73889
  }
73854
73890
  this.tasks.delete(oldestId);
73855
73891
  this.terminalTimestamps.delete(oldestId);
73892
+ this.recordEviction(oldestId, "evicted due to task capacity limit (50 tasks max)");
73856
73893
  }
73857
73894
  }
73895
+ recordEviction(taskId, reason) {
73896
+ if (this.evictedTaskReasons.size >= 100) {
73897
+ const oldest = this.evictedTaskReasons.keys().next().value;
73898
+ if (oldest) {
73899
+ this.evictedTaskReasons.delete(oldest);
73900
+ }
73901
+ }
73902
+ this.evictedTaskReasons.set(taskId, reason);
73903
+ }
73904
+ getTaskNotFoundError(taskId) {
73905
+ const reason = this.evictedTaskReasons.get(taskId);
73906
+ if (reason) {
73907
+ return new Error(`Task "${taskId}" was ${reason}.`);
73908
+ }
73909
+ return new Error(`Task "${taskId}" not found (task ID does not exist).`);
73910
+ }
73858
73911
  }
73859
73912
  // src/errors/classifier.ts
73860
73913
  function classifyError(error) {
@@ -74000,15 +74053,15 @@ function classifyError(error) {
74000
74053
  };
74001
74054
  }
74002
74055
  // src/errors/logger.ts
74003
- import { appendFileSync as appendFileSync3, existsSync as existsSync14, mkdirSync as mkdirSync10, writeFileSync as writeFileSync4 } from "node:fs";
74056
+ import { appendFileSync as appendFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync4 } from "node:fs";
74004
74057
  import { homedir as homedir6 } from "node:os";
74005
- import { join as join15 } from "node:path";
74058
+ import { join as join14 } from "node:path";
74006
74059
  var SENSITIVE_KEY_REGEX = /api[_-]?key|token|secret|password|auth|credential|bearer/i;
74007
74060
  function getLogsRootDir() {
74008
74061
  if (process.env.STEWARD_LOGS_DIR) {
74009
74062
  return process.env.STEWARD_LOGS_DIR;
74010
74063
  }
74011
- return join15(homedir6(), ".steward", "logs");
74064
+ return join14(homedir6(), ".steward", "logs");
74012
74065
  }
74013
74066
  function getLogDateTime(d = new Date) {
74014
74067
  const year = d.getFullYear();
@@ -74092,11 +74145,11 @@ function logError(error, contextInfo) {
74092
74145
  try {
74093
74146
  const rootDir = getLogsRootDir();
74094
74147
  const { date, time } = getLogDateTime();
74095
- const dateDir = join15(rootDir, date);
74096
- if (!existsSync14(dateDir)) {
74148
+ const dateDir = join14(rootDir, date);
74149
+ if (!existsSync15(dateDir)) {
74097
74150
  mkdirSync10(dateDir, { recursive: true });
74098
74151
  }
74099
- const logPath = join15(dateDir, `${time}.log`);
74152
+ const logPath = join14(dateDir, `${time}.log`);
74100
74153
  const structured = classifyError(error);
74101
74154
  const mem = process.memoryUsage ? process.memoryUsage() : undefined;
74102
74155
  const logEntry = {
@@ -74122,7 +74175,7 @@ function logError(error, contextInfo) {
74122
74175
  ` + JSON.stringify(logEntry, null, 2) + `
74123
74176
 
74124
74177
  `;
74125
- if (existsSync14(logPath)) {
74178
+ if (existsSync15(logPath)) {
74126
74179
  appendFileSync3(logPath, formatted, "utf-8");
74127
74180
  } else {
74128
74181
  writeFileSync4(logPath, formatted, "utf-8");
@@ -117839,24 +117892,24 @@ IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify,
117839
117892
 
117840
117893
  # Tool Usage Policy
117841
117894
  - Investigation: use read_file, grep, list_dir, and glob to explore code before modifying it.
117842
- - Mutations: use write_file and edit_file to apply changes. Respect user permission prompts.
117895
+ - Mutations: ALWAYS use write_file and edit_file to create or modify files rather than bash redirection (e.g. cat > file). write_file and edit_file participate in the file checkpoint and rewind system (/rewind), whereas bash commands are not tracked by checkpoints.
117843
117896
  - Commands: when using bash, you MUST always provide a concise, non-empty 'explanation' parameter describing what the command does and why it is needed.
117844
- - Background tasks: use task_read to check status/output, task_send_input to send standard input (with a trailing newline), and task_kill to terminate.
117897
+ - Background tasks: use task_list to inspect active and recent tasks, task_read to check status/output, task_send_input to send standard input (with a trailing newline), and task_kill to terminate.
117845
117898
  - Web: use web_fetch and web_search when external documentation or live data is needed. If web_fetch indicates a redirect, follow up with the target URL.
117846
117899
 
117847
117900
  # Task and Plan Management (Todos)
117848
117901
  - For multi-step, complex, or verification-heavy tasks, maintain operational state using session todos:
117849
- - TodoWrite: initialize or replace the session todo list (must contain 2 to 10 items).
117850
- - TodoUpdate: update exactly one todo item status or description as progress occurs.
117851
- - TodoRead: inspect current session todos when recovering or verifying progress.
117902
+ - todo_write (TodoWrite): initialize or replace the session todo list (must contain 2 to 10 items).
117903
+ - todo_update (TodoUpdate): update exactly one todo item status or description as progress occurs.
117904
+ - todo_read (TodoRead): inspect current session todos when recovering or verifying progress.
117852
117905
  - Keep at most one item in 'in_progress' status at any time.
117853
117906
  - Mark todos completed immediately after completing each step; do not batch progress updates.
117854
117907
  - For simple, one-step queries or edits, do not create a todo list.
117855
117908
 
117856
117909
  # Specialized Skills Policy
117857
117910
  - Skills are discoverable on-demand packages of specialized instructions located in workspace or user configuration.
117858
- - Use SkillList to discover available skills and their descriptions.
117859
- - Use SkillRead to load a skill's instructions (SKILL.md) or relative resources only when a task requires specialized domain guidance.
117911
+ - Use skill_list (SkillList) to discover available skills and their descriptions.
117912
+ - Use skill_read (SkillRead) to load a skill's instructions (SKILL.md) or relative resources only when a task requires specialized domain guidance.
117860
117913
  - Skill instructions are supplemental and must not override core safety rules, tool boundaries, or user permission constraints.
117861
117914
 
117862
117915
  <env>
@@ -118006,7 +118059,7 @@ class AgentSession {
118006
118059
  this.messages.push(userMessage);
118007
118060
  this.isGenerating = true;
118008
118061
  this.activeAbortController = new AbortController;
118009
- const turnId = randomUUID8();
118062
+ const turnId = randomUUID7();
118010
118063
  const turnStartedAt = new Date().toISOString();
118011
118064
  const turnStartMonotonic = performance.now();
118012
118065
  const turnModel = this.getModel();
@@ -120537,6 +120590,9 @@ class TrustGate extends Component {
120537
120590
 
120538
120591
  // src/tui/components/docks/ModelPicker.tsx
120539
120592
  class ModelPicker extends SelectList {
120593
+ wrap = false;
120594
+ clip = true;
120595
+ ellipsis = false;
120540
120596
  constructor(props) {
120541
120597
  super({
120542
120598
  items: props.models,
@@ -120585,6 +120641,9 @@ class ModelPicker extends SelectList {
120585
120641
 
120586
120642
  // src/tui/components/docks/SessionMenu.tsx
120587
120643
  class SessionMenu extends SelectList {
120644
+ wrap = false;
120645
+ clip = true;
120646
+ ellipsis = false;
120588
120647
  constructor(props) {
120589
120648
  super({
120590
120649
  items: props.sessions,
@@ -120967,6 +121026,9 @@ function buildRewindItems(session, cwd) {
120967
121026
  }
120968
121027
 
120969
121028
  class RewindMenu extends SelectList {
121029
+ wrap = false;
121030
+ clip = true;
121031
+ ellipsis = false;
120970
121032
  constructor(props) {
120971
121033
  const items = buildRewindItems(props.session, props.cwd);
120972
121034
  super({
@@ -121588,6 +121650,15 @@ class FilePermissionDock extends Component {
121588
121650
  }
121589
121651
 
121590
121652
  // src/tui/utils/permission-queue.ts
121653
+ function settleItem(item, allowed) {
121654
+ item.settled = true;
121655
+ if (item.kind === "bash") {
121656
+ item.resolve({ allowed });
121657
+ } else {
121658
+ item.resolve({ allowed });
121659
+ }
121660
+ }
121661
+
121591
121662
  class PermissionQueue {
121592
121663
  queue = [];
121593
121664
  activeItem = null;
@@ -121658,28 +121729,25 @@ class PermissionQueue {
121658
121729
  if (!current || current.settled) {
121659
121730
  return;
121660
121731
  }
121661
- current.settled = true;
121662
121732
  this.activeItem = null;
121663
121733
  this.onHide();
121664
- current.resolve({ allowed });
121734
+ settleItem(current, allowed);
121665
121735
  this.processNext();
121666
121736
  }
121667
121737
  cancelItem(item) {
121668
121738
  if (item.settled)
121669
121739
  return;
121670
121740
  if (this.activeItem === item) {
121671
- item.settled = true;
121672
121741
  this.activeItem = null;
121673
121742
  this.onHide();
121674
- item.resolve({ allowed: false });
121743
+ settleItem(item, false);
121675
121744
  this.processNext();
121676
121745
  } else {
121677
121746
  const idx = this.queue.indexOf(item);
121678
121747
  if (idx !== -1) {
121679
121748
  this.queue.splice(idx, 1);
121680
121749
  }
121681
- item.settled = true;
121682
- item.resolve({ allowed: false });
121750
+ settleItem(item, false);
121683
121751
  }
121684
121752
  }
121685
121753
  processNext() {
@@ -121687,8 +121755,7 @@ class PermissionQueue {
121687
121755
  const next = this.queue.shift();
121688
121756
  if (next.settled || next.abortSignal?.aborted) {
121689
121757
  if (!next.settled) {
121690
- next.settled = true;
121691
- next.resolve({ allowed: false });
121758
+ settleItem(next, false);
121692
121759
  }
121693
121760
  continue;
121694
121761
  }
@@ -121700,16 +121767,15 @@ class PermissionQueue {
121700
121767
  }
121701
121768
  clear() {
121702
121769
  if (this.activeItem && !this.activeItem.settled) {
121703
- this.activeItem.settled = true;
121704
- this.activeItem.resolve({ allowed: false });
121770
+ const active = this.activeItem;
121705
121771
  this.activeItem = null;
121706
121772
  this.onHide();
121773
+ settleItem(active, false);
121707
121774
  }
121708
121775
  while (this.queue.length > 0) {
121709
121776
  const item = this.queue.shift();
121710
121777
  if (!item.settled) {
121711
- item.settled = true;
121712
- item.resolve({ allowed: false });
121778
+ settleItem(item, false);
121713
121779
  }
121714
121780
  }
121715
121781
  }
@@ -121796,17 +121862,19 @@ function classifyVoiceError(err) {
121796
121862
  };
121797
121863
  }
121798
121864
  // src/voice/prerequisites.ts
121865
+ import { spawn as spawn2 } from "node:child_process";
121799
121866
  async function defaultCheckParec() {
121800
- try {
121801
- const proc = Bun.spawn(["which", "parec"], {
121802
- stdout: "ignore",
121803
- stderr: "ignore"
121804
- });
121805
- const exitCode = await proc.exited;
121806
- return exitCode === 0;
121807
- } catch {
121808
- return false;
121809
- }
121867
+ return new Promise((resolve) => {
121868
+ try {
121869
+ const proc = spawn2("which", ["parec"], {
121870
+ stdio: "ignore"
121871
+ });
121872
+ proc.on("error", () => resolve(false));
121873
+ proc.on("close", (code) => resolve(code === 0));
121874
+ } catch {
121875
+ resolve(false);
121876
+ }
121877
+ });
121810
121878
  }
121811
121879
  async function checkVoicePrerequisites(options = {}) {
121812
121880
  const config = options.config ?? getEnvConfig();
@@ -121874,6 +121942,8 @@ class TranscriptAccumulator {
121874
121942
  }
121875
121943
  }
121876
121944
  // src/voice/recorder.ts
121945
+ import { spawn as spawn3 } from "node:child_process";
121946
+ import { Readable } from "node:stream";
121877
121947
  var DEFAULT_PAREC_ARGS = ["--raw", "--rate=16000", "--channels=1", "--format=s16le"];
121878
121948
 
121879
121949
  class ParecAudioRecorder {
@@ -121885,12 +121955,40 @@ class ParecAudioRecorder {
121885
121955
  checkAvailableFn;
121886
121956
  constructor(options) {
121887
121957
  this.spawnFn = options?.spawnFn ?? ((cmd, opts) => {
121888
- const p = Bun.spawn(cmd, opts);
121958
+ const [executable, ...args] = cmd;
121959
+ const child = spawn3(executable, args, {
121960
+ stdio: [
121961
+ "ignore",
121962
+ opts.stdout === "pipe" ? "pipe" : "ignore",
121963
+ opts.stderr === "pipe" ? "pipe" : "ignore"
121964
+ ]
121965
+ });
121966
+ const exited = new Promise((resolve) => {
121967
+ child.on("close", (code) => resolve(code ?? 0));
121968
+ child.on("error", () => resolve(1));
121969
+ });
121970
+ const stdout = child.stdout ? Readable.toWeb(child.stdout) : null;
121971
+ const stderr = child.stderr ? Readable.toWeb(child.stderr) : null;
121889
121972
  return {
121890
- stdout: p.stdout,
121891
- stderr: p.stderr,
121892
- exited: p.exited,
121893
- kill: (sig) => p.kill(sig)
121973
+ stdout,
121974
+ stderr,
121975
+ exited,
121976
+ kill: (sig) => {
121977
+ try {
121978
+ if (typeof sig === "number") {
121979
+ const signalMap = {
121980
+ 2: "SIGINT",
121981
+ 9: "SIGKILL",
121982
+ 15: "SIGTERM"
121983
+ };
121984
+ child.kill(signalMap[sig] || sig);
121985
+ } else if (sig) {
121986
+ child.kill(sig);
121987
+ } else {
121988
+ child.kill();
121989
+ }
121990
+ } catch {}
121991
+ }
121894
121992
  };
121895
121993
  });
121896
121994
  this.checkAvailableFn = options?.checkAvailableFn ?? defaultCheckParec;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "steward-cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Interactive AI engineering assistant for the terminal with human-in-the-loop control across local and frontier models.",
5
5
  "author": "sapirrior",
6
6
  "license": "MIT",