@wrongstack/core 0.8.6 → 0.9.1

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 (54) hide show
  1. package/dist/{agent-bridge-Bad3ZGQ7.d.ts → agent-bridge-DMVOX0cF.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-Cc8sRJOF.d.ts → agent-subagent-runner-R0I0Xusn.d.ts} +3 -3
  3. package/dist/{compactor-Mw7-rNyb.d.ts → compactor-DVTKL7XD.d.ts} +1 -1
  4. package/dist/{config-Bi4Q0fnz.d.ts → config-DoUAEvXH.d.ts} +8 -2
  5. package/dist/{context-z2x5gv_V.d.ts → context-BRNbHmRM.d.ts} +6 -0
  6. package/dist/coordination/index.d.ts +12 -12
  7. package/dist/coordination/index.js +58 -37
  8. package/dist/coordination/index.js.map +1 -1
  9. package/dist/{default-config-DvRSTELf.d.ts → default-config-Uzla85H-.d.ts} +5 -1
  10. package/dist/defaults/index.d.ts +22 -22
  11. package/dist/defaults/index.js +119 -76
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/{events-yCpJD4wa.d.ts → events-CiG9qUM_.d.ts} +1 -1
  14. package/dist/execution/index.d.ts +13 -13
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/extension/index.d.ts +6 -6
  17. package/dist/{index-SxpnwRlJ.d.ts → index-bOl8lzFV.d.ts} +5 -5
  18. package/dist/{index-BvZld4Dr.d.ts → index-u2ROAV0F.d.ts} +17 -8
  19. package/dist/index.d.ts +31 -31
  20. package/dist/index.js +182 -95
  21. package/dist/index.js.map +1 -1
  22. package/dist/infrastructure/index.d.ts +6 -6
  23. package/dist/kernel/index.d.ts +9 -9
  24. package/dist/{mcp-servers-n2L9ohMX.d.ts → mcp-servers-jEoMIBZv.d.ts} +3 -3
  25. package/dist/models/index.d.ts +2 -2
  26. package/dist/{multi-agent-C8uY45AY.d.ts → multi-agent-3ZnTB1aT.d.ts} +2 -2
  27. package/dist/{multi-agent-coordinator-C7YbZUY8.d.ts → multi-agent-coordinator-COwCGIrU.d.ts} +2 -2
  28. package/dist/{null-fleet-bus-Bb2jVLKr.d.ts → null-fleet-bus-C1grTnOF.d.ts} +6 -6
  29. package/dist/observability/index.d.ts +2 -2
  30. package/dist/{path-resolver-DnC1nOjb.d.ts → path-resolver-TcJfc29Y.d.ts} +2 -2
  31. package/dist/{permission-policy-BBa1M1xc.d.ts → permission-policy-BpCGYBud.d.ts} +2 -2
  32. package/dist/{plan-templates-Cqdy6rtM.d.ts → plan-templates-ByT1HcRI.d.ts} +17 -7
  33. package/dist/{provider-runner-C4ouU7--.d.ts → provider-runner-BEpikbbN.d.ts} +3 -3
  34. package/dist/{retry-policy-OwtKNxo8.d.ts → retry-policy-BYkq0ugs.d.ts} +1 -1
  35. package/dist/sdd/index.d.ts +8 -8
  36. package/dist/{secret-scrubber-CyE1-EMG.d.ts → secret-scrubber-DttNiGYA.d.ts} +1 -1
  37. package/dist/{secret-scrubber-C0n1EqrC.d.ts → secret-scrubber-QSeI0ADi.d.ts} +1 -1
  38. package/dist/security/index.d.ts +4 -4
  39. package/dist/security/index.js +5 -2
  40. package/dist/security/index.js.map +1 -1
  41. package/dist/{selector-DkvgYVS4.d.ts → selector-pox8abg0.d.ts} +1 -1
  42. package/dist/{session-reader-DsadjyF9.d.ts → session-reader-CSWcb5Ga.d.ts} +1 -1
  43. package/dist/storage/index.d.ts +9 -6
  44. package/dist/storage/index.js +109 -63
  45. package/dist/storage/index.js.map +1 -1
  46. package/dist/{system-prompt-CWA6ml-d.d.ts → system-prompt-Bs-Wliab.d.ts} +1 -1
  47. package/dist/{tool-executor-Ch25BDwa.d.ts → tool-executor-D7a00JVJ.d.ts} +4 -4
  48. package/dist/types/index.d.ts +16 -16
  49. package/dist/types/index.js +8 -2
  50. package/dist/types/index.js.map +1 -1
  51. package/dist/utils/index.d.ts +1 -1
  52. package/dist/utils/index.js +3 -1
  53. package/dist/utils/index.js.map +1 -1
  54. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { randomBytes, randomUUID, createHash } from 'crypto';
2
2
  import * as fsp from 'fs/promises';
3
- import * as path2 from 'path';
3
+ import * as path7 from 'path';
4
4
  import * as os from 'os';
5
5
 
6
6
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
@@ -10,9 +10,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
10
  throw Error('Dynamic require of "' + x + '" is not supported');
11
11
  });
12
12
  async function atomicWrite(targetPath, content, opts = {}) {
13
- const dir = path2.dirname(targetPath);
13
+ const dir = path7.dirname(targetPath);
14
14
  await fsp.mkdir(dir, { recursive: true });
15
- const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
15
+ const tmp = path7.join(dir, `.${path7.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
16
16
  try {
17
17
  if (typeof content === "string") {
18
18
  await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -68,7 +68,7 @@ async function renameWithRetry(from, to) {
68
68
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
69
69
  throw err;
70
70
  }
71
- await new Promise((resolve2) => setTimeout(resolve2, delays[i]));
71
+ await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
72
72
  }
73
73
  }
74
74
  throw lastErr;
@@ -174,24 +174,30 @@ var DefaultSessionStore = class {
174
174
  this.dir = opts.dir;
175
175
  this.events = opts.events;
176
176
  }
177
- async create(meta) {
177
+ /** Join session ID to its absolute path within the store directory. */
178
+ sessionPath(id, ext) {
179
+ return path7.join(this.dir, `${id}${ext}`);
180
+ }
181
+ async ensureShardDir(_id) {
178
182
  await ensureDir(this.dir);
183
+ return this.dir;
184
+ }
185
+ async create(meta) {
179
186
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
180
187
  const id = meta.id ?? `${startedAt.replace(/[:.]/g, "-")}-${randomBytes(2).toString("hex")}`;
181
- const file = path2.join(this.dir, `${id}.jsonl`);
188
+ const shardDir = await this.ensureShardDir(id);
189
+ const file = path7.join(shardDir, `${id}.jsonl`);
182
190
  let handle;
183
191
  try {
184
192
  handle = await fsp.open(file, "a", 384);
185
193
  } catch (err) {
186
194
  throw new Error(
187
195
  `Failed to open session file: ${err instanceof Error ? err.message : String(err)}`,
188
- {
189
- cause: err
190
- }
196
+ { cause: err }
191
197
  );
192
198
  }
193
199
  try {
194
- return new FileSessionWriter(id, handle, startedAt, meta, this.events, { dir: this.dir, filePath: file });
200
+ return new FileSessionWriter(id, handle, startedAt, meta, this.events, { dir: shardDir, filePath: file });
195
201
  } catch (err) {
196
202
  await handle.close().catch(() => {
197
203
  });
@@ -199,15 +205,7 @@ var DefaultSessionStore = class {
199
205
  }
200
206
  }
201
207
  async resume(id) {
202
- const file = path2.join(this.dir, `${id}.jsonl`);
203
- try {
204
- await fsp.access(file, fsp.constants.R_OK);
205
- } catch {
206
- throw new Error(
207
- `Session "${id}" not found \u2014 the session file does not exist or was deleted.`,
208
- { cause: new Error("ENOENT") }
209
- );
210
- }
208
+ const file = this.sessionPath(id, ".jsonl");
211
209
  const data = await this.load(id);
212
210
  let handle;
213
211
  try {
@@ -239,7 +237,7 @@ var DefaultSessionStore = class {
239
237
  }
240
238
  }
241
239
  async load(id) {
242
- const file = path2.join(this.dir, `${id}.jsonl`);
240
+ const file = this.sessionPath(id, ".jsonl");
243
241
  const raw = await fsp.readFile(file, "utf8");
244
242
  const lines = raw.split("\n").filter((l) => l.trim());
245
243
  const events = [];
@@ -259,8 +257,7 @@ var DefaultSessionStore = class {
259
257
  async list(limit = 20) {
260
258
  try {
261
259
  await ensureDir(this.dir);
262
- const files = await fsp.readdir(this.dir);
263
- const ids = files.filter((f) => f.endsWith(".jsonl")).map((f) => f.replace(/\.jsonl$/, ""));
260
+ const ids = await this.collectSessionIds(this.dir);
264
261
  const sessions = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
265
262
  const out = sessions.filter((s) => s !== null);
266
263
  out.sort((a, b) => {
@@ -273,13 +270,27 @@ var DefaultSessionStore = class {
273
270
  return [];
274
271
  }
275
272
  }
273
+ /** Recursively collect all session IDs from shard subdirectories. */
274
+ async collectSessionIds(dir) {
275
+ const ids = [];
276
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
277
+ for (const entry of entries) {
278
+ const full = path7.join(dir, entry.name);
279
+ if (entry.isDirectory()) {
280
+ ids.push(...await this.collectSessionIds(full));
281
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
282
+ ids.push(entry.name.replace(/\.jsonl$/, ""));
283
+ }
284
+ }
285
+ return ids;
286
+ }
276
287
  async summaryFor(id) {
277
- const manifest = path2.join(this.dir, `${id}.summary.json`);
288
+ const manifest = this.sessionPath(id, ".summary.json");
278
289
  try {
279
290
  const raw = await fsp.readFile(manifest, "utf8");
280
291
  return JSON.parse(raw);
281
292
  } catch {
282
- const full = path2.join(this.dir, `${id}.jsonl`);
293
+ const full = this.sessionPath(id, ".jsonl");
283
294
  const stat3 = await fsp.stat(full);
284
295
  const summary = await this.summarize(id, stat3.mtime.toISOString());
285
296
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
@@ -292,12 +303,13 @@ var DefaultSessionStore = class {
292
303
  }
293
304
  }
294
305
  async delete(id) {
295
- await fsp.unlink(path2.join(this.dir, `${id}.jsonl`));
296
- await fsp.unlink(path2.join(this.dir, `${id}.summary.json`)).catch(() => void 0);
306
+ await fsp.unlink(this.sessionPath(id, ".jsonl"));
307
+ await fsp.unlink(this.sessionPath(id, ".summary.json")).catch(() => void 0);
297
308
  }
298
309
  async clearHistory(id) {
299
- const file = path2.join(this.dir, `${id}.jsonl`);
300
- const meta = path2.join(this.dir, `${id}.summary.json`);
310
+ await this.ensureShardDir(id);
311
+ const file = this.sessionPath(id, ".jsonl");
312
+ const meta = this.sessionPath(id, ".summary.json");
301
313
  const record = `${JSON.stringify({
302
314
  type: "session_start",
303
315
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -341,7 +353,8 @@ var DefaultSessionStore = class {
341
353
  startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
342
354
  endedAt: end?.ts,
343
355
  model: start?.model,
344
- provider: start?.provider
356
+ provider: start?.provider,
357
+ pendingToolUses: end?.pendingToolUses
345
358
  };
346
359
  }
347
360
  replay(events, sessionId = "unknown") {
@@ -418,7 +431,7 @@ var FileSessionWriter = class {
418
431
  this.meta = meta;
419
432
  this.events = events;
420
433
  this.resumed = opts.resumed ?? false;
421
- this.manifestFile = opts.dir ? path2.join(opts.dir, `${id}.summary.json`) : "";
434
+ this.manifestFile = opts.dir ? path7.join(opts.dir, `${id}.summary.json`) : "";
422
435
  this.filePath = opts.filePath ?? "";
423
436
  this.summary = {
424
437
  id,
@@ -441,9 +454,6 @@ var FileSessionWriter = class {
441
454
  tokenIn = 0;
442
455
  tokenOut = 0;
443
456
  filePath;
444
- /** Public accessor for the JSONL path — required by SessionWriter so
445
- * observability surfaces (`/fleet log`, FleetPanel) can locate the
446
- * transcript without recomputing the path from session metadata. */
447
457
  get transcriptPath() {
448
458
  return this.filePath || void 0;
449
459
  }
@@ -451,13 +461,16 @@ var FileSessionWriter = class {
451
461
  resumed;
452
462
  appendFailCount = 0;
453
463
  lastAppendWarnAt = 0;
454
- // Rewind support: track pending file changes and prompt index
455
464
  promptIndex = 0;
456
465
  pendingFileSnapshots = [];
457
- // Register a pending file change. Call recordFileChange() from tools.
466
+ /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
467
+ openToolUses = /* @__PURE__ */ new Set();
458
468
  recordFileChange(input) {
459
469
  this.pendingFileSnapshots.push(input);
460
470
  }
471
+ get pendingToolUses() {
472
+ return Array.from(this.openToolUses);
473
+ }
461
474
  async writeSessionStartLazy() {
462
475
  const record = `${JSON.stringify({
463
476
  type: this.resumed ? "session_resumed" : "session_start",
@@ -500,13 +513,12 @@ var FileSessionWriter = class {
500
513
  }
501
514
  }
502
515
  }
503
- /**
504
- * Watch events as they're appended and keep the summary state hot, so
505
- * `close()` can flush a `<id>.summary.json` manifest without re-reading
506
- * the JSONL. `list()` reads only manifests, turning a per-session full
507
- * parse into a single stat+read.
508
- */
509
516
  observeForSummary(event) {
517
+ if (event.type === "tool_use") {
518
+ this.openToolUses.add(event.id);
519
+ } else if (event.type === "tool_result") {
520
+ this.openToolUses.delete(event.id);
521
+ }
510
522
  if (event.type === "user_input" && this.summary.title === "(empty session)") {
511
523
  this.summary = { ...this.summary, title: userInputTitle(event.content) };
512
524
  } else if (event.type === "llm_response") {
@@ -600,7 +612,16 @@ var FileSessionWriter = class {
600
612
  }
601
613
  }
602
614
  const truncated = kept.join("\n");
603
- await fsp.writeFile(this.filePath, truncated + "\n", "utf8");
615
+ const tmpPath = `${this.filePath}.rewind.tmp`;
616
+ await fsp.writeFile(tmpPath, truncated + "\n", "utf8");
617
+ try {
618
+ await this.handle.close();
619
+ await fsp.rename(tmpPath, this.filePath);
620
+ this.handle = await fsp.open(this.filePath, "a", 384);
621
+ } catch (err) {
622
+ await fsp.unlink(tmpPath).catch(() => void 0);
623
+ throw err;
624
+ }
604
625
  await this.append({
605
626
  type: "rewound",
606
627
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -634,7 +655,7 @@ function userInputTitle(content) {
634
655
  var QueueStore = class {
635
656
  file;
636
657
  constructor(opts) {
637
- this.file = path2.join(opts.dir, "queue.json");
658
+ this.file = path7.join(opts.dir, "queue.json");
638
659
  }
639
660
  async write(items) {
640
661
  if (items.length === 0) {
@@ -701,7 +722,7 @@ var DefaultAttachmentStore = class {
701
722
  let data = input.data;
702
723
  if (this.spoolDir && bytes >= this.spoolThreshold) {
703
724
  await fsp.mkdir(this.spoolDir, { recursive: true });
704
- spooledPath = path2.join(this.spoolDir, `${id}.bin`);
725
+ spooledPath = path7.join(this.spoolDir, `${id}.bin`);
705
726
  await atomicWrite(spooledPath, input.data, {
706
727
  encoding: input.kind === "image" ? "base64" : "utf8"
707
728
  });
@@ -871,7 +892,7 @@ ${body.trim()}`);
871
892
  async remember(text, scope = "project-memory") {
872
893
  return this.runSerialized(scope, async () => {
873
894
  const file = this.files[scope];
874
- await ensureDir(path2.dirname(file));
895
+ await ensureDir(path7.dirname(file));
875
896
  let existing = "";
876
897
  try {
877
898
  existing = await fsp.readFile(file, "utf8");
@@ -1364,7 +1385,10 @@ var DefaultConfigLoader = class {
1364
1385
  }
1365
1386
  if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
1366
1387
  const known = listContextWindowModes().map((m) => m.id).join(", ");
1367
- throw new Error(`Config: context.mode must be one of: ${known}`);
1388
+ console.warn(
1389
+ `[config] Ignoring unknown context.mode "${c.mode}" (expected one of: ${known}); falling back to "${DEFAULT_CONTEXT_WINDOW_MODE_ID}".`
1390
+ );
1391
+ c.mode = DEFAULT_CONTEXT_WINDOW_MODE_ID;
1368
1392
  }
1369
1393
  }
1370
1394
  validateIdentity(cfg) {
@@ -1440,7 +1464,7 @@ var RecoveryLock = class {
1440
1464
  sessionStore;
1441
1465
  probe;
1442
1466
  constructor(opts) {
1443
- this.file = path2.join(opts.dir, LOCK_FILE);
1467
+ this.file = path7.join(opts.dir, LOCK_FILE);
1444
1468
  this.pid = opts.pid ?? process.pid;
1445
1469
  this.hostname = opts.hostname ?? os.hostname();
1446
1470
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -1488,12 +1512,17 @@ var RecoveryLock = class {
1488
1512
  };
1489
1513
  }
1490
1514
  /**
1491
- * Claim the lock for the given session. Overwrites any existing lock
1492
- * the caller should have already handled abandonment (via
1493
- * `checkAbandoned`) before calling this.
1515
+ * Claim the lock for the given session. Uses exclusive-create (`O_EXCL`)
1516
+ * to detect whether another process acquired the lock between our
1517
+ * `checkAbandoned()` call and now. If the file already exists, it means
1518
+ * another process won the race and we throw instead of silently
1519
+ * overwriting their recovery record.
1520
+ *
1521
+ * The caller MUST have already called `checkAbandoned()` and handled its
1522
+ * null return before calling this.
1494
1523
  */
1495
1524
  async write(sessionId) {
1496
- await ensureDir(path2.dirname(this.file));
1525
+ await ensureDir(path7.dirname(this.file));
1497
1526
  const lock = {
1498
1527
  v: 1,
1499
1528
  sessionId,
@@ -1501,7 +1530,15 @@ var RecoveryLock = class {
1501
1530
  hostname: this.hostname,
1502
1531
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1503
1532
  };
1504
- await atomicWrite(this.file, JSON.stringify(lock), { mode: 384 });
1533
+ try {
1534
+ await fsp.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
1535
+ } catch (err) {
1536
+ const code = err.code;
1537
+ if (code === "EEXIST") {
1538
+ throw new Error(`Recovery lock already held by another process`);
1539
+ }
1540
+ throw err;
1541
+ }
1505
1542
  }
1506
1543
  /**
1507
1544
  * Release the lock. Idempotent — silently succeeds if the file is
@@ -1945,12 +1982,14 @@ var SessionAnalyzer = class {
1945
1982
  }
1946
1983
  };
1947
1984
  var DefaultSessionRewinder = class {
1948
- constructor(sessionsDir) {
1985
+ constructor(sessionsDir, projectRoot) {
1949
1986
  this.sessionsDir = sessionsDir;
1987
+ this.projectRoot = projectRoot;
1950
1988
  }
1951
1989
  sessionsDir;
1990
+ projectRoot;
1952
1991
  async listCheckpoints(sessionId) {
1953
- const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
1992
+ const file = path7.join(this.sessionsDir, `${sessionId}.jsonl`);
1954
1993
  const raw = await fsp.readFile(file, "utf8");
1955
1994
  const events = parseEvents(raw);
1956
1995
  const fileCountMap = /* @__PURE__ */ new Map();
@@ -1975,7 +2014,7 @@ var DefaultSessionRewinder = class {
1975
2014
  return checkpoints;
1976
2015
  }
1977
2016
  async rewindToCheckpoint(sessionId, checkpointIndex) {
1978
- const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
2017
+ const file = path7.join(this.sessionsDir, `${sessionId}.jsonl`);
1979
2018
  const raw = await fsp.readFile(file, "utf8");
1980
2019
  const events = parseEvents(raw);
1981
2020
  let targetIdx = -1;
@@ -2005,12 +2044,12 @@ var DefaultSessionRewinder = class {
2005
2044
  }
2006
2045
  }
2007
2046
  }
2008
- const result = await revertSnapshots(snapshotsToRevert);
2047
+ const result = await revertSnapshots(snapshotsToRevert, this.projectRoot);
2009
2048
  const removedEvents = events.length - targetIdx - 1;
2010
2049
  return { ...result, toPromptIndex: checkpointIndex, removedEvents };
2011
2050
  }
2012
2051
  async rewindLastN(sessionId, n) {
2013
- const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
2052
+ const file = path7.join(this.sessionsDir, `${sessionId}.jsonl`);
2014
2053
  const raw = await fsp.readFile(file, "utf8");
2015
2054
  const events = parseEvents(raw);
2016
2055
  const checkpoints = [];
@@ -2035,11 +2074,11 @@ var DefaultSessionRewinder = class {
2035
2074
  snapshotsToRevert.push({ promptIndex: event.promptIndex, files: event.files });
2036
2075
  }
2037
2076
  }
2038
- const result = await revertSnapshots(snapshotsToRevert.reverse());
2077
+ const result = await revertSnapshots(snapshotsToRevert.reverse(), this.projectRoot);
2039
2078
  return { ...result, toPromptIndex: targetIndex, removedEvents: snapshotsToRevert.length };
2040
2079
  }
2041
2080
  async rewindToStart(sessionId) {
2042
- const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
2081
+ const file = path7.join(this.sessionsDir, `${sessionId}.jsonl`);
2043
2082
  const raw = await fsp.readFile(file, "utf8");
2044
2083
  const events = parseEvents(raw);
2045
2084
  const allSnapshots = [];
@@ -2051,7 +2090,7 @@ var DefaultSessionRewinder = class {
2051
2090
  if (allSnapshots.length === 0) {
2052
2091
  return { revertedFiles: [], errors: [], toPromptIndex: 0, removedEvents: 0 };
2053
2092
  }
2054
- const result = await revertSnapshots(allSnapshots.reverse());
2093
+ const result = await revertSnapshots(allSnapshots.reverse(), this.projectRoot);
2055
2094
  return { ...result, toPromptIndex: 0, removedEvents: allSnapshots.length };
2056
2095
  }
2057
2096
  };
@@ -2069,12 +2108,19 @@ function parseEvents(raw) {
2069
2108
  }
2070
2109
  return events;
2071
2110
  }
2072
- async function revertSnapshots(snapshots) {
2111
+ async function revertSnapshots(snapshots, projectRoot) {
2073
2112
  const revertedFiles = [];
2074
2113
  const errors = [];
2075
2114
  for (const snapshot of snapshots) {
2076
2115
  for (const file of snapshot.files) {
2077
2116
  try {
2117
+ const absPath = path7.resolve(file.path);
2118
+ const root = path7.resolve(projectRoot);
2119
+ const rel = path7.relative(root, absPath);
2120
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
2121
+ errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
2122
+ continue;
2123
+ }
2078
2124
  if (file.action === "deleted") {
2079
2125
  if (file.before !== null) {
2080
2126
  await atomicWrite(file.path, file.before, { mode: 420 });
@@ -2630,8 +2676,8 @@ var FsError = class extends WrongStackError {
2630
2676
  // src/storage/goal-store.ts
2631
2677
  var MAX_JOURNAL_ENTRIES = 500;
2632
2678
  function goalFilePath(projectRoot) {
2633
- const hash = createHash("sha256").update(path2.resolve(projectRoot)).digest("hex").slice(0, 12);
2634
- return path2.join(os.homedir(), ".wrongstack", "projects", hash, "goal.json");
2679
+ const hash = createHash("sha256").update(path7.resolve(projectRoot)).digest("hex").slice(0, 12);
2680
+ return path7.join(os.homedir(), ".wrongstack", "projects", hash, "goal.json");
2635
2681
  }
2636
2682
  async function loadGoal(filePath) {
2637
2683
  let raw;