hostwares-cli 2.4.3 → 2.4.4

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 (2) hide show
  1. package/dist/index.js +29 -146
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1877,12 +1877,6 @@ async function runTurn(opts) {
1877
1877
  let assistantContent = null;
1878
1878
  let toolResults = [];
1879
1879
  let firstHop = true;
1880
- const callCounts = /* @__PURE__ */ new Map();
1881
- const writeCounts = /* @__PURE__ */ new Map();
1882
- let stalledHops = 0;
1883
- let totalGuardHits = 0;
1884
- let emptyHops = 0;
1885
- const MAX_EMPTY_RETRIES = 1;
1886
1880
  const steering = buildSteeringContext(host.cwd ?? process.cwd(), opts.message);
1887
1881
  const firstMessage = steering ? `${steering}
1888
1882
 
@@ -1897,7 +1891,6 @@ ${opts.message}` : opts.message;
1897
1891
  throw new AbortedError();
1898
1892
  emit({ type: "thinking" });
1899
1893
  const pending = [];
1900
- const textBefore = text.length;
1901
1894
  const done = await streamChat(credentials, {
1902
1895
  ...firstHop ? { message: firstMessage, images: opts.images } : { toolResults, assistantContent },
1903
1896
  conversationId,
@@ -1952,113 +1945,15 @@ ${opts.message}` : opts.message;
1952
1945
  }
1953
1946
  }
1954
1947
  firstHop = false;
1955
- if (!pending.length) {
1956
- const producedText = text.length > textBefore;
1957
- if (!producedText && hop > 0 && emptyHops < MAX_EMPTY_RETRIES) {
1958
- emptyHops++;
1959
- toolResults = [{
1960
- id: `empty-${hop}`,
1961
- result: `[continue] Your last turn was empty \u2014 no message and no tool call. Do NOT stop here. Look at what has been done so far in this conversation and take the NEXT concrete step toward the goal: create the next file, run the next command, or if the task is genuinely complete, give a short final answer stating what you built and how to run it. Act now.`
1962
- }];
1963
- assistantContent = assistantContent ?? "";
1964
- continue;
1965
- }
1966
- if (!producedText && emptyHops >= MAX_EMPTY_RETRIES) {
1967
- const wroteSomething = writeCounts.size > 0;
1968
- emit({ type: "error", message: wroteSomething ? `Stopping: the last steps returned nothing new. What I changed is saved \u2014 tell me the specific next change and I'll make it directly.` : `Stopping: I couldn't make progress on that. Tell me the specific step you want (e.g. "create the server file") and I'll do it directly.` });
1969
- }
1948
+ if (!pending.length)
1970
1949
  break;
1950
+ const ran = await executePending(pending, opts, emit, checkpoint);
1951
+ if (ran === null) {
1952
+ return finish({ conversationId, turnCredits, balance, text, aborted: true });
1971
1953
  }
1972
- emptyHops = 0;
1973
- const toRun = [];
1974
- const shortCircuited = [];
1975
- for (const call of pending) {
1976
- const fp = `${call.name}:${stableStringify(call.input)}`;
1977
- const seen = (callCounts.get(fp) ?? 0) + 1;
1978
- callCounts.set(fp, seen);
1979
- const path = typeof call.input?.path === "string" ? call.input.path : void 0;
1980
- if (seen > MAX_IDENTICAL_CALLS && isReadOnlyTool(call.name)) {
1981
- shortCircuited.push({
1982
- id: call.id,
1983
- result: `[loop guard] You already ran ${call.name}(${compactArgs(call.input)}) and nothing has changed since \u2014 re-reading it will keep returning the same thing. This call was NOT executed. STOP re-inspecting and DECIDE from what you already know: if a file the task needs does not exist yet, WRITE it now (write_file) \u2014 an empty or missing directory means the file has not been created, not that you should look again. If every file the task needs already exists and has been run/verified, give your final answer now (what you built + how to run it) and end. Do NOT read or list again.`
1984
- });
1985
- emit({ type: "tool:end", id: call.id, tool: call.name, ok: false, durationMs: 0, summary: "loop guard: repeated read skipped" });
1986
- } else if (WRITE_TOOLS.has(call.name) && path && (writeCounts.get(path) ?? 0) >= MAX_WRITES_PER_FILE) {
1987
- shortCircuited.push({
1988
- id: call.id,
1989
- result: `[loop guard] You have already written ${path} ${writeCounts.get(path)} times this turn \u2014 its content is on disk. This write was NOT executed. Stop rewriting it: verify it (run it / typecheck it) or give your final answer. Do not write this file again unless you are making a specific, different change.`
1990
- });
1991
- emit({ type: "tool:end", id: call.id, tool: call.name, ok: false, durationMs: 0, summary: "loop guard: write thrash skipped" });
1992
- } else {
1993
- toRun.push(call);
1994
- }
1995
- }
1996
- totalGuardHits += shortCircuited.length;
1997
- if (toRun.length === 0 && shortCircuited.length > 0)
1998
- stalledHops++;
1999
- else
2000
- stalledHops = 0;
2001
- const wroteThisHop = toRun.some((c2) => WRITE_TOOLS.has(c2.name));
2002
- if (wroteThisHop) {
2003
- stalledHops = 0;
2004
- }
2005
- const filesWritten = writeCounts.size;
2006
- const guardHitCeiling = 6 + filesWritten * 3;
2007
- const stallCeiling = filesWritten > 0 ? 4 : 2;
2008
- if (stalledHops >= stallCeiling || totalGuardHits >= guardHitCeiling) {
2009
- const wroteSomething = filesWritten > 0;
2010
- emit({ type: "error", message: wroteSomething ? `Stopping: I kept re-reading the same files without making new progress. What I built so far is saved. Tell me the specific next change (e.g. "write the UI file" / "start it") and I'll do it directly.` : `Stopping: I kept re-reading the same files without writing anything. I have enough context now \u2014 tell me the specific change you want and I'll make it directly.` });
2011
- return finish({ conversationId, turnCredits, balance, text, aborted: false });
2012
- }
2013
- for (const c2 of toRun) {
2014
- if (!MUTATION_TOOLS.has(c2.name))
2015
- continue;
2016
- const p = typeof c2.input?.path === "string" ? c2.input.path : void 0;
2017
- if (!p)
2018
- continue;
2019
- if (WRITE_TOOLS.has(c2.name))
2020
- writeCounts.set(p, (writeCounts.get(p) ?? 0) + 1);
2021
- for (const key of [...callCounts.keys()]) {
2022
- if (key.includes(JSON.stringify(p)))
2023
- callCounts.delete(key);
2024
- }
2025
- }
2026
- let results = shortCircuited;
2027
- if (toRun.length) {
2028
- const ran = await executePending(toRun, opts, emit, checkpoint);
2029
- if (ran === null) {
2030
- return finish({ conversationId, turnCredits, balance, text, aborted: true });
2031
- }
2032
- for (const call of toRun) {
2033
- if (!WRITE_TOOLS.has(call.name))
2034
- continue;
2035
- const res = ran.find((r) => r.id === call.id);
2036
- if (!res || /^Error:/i.test(res.result))
2037
- continue;
2038
- const path = String(call.input?.path ?? "");
2039
- if (!path)
2040
- continue;
2041
- let hookResults;
2042
- try {
2043
- hookResults = await runPostWriteHooks(host.cwd ?? process.cwd(), path);
2044
- } catch {
2045
- hookResults = [];
2046
- }
2047
- for (const h of hookResults) {
2048
- emit({ type: "tool:end", id: `${call.id}:${h.name}`, tool: h.name, ok: h.ok, durationMs: 0, summary: h.ok ? `${h.name} passed` : `${h.name} failed` });
2049
- res.result += h.ok ? `
2050
-
2051
- [${h.name}] passed on ${path}.` : `
2052
-
2053
- [${h.name}] FAILED after this write \u2014 fix before continuing:
2054
- ${h.output}`;
2055
- }
2056
- }
2057
- results = [...shortCircuited, ...ran];
2058
- }
2059
- toolResults = results;
1954
+ toolResults = ran;
2060
1955
  if (hop === MAX_HOPS - 1) {
2061
- emit({ type: "error", message: `Paused after ${MAX_HOPS} steps to check in - this is a long task, not necessarily a finished one. Say "continue" and I'll pick up exactly where I left off.` });
1956
+ emit({ type: "error", message: `Paused after ${MAX_HOPS} steps to check in \u2014 this is a long task, not necessarily a finished one. Say "continue" and I'll pick up exactly where I left off.` });
2062
1957
  }
2063
1958
  }
2064
1959
  emit({
@@ -2097,21 +1992,6 @@ function finish(r) {
2097
1992
  error: r.error
2098
1993
  };
2099
1994
  }
2100
- function isReadOnlyTool(name) {
2101
- return READ_ONLY_TOOLS.has(name);
2102
- }
2103
- function stableStringify(v) {
2104
- if (v === null || typeof v !== "object")
2105
- return JSON.stringify(v);
2106
- if (Array.isArray(v))
2107
- return `[${v.map(stableStringify).join(",")}]`;
2108
- const o = v;
2109
- return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
2110
- }
2111
- function compactArgs(input) {
2112
- const s = Object.entries(input).map(([k, val]) => `${k}: ${JSON.stringify(val)}`).join(", ");
2113
- return s.length > 120 ? `${s.slice(0, 117)}...` : s;
2114
- }
2115
1995
  async function executePending(calls, opts, emit, checkpoint) {
2116
1996
  const results = [];
2117
1997
  for (const call of calls) {
@@ -2199,7 +2079,25 @@ async function executePending(calls, opts, emit, checkpoint) {
2199
2079
  }
2200
2080
  const bytes = effect.kind === "wrote" || effect.kind === "modified" ? byteLength(call.input?.content) : void 0;
2201
2081
  emit({ type: "tool:end", id: call.id, tool: call.name, ok: !failed2, durationMs: Date.now() - started, effect, bytes });
2202
- results.push({ id: call.id, result: truncate(output) });
2082
+ let resultText = output;
2083
+ if (!failed2 && WRITE_TOOLS.has(call.name) && typeof call.input?.path === "string" && call.input.path) {
2084
+ let hookResults;
2085
+ try {
2086
+ hookResults = await runPostWriteHooks(opts.host.cwd ?? process.cwd(), String(call.input.path));
2087
+ } catch {
2088
+ hookResults = [];
2089
+ }
2090
+ for (const h of hookResults) {
2091
+ emit({ type: "tool:end", id: `${call.id}:${h.name}`, tool: h.name, ok: h.ok, durationMs: 0, summary: h.ok ? `${h.name} passed` : `${h.name} failed` });
2092
+ resultText += h.ok ? `
2093
+
2094
+ [${h.name}] passed on ${call.input.path}.` : `
2095
+
2096
+ [${h.name}] FAILED after this write \u2014 fix before continuing:
2097
+ ${h.output}`;
2098
+ }
2099
+ }
2100
+ results.push({ id: call.id, result: truncate(resultText) });
2203
2101
  }
2204
2102
  return results;
2205
2103
  }
@@ -2247,7 +2145,7 @@ function targetOf(call) {
2247
2145
  return void 0;
2248
2146
  return s.length > 60 ? `${s.slice(0, 57)}\u2026` : s;
2249
2147
  }
2250
- var MAX_HOPS, MAX_IDENTICAL_CALLS, MAX_WRITES_PER_FILE, MAX_RESULT_CHARS, READ_ONLY_TOOLS, MUTATION_TOOLS, WRITE_TOOLS;
2148
+ var MAX_HOPS, MAX_RESULT_CHARS, MUTATION_TOOLS, WRITE_TOOLS;
2251
2149
  var init_loop = __esm({
2252
2150
  "node_modules/@hostwares/agent-client/dist/esm/agent/loop.js"() {
2253
2151
  init_client();
@@ -2259,28 +2157,13 @@ var init_loop = __esm({
2259
2157
  init_subagents();
2260
2158
  init_safety();
2261
2159
  init_events();
2262
- MAX_HOPS = 50;
2263
- MAX_IDENTICAL_CALLS = 2;
2264
- MAX_WRITES_PER_FILE = 2;
2160
+ MAX_HOPS = 80;
2265
2161
  MAX_RESULT_CHARS = 3e4;
2266
- READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
2267
- "read_file",
2268
- "list_directory",
2269
- "search_files",
2270
- "file_search",
2271
- "get_system_info",
2272
- "list_processes",
2273
- "get_process_output",
2274
- "check_port",
2275
- "git_status",
2276
- "git_log",
2277
- "git_diff",
2278
- "docker_ps"
2279
- ]);
2280
2162
  MUTATION_TOOLS = /* @__PURE__ */ new Set([
2281
2163
  "write_file",
2282
2164
  "str_replace_file",
2283
2165
  "append_file",
2166
+ "edit_file",
2284
2167
  "delete_path",
2285
2168
  "git_commit",
2286
2169
  "git_add",
@@ -2290,7 +2173,7 @@ var init_loop = __esm({
2290
2173
  "stop_process",
2291
2174
  "kill_process"
2292
2175
  ]);
2293
- WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "str_replace_file", "append_file"]);
2176
+ WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "str_replace_file", "append_file", "edit_file"]);
2294
2177
  }
2295
2178
  });
2296
2179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostwares-cli",
3
- "version": "2.4.3",
3
+ "version": "2.4.4",
4
4
  "description": "Hostwares CLI - AI DevOps in your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",