reasonix 0.30.3 → 0.30.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1480,6 +1480,10 @@ var SessionStats = class {
1480
1480
  _carryoverCost = 0;
1481
1481
  /** Turn count from prior runs of a resumed session. */
1482
1482
  _carryoverTurns = 0;
1483
+ _carryoverCacheHit = 0;
1484
+ _carryoverCacheMiss = 0;
1485
+ /** Last turn's promptTokens before exit — surfaced via summary() until the next live turn lands. */
1486
+ _carryoverLastPromptTokens = 0;
1483
1487
  /** Seed totals from a resumed session's persisted meta — only call once at construction. */
1484
1488
  seedCarryover(opts) {
1485
1489
  if (typeof opts.totalCostUsd === "number" && opts.totalCostUsd > 0) {
@@ -1488,6 +1492,15 @@ var SessionStats = class {
1488
1492
  if (typeof opts.turnCount === "number" && opts.turnCount > 0) {
1489
1493
  this._carryoverTurns = opts.turnCount;
1490
1494
  }
1495
+ if (typeof opts.cacheHitTokens === "number" && opts.cacheHitTokens > 0) {
1496
+ this._carryoverCacheHit = opts.cacheHitTokens;
1497
+ }
1498
+ if (typeof opts.cacheMissTokens === "number" && opts.cacheMissTokens > 0) {
1499
+ this._carryoverCacheMiss = opts.cacheMissTokens;
1500
+ }
1501
+ if (typeof opts.lastPromptTokens === "number" && opts.lastPromptTokens > 0) {
1502
+ this._carryoverLastPromptTokens = opts.lastPromptTokens;
1503
+ }
1491
1504
  }
1492
1505
  record(turn, model, usage) {
1493
1506
  const cost = costUsd(model, usage);
@@ -1518,8 +1531,8 @@ var SessionStats = class {
1518
1531
  return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
1519
1532
  }
1520
1533
  get aggregateCacheHitRatio() {
1521
- let hit = 0;
1522
- let miss = 0;
1534
+ let hit = this._carryoverCacheHit;
1535
+ let miss = this._carryoverCacheMiss;
1523
1536
  for (const t of this.turns) {
1524
1537
  hit += t.usage.promptCacheHitTokens;
1525
1538
  miss += t.usage.promptCacheMissTokens;
@@ -1537,7 +1550,7 @@ var SessionStats = class {
1537
1550
  claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
1538
1551
  savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
1539
1552
  cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
1540
- lastPromptTokens: last?.usage.promptTokens ?? 0,
1553
+ lastPromptTokens: last?.usage.promptTokens ?? this._carryoverLastPromptTokens,
1541
1554
  lastTurnCostUsd: round(last?.cost ?? 0, 6)
1542
1555
  };
1543
1556
  }
@@ -2269,15 +2282,18 @@ var StormBreaker = class {
2269
2282
  windowSize;
2270
2283
  threshold;
2271
2284
  isMutating;
2285
+ isStormExempt;
2272
2286
  recent = [];
2273
- constructor(windowSize = 6, threshold = 3, isMutating) {
2287
+ constructor(windowSize = 6, threshold = 3, isMutating, isStormExempt) {
2274
2288
  this.windowSize = windowSize;
2275
2289
  this.threshold = threshold;
2276
2290
  this.isMutating = isMutating;
2291
+ this.isStormExempt = isStormExempt;
2277
2292
  }
2278
2293
  inspect(call) {
2279
2294
  const name = call.function?.name;
2280
2295
  if (!name) return { suppress: false };
2296
+ if (this.isStormExempt?.(call)) return { suppress: false };
2281
2297
  const args = call.function?.arguments ?? "";
2282
2298
  const mutating = this.isMutating ? this.isMutating(call) : false;
2283
2299
  const readOnly = !mutating;
@@ -2378,7 +2394,12 @@ var ToolCallRepair = class {
2378
2394
  opts;
2379
2395
  constructor(opts) {
2380
2396
  this.opts = opts;
2381
- this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3, opts.isMutating);
2397
+ this.storm = new StormBreaker(
2398
+ opts.stormWindow ?? 6,
2399
+ opts.stormThreshold ?? 3,
2400
+ opts.isMutating,
2401
+ opts.isStormExempt
2402
+ );
2382
2403
  }
2383
2404
  /** Called at start of every user turn — fresh intent shouldn't inherit old repetition state. */
2384
2405
  resetStorm() {
@@ -2521,9 +2542,15 @@ var CacheFirstLoop = class {
2521
2542
  }
2522
2543
  return def.readOnly !== true;
2523
2544
  };
2545
+ const isStormExempt = (call) => {
2546
+ const name = call.function?.name;
2547
+ if (!name) return false;
2548
+ return registry.get(name)?.stormExempt === true;
2549
+ };
2524
2550
  this.repair = new ToolCallRepair({
2525
2551
  allowedToolNames: allowedNames,
2526
2552
  isMutating,
2553
+ isStormExempt,
2527
2554
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
2528
2555
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
2529
2556
  });
@@ -2541,7 +2568,10 @@ var CacheFirstLoop = class {
2541
2568
  const meta = loadSessionMeta(this.sessionName);
2542
2569
  this.stats.seedCarryover({
2543
2570
  totalCostUsd: meta.totalCostUsd,
2544
- turnCount: meta.turnCount
2571
+ turnCount: meta.turnCount,
2572
+ cacheHitTokens: meta.cacheHitTokens,
2573
+ cacheMissTokens: meta.cacheMissTokens,
2574
+ lastPromptTokens: meta.lastPromptTokens
2545
2575
  });
2546
2576
  }
2547
2577
  if (healedCount > 0) {
@@ -3637,19 +3667,19 @@ ${mem.content}
3637
3667
  import { createHash as createHash2 } from "crypto";
3638
3668
  import {
3639
3669
  existsSync as existsSync7,
3640
- mkdirSync as mkdirSync2,
3670
+ mkdirSync as mkdirSync3,
3641
3671
  readFileSync as readFileSync8,
3642
3672
  readdirSync as readdirSync4,
3643
3673
  unlinkSync as unlinkSync2,
3644
- writeFileSync as writeFileSync2
3674
+ writeFileSync as writeFileSync3
3645
3675
  } from "fs";
3646
3676
  import { homedir as homedir4 } from "os";
3647
3677
  import { join as join7, resolve as resolve3 } from "path";
3648
3678
 
3649
3679
  // src/skills.ts
3650
- import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
3680
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
3651
3681
  import { homedir as homedir3 } from "os";
3652
- import { join as join6, resolve as resolve2 } from "path";
3682
+ import { dirname as dirname3, join as join6, resolve as resolve2 } from "path";
3653
3683
 
3654
3684
  // src/prompt-fragments.ts
3655
3685
  var TUI_FORMATTING_RULES = `Formatting (rendered in a TUI with a real markdown renderer):
@@ -3754,6 +3784,31 @@ var SkillStore = class {
3754
3784
  }
3755
3785
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
3756
3786
  }
3787
+ /** Scaffold a new skill stub at the chosen scope. Refuses to overwrite. */
3788
+ create(name, scope) {
3789
+ if (!isValidSkillName(name)) {
3790
+ return { error: `invalid skill name: "${name}" \u2014 use letters, digits, _, -, .` };
3791
+ }
3792
+ if (scope === "project" && !this.projectRoot) {
3793
+ return { error: "project scope requires a workspace \u2014 run from `reasonix code`" };
3794
+ }
3795
+ const root = scope === "project" ? join6(this.projectRoot ?? "", ".reasonix", SKILLS_DIRNAME) : join6(this.homeDir, ".reasonix", SKILLS_DIRNAME);
3796
+ const flat = join6(root, `${name}.md`);
3797
+ const folder = join6(root, name, SKILL_FILE);
3798
+ if (existsSync6(folder)) {
3799
+ return { error: `skill "${name}" already exists at ${folder}` };
3800
+ }
3801
+ mkdirSync2(dirname3(flat), { recursive: true });
3802
+ try {
3803
+ writeFileSync2(flat, skillStubBody(name), { encoding: "utf8", flag: "wx" });
3804
+ } catch (err) {
3805
+ if (err.code === "EEXIST") {
3806
+ return { error: `skill "${name}" already exists at ${flat}` };
3807
+ }
3808
+ throw err;
3809
+ }
3810
+ return { path: flat };
3811
+ }
3757
3812
  /** Resolve one skill by name. Returns `null` if not found or malformed. */
3758
3813
  read(name) {
3759
3814
  if (!isValidSkillName(name)) return null;
@@ -3813,6 +3868,22 @@ var SkillStore = class {
3813
3868
  function parseRunAs(raw) {
3814
3869
  return raw?.trim() === "subagent" ? "subagent" : "inline";
3815
3870
  }
3871
+ function skillStubBody(name) {
3872
+ return `---
3873
+ name: ${name}
3874
+ description: One-liner \u2014 what does this skill do?
3875
+ ---
3876
+
3877
+ # ${name}
3878
+
3879
+ Replace this body with the playbook the model should follow when this skill is invoked.
3880
+
3881
+ Tips:
3882
+ - Reference tools by name (run_command, edit_file, search_content, ...)
3883
+ - Add \`runAs: subagent\` to frontmatter to spawn an isolated subagent loop
3884
+ - Add \`allowed-tools: read_file, search_content\` to scope a subagent's tools
3885
+ `;
3886
+ }
3816
3887
  function skillIndexLine(s) {
3817
3888
  const safeDesc = s.description.replace(/\n/g, " ").trim();
3818
3889
  const tag = s.runAs === "subagent" ? " [\u{1F9EC} subagent]" : "";
@@ -4055,7 +4126,7 @@ function scopeDir(opts) {
4055
4126
  return join7(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
4056
4127
  }
4057
4128
  function ensureDir(p) {
4058
- if (!existsSync7(p)) mkdirSync2(p, { recursive: true });
4129
+ if (!existsSync7(p)) mkdirSync3(p, { recursive: true });
4059
4130
  }
4060
4131
  function parseFrontmatter2(raw) {
4061
4132
  const lines = raw.split(/\r?\n/);
@@ -4200,7 +4271,7 @@ var MemoryStore = class {
4200
4271
  const file = join7(dir, `${name}.md`);
4201
4272
  const content = `${formatFrontmatter(entry)}${body}
4202
4273
  `;
4203
- writeFileSync2(file, content, "utf8");
4274
+ writeFileSync3(file, content, "utf8");
4204
4275
  this.regenerateIndex(input.scope);
4205
4276
  return file;
4206
4277
  }
@@ -4241,7 +4312,7 @@ var MemoryStore = class {
4241
4312
  lines.push(`- [${name}](${name}.md) \u2014 (malformed, check frontmatter)`);
4242
4313
  }
4243
4314
  }
4244
- writeFileSync2(indexPath, `${lines.join("\n")}
4315
+ writeFileSync3(indexPath, `${lines.join("\n")}
4245
4316
  `, "utf8");
4246
4317
  }
4247
4318
  };
@@ -4662,7 +4733,9 @@ function registerFilesystemTools(registry, opts) {
4662
4733
  const normRoot = pathMod3.resolve(rootDir);
4663
4734
  const rel = pathMod3.relative(normRoot, resolved);
4664
4735
  if (rel.startsWith("..") || pathMod3.isAbsolute(rel)) {
4665
- throw new Error(`path escapes sandbox root (${normRoot}): ${raw}`);
4736
+ throw new Error(
4737
+ `path escapes sandbox root (${normRoot}): ${raw} \u2014 workspace is pinned at launch; quit and relaunch with \`reasonix code --dir <path>\` to work in a different folder`
4738
+ );
4666
4739
  }
4667
4740
  return resolved;
4668
4741
  };
@@ -4675,6 +4748,7 @@ function registerFilesystemTools(registry, opts) {
4675
4748
  - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
4676
4749
  When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_LINES} lines, the tool auto-returns a head+tail preview with an "N lines omitted" marker rather than dumping everything. If you need the middle, re-call with a range. Prefer search_content to locate a symbol first, then read_file with a range around the hit \u2014 one scoped read beats three full-file reads.`,
4677
4750
  readOnly: true,
4751
+ stormExempt: true,
4678
4752
  parameters: {
4679
4753
  type: "object",
4680
4754
  properties: {
@@ -4755,6 +4829,7 @@ ${slice.join("\n")}`;
4755
4829
  parallelSafe: true,
4756
4830
  description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
4757
4831
  readOnly: true,
4832
+ stormExempt: true,
4758
4833
  parameters: {
4759
4834
  type: "object",
4760
4835
  properties: {
@@ -5856,9 +5931,9 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
5856
5931
  import * as pathMod7 from "path";
5857
5932
 
5858
5933
  // src/config.ts
5859
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
5934
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
5860
5935
  import { homedir as homedir5 } from "os";
5861
- import { dirname as dirname4, join as join10 } from "path";
5936
+ import { dirname as dirname5, join as join10 } from "path";
5862
5937
  function defaultConfigPath() {
5863
5938
  return join10(homedir5(), ".reasonix", "config.json");
5864
5939
  }
@@ -5872,8 +5947,8 @@ function readConfig(path2 = defaultConfigPath()) {
5872
5947
  return {};
5873
5948
  }
5874
5949
  function writeConfig(cfg, path2 = defaultConfigPath()) {
5875
- mkdirSync3(dirname4(path2), { recursive: true });
5876
- writeFileSync3(path2, JSON.stringify(cfg, null, 2), "utf8");
5950
+ mkdirSync4(dirname5(path2), { recursive: true });
5951
+ writeFileSync4(path2, JSON.stringify(cfg, null, 2), "utf8");
5877
5952
  try {
5878
5953
  chmodSync2(path2, 384);
5879
5954
  } catch {
@@ -6015,7 +6090,8 @@ var JobRegistry = class {
6015
6090
  },
6016
6091
  closedPromise: Promise.resolve(),
6017
6092
  signalClosed: () => {
6018
- }
6093
+ },
6094
+ outputWaiters: /* @__PURE__ */ new Set()
6019
6095
  };
6020
6096
  this.jobs.set(id2, job2);
6021
6097
  return {
@@ -6051,7 +6127,8 @@ var JobRegistry = class {
6051
6127
  readyPromise,
6052
6128
  signalReady: readyResolve,
6053
6129
  closedPromise,
6054
- signalClosed: closedResolve
6130
+ signalClosed: closedResolve,
6131
+ outputWaiters: /* @__PURE__ */ new Set()
6055
6132
  };
6056
6133
  this.jobs.set(id, job);
6057
6134
  let readyMatched = false;
@@ -6078,6 +6155,11 @@ ${job.output.slice(start)}`;
6078
6155
  }
6079
6156
  }
6080
6157
  }
6158
+ if (job.outputWaiters.size > 0) {
6159
+ const waiters = [...job.outputWaiters];
6160
+ job.outputWaiters.clear();
6161
+ for (const wake of waiters) wake();
6162
+ }
6081
6163
  };
6082
6164
  child.stdout?.on("data", onData);
6083
6165
  child.stderr?.on("data", onData);
@@ -6139,6 +6221,39 @@ ${job.output.slice(start)}`;
6139
6221
  spawnError: job.spawnError
6140
6222
  };
6141
6223
  }
6224
+ async waitForJob(id, opts = {}) {
6225
+ const job = this.jobs.get(id);
6226
+ if (!job) return null;
6227
+ if (!job.running) {
6228
+ return {
6229
+ exited: true,
6230
+ exitCode: job.exitCode,
6231
+ latestOutput: job.output
6232
+ };
6233
+ }
6234
+ const timeoutMs = Math.max(0, Math.min(3e4, opts.timeoutMs ?? 5e3));
6235
+ const startOutput = job.output;
6236
+ let wakeOutput = null;
6237
+ const outputPromise = new Promise((resolve10) => {
6238
+ wakeOutput = resolve10;
6239
+ job.outputWaiters.add(resolve10);
6240
+ });
6241
+ let timer = null;
6242
+ await Promise.race([
6243
+ job.closedPromise,
6244
+ outputPromise,
6245
+ new Promise((resolve10) => {
6246
+ timer = setTimeout(resolve10, timeoutMs);
6247
+ })
6248
+ ]);
6249
+ if (timer) clearTimeout(timer);
6250
+ if (wakeOutput) job.outputWaiters.delete(wakeOutput);
6251
+ return {
6252
+ exited: !job.running,
6253
+ exitCode: job.exitCode,
6254
+ latestOutput: latestOutputSince(startOutput, job.output)
6255
+ };
6256
+ }
6142
6257
  /** SIGTERM, wait graceMs, then SIGKILL. Idempotent on already-exited jobs. */
6143
6258
  async stop(id, opts = {}) {
6144
6259
  const job = this.jobs.get(id);
@@ -6218,6 +6333,11 @@ function snapshot(job) {
6218
6333
  spawnError: job.spawnError
6219
6334
  };
6220
6335
  }
6336
+ function latestOutputSince(before, after) {
6337
+ if (!before) return after;
6338
+ if (after.startsWith(before)) return after.slice(before.length);
6339
+ return after;
6340
+ }
6221
6341
 
6222
6342
  // src/tools/shell/exec.ts
6223
6343
  import { spawn as spawn4, spawnSync } from "child_process";
@@ -7197,6 +7317,7 @@ function registerShellTools(registry, opts) {
7197
7317
  description: "Read the latest output of a background job started with `run_background`. By default returns the tail of the buffer (last 80 lines). Pass `since` (the `byteLength` from a previous call) to stream only new content incrementally. Tells you whether the job is still running, so you can stop polling when it's done.",
7198
7318
  readOnly: true,
7199
7319
  parallelSafe: true,
7320
+ stormExempt: true,
7200
7321
  parameters: {
7201
7322
  type: "object",
7202
7323
  properties: {
@@ -7221,6 +7342,32 @@ function registerShellTools(registry, opts) {
7221
7342
  return formatJobRead(args.jobId, out);
7222
7343
  }
7223
7344
  });
7345
+ registry.register({
7346
+ name: "wait_for_job",
7347
+ description: "Block until a background job exits or produces new output, bounded by `timeoutMs`. Use this instead of polling `job_output` with identical args when you're intentionally waiting for state to change. Returns JSON with `exited`, `exitCode`, and `latestOutput`.",
7348
+ readOnly: true,
7349
+ parameters: {
7350
+ type: "object",
7351
+ properties: {
7352
+ jobId: { type: "integer", description: "Job id returned by run_background." },
7353
+ timeoutMs: {
7354
+ type: "integer",
7355
+ description: "Max time to block before returning if nothing changes. Clamped to 0..30000. Default 5000."
7356
+ }
7357
+ },
7358
+ required: ["jobId"]
7359
+ },
7360
+ fn: async (args) => {
7361
+ const out = await jobs.waitForJob(args.jobId, { timeoutMs: args.timeoutMs });
7362
+ if (!out) return `job ${args.jobId}: not found (use list_jobs)`;
7363
+ return {
7364
+ jobId: args.jobId,
7365
+ exited: out.exited,
7366
+ exitCode: out.exitCode,
7367
+ latestOutput: out.latestOutput
7368
+ };
7369
+ }
7370
+ });
7224
7371
  registry.register({
7225
7372
  name: "stop_job",
7226
7373
  description: "Stop a background job started with `run_background`. SIGTERM first; SIGKILL after a short grace period if it doesn't exit cleanly. Returns the final output + exit code. Safe to call on an already-exited job.",
@@ -7242,6 +7389,7 @@ function registerShellTools(registry, opts) {
7242
7389
  description: "List every background job started this session \u2014 running and exited \u2014 with id, command, pid, status. Use when you've lost track of which job_id corresponds to which process, or to see what's still alive.",
7243
7390
  readOnly: true,
7244
7391
  parallelSafe: true,
7392
+ stormExempt: true,
7245
7393
  parameters: { type: "object", properties: {} },
7246
7394
  fn: async () => {
7247
7395
  const all = jobs.list();
@@ -8174,16 +8322,16 @@ function truncate(s, n) {
8174
8322
  }
8175
8323
 
8176
8324
  // src/version.ts
8177
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
8325
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
8178
8326
  import { homedir as homedir6 } from "os";
8179
- import { dirname as dirname5, join as join11 } from "path";
8327
+ import { dirname as dirname6, join as join11 } from "path";
8180
8328
  import { fileURLToPath as fileURLToPath2 } from "url";
8181
8329
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
8182
8330
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
8183
8331
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
8184
8332
  function readPackageVersion() {
8185
8333
  try {
8186
- let dir = dirname5(fileURLToPath2(import.meta.url));
8334
+ let dir = dirname6(fileURLToPath2(import.meta.url));
8187
8335
  for (let i = 0; i < 6; i++) {
8188
8336
  const p = join11(dir, "package.json");
8189
8337
  if (existsSync9(p)) {
@@ -8192,7 +8340,7 @@ function readPackageVersion() {
8192
8340
  return pkg.version;
8193
8341
  }
8194
8342
  }
8195
- const parent = dirname5(dir);
8343
+ const parent = dirname6(dir);
8196
8344
  if (parent === dir) break;
8197
8345
  dir = parent;
8198
8346
  }
@@ -8218,8 +8366,8 @@ function readCache(homeDirOverride) {
8218
8366
  function writeCache(entry, homeDirOverride) {
8219
8367
  try {
8220
8368
  const p = cachePath(homeDirOverride);
8221
- mkdirSync4(dirname5(p), { recursive: true });
8222
- writeFileSync4(p, JSON.stringify(entry), "utf8");
8369
+ mkdirSync5(dirname6(p), { recursive: true });
8370
+ writeFileSync5(p, JSON.stringify(entry), "utf8");
8223
8371
  } catch {
8224
8372
  }
8225
8373
  }
@@ -9023,15 +9171,15 @@ import {
9023
9171
  existsSync as existsSync10,
9024
9172
  fstatSync,
9025
9173
  ftruncateSync,
9026
- mkdirSync as mkdirSync5,
9174
+ mkdirSync as mkdirSync6,
9027
9175
  openSync as openSync2,
9028
9176
  readFileSync as readFileSync13,
9029
9177
  readSync,
9030
9178
  unlinkSync as unlinkSync3,
9031
- writeFileSync as writeFileSync5,
9179
+ writeFileSync as writeFileSync6,
9032
9180
  writeSync
9033
9181
  } from "fs";
9034
- import { dirname as dirname6, resolve as resolve9 } from "path";
9182
+ import { dirname as dirname7, resolve as resolve9 } from "path";
9035
9183
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
9036
9184
  function parseEditBlocks(text) {
9037
9185
  const out = [];
@@ -9061,7 +9209,7 @@ function applyEditBlock(block, rootDir) {
9061
9209
  const searchEmpty = block.search.length === 0;
9062
9210
  if (searchEmpty) {
9063
9211
  try {
9064
- mkdirSync5(dirname6(absTarget), { recursive: true });
9212
+ mkdirSync6(dirname7(absTarget), { recursive: true });
9065
9213
  const fd = openSync2(absTarget, "wx");
9066
9214
  try {
9067
9215
  writeSync(fd, block.replace);
@@ -9176,7 +9324,7 @@ function restoreSnapshots(snapshots, rootDir) {
9176
9324
  message: "removed (the edit had created it)"
9177
9325
  };
9178
9326
  }
9179
- writeFileSync5(abs, snap.prevContent, "utf8");
9327
+ writeFileSync6(abs, snap.prevContent, "utf8");
9180
9328
  return {
9181
9329
  path: snap.path,
9182
9330
  status: "applied",
@@ -9352,6 +9500,7 @@ You have TWO tools for running shell commands, and picking the right one is non-
9352
9500
 
9353
9501
  After \`run_background\`, tools available to you:
9354
9502
  - \`job_output(jobId, tailLines?)\` \u2014 read recent logs to verify startup / debug errors.
9503
+ - \`wait_for_job(jobId, timeoutMs?)\` \u2014 block until the job exits or emits new output. Prefer this over repeating identical \`job_output\` calls while you're intentionally waiting.
9355
9504
  - \`list_jobs\` \u2014 see every job this session (running + exited).
9356
9505
  - \`stop_job(jobId)\` \u2014 SIGTERM \u2192 SIGKILL after grace. Stop before switching port / config.
9357
9506
 
@@ -9439,17 +9588,17 @@ import {
9439
9588
  closeSync as closeSync3,
9440
9589
  existsSync as existsSync12,
9441
9590
  fstatSync as fstatSync2,
9442
- mkdirSync as mkdirSync6,
9591
+ mkdirSync as mkdirSync7,
9443
9592
  openSync as openSync3,
9444
9593
  readFileSync as readFileSync15,
9445
9594
  readSync as readSync2,
9446
9595
  renameSync as renameSync2,
9447
9596
  statSync as statSync5,
9448
9597
  unlinkSync as unlinkSync4,
9449
- writeFileSync as writeFileSync6
9598
+ writeFileSync as writeFileSync7
9450
9599
  } from "fs";
9451
9600
  import { homedir as homedir7 } from "os";
9452
- import { dirname as dirname7, join as join13 } from "path";
9601
+ import { dirname as dirname8, join as join13 } from "path";
9453
9602
  function defaultUsageLogPath(homeDirOverride) {
9454
9603
  return join13(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
9455
9604
  }
@@ -9490,7 +9639,7 @@ function compactUsageLogIfLarge(path2, now) {
9490
9639
  if (kept.length === lines.filter((l) => l.trim()).length) return;
9491
9640
  const tmp = `${path2}.compacting`;
9492
9641
  try {
9493
- writeFileSync6(tmp, kept.length > 0 ? `${kept.join("\n")}
9642
+ writeFileSync7(tmp, kept.length > 0 ? `${kept.join("\n")}
9494
9643
  ` : "", "utf8");
9495
9644
  renameSync2(tmp, path2);
9496
9645
  } catch {
@@ -9516,7 +9665,7 @@ function appendUsage(input) {
9516
9665
  if (input.subagent) record.subagent = input.subagent;
9517
9666
  const path2 = input.path ?? defaultUsageLogPath();
9518
9667
  try {
9519
- mkdirSync6(dirname7(path2), { recursive: true });
9668
+ mkdirSync7(dirname8(path2), { recursive: true });
9520
9669
  appendFileSync2(path2, `${JSON.stringify(record)}
9521
9670
  `, "utf8");
9522
9671
  compactUsageLogIfLarge(path2, record.ts);