@wrongstack/core 0.273.0 → 0.273.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 (60) hide show
  1. package/dist/{agent-bridge-BZ2enORi.d.ts → agent-bridge-DpKIxHhE.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-ehb4xGvd.d.ts → agent-subagent-runner-Dx7fZ1bE.d.ts} +7 -7
  3. package/dist/{brain-BxN2k2HP.d.ts → brain-BDcQaku-.d.ts} +11 -5
  4. package/dist/{compactor-72ug-ZRB.d.ts → compactor-BuSdj3fq.d.ts} +1 -1
  5. package/dist/{config-C8IYxlO8.d.ts → config-CR2yoG8c.d.ts} +54 -4
  6. package/dist/{context-Dw55zZ_Q.d.ts → context-DulAr8Zo.d.ts} +24 -0
  7. package/dist/coordination/index.d.ts +23 -16
  8. package/dist/coordination/index.js +113 -13
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +26 -26
  11. package/dist/defaults/index.js +405 -8
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +15 -15
  14. package/dist/execution/index.js +75 -1
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +1 -1
  17. package/dist/extension/index.d.ts +6 -6
  18. package/dist/{global-mailbox-C9dsc9Y_.d.ts → global-mailbox-CwcubDkA.d.ts} +1 -1
  19. package/dist/{goal-preamble-NhflDjYb.d.ts → goal-preamble-Bu0a2uCG.d.ts} +9 -9
  20. package/dist/{goal-store-Cx363x7Z.d.ts → goal-store-CTmFuZ8J.d.ts} +1 -1
  21. package/dist/hq/index.d.ts +5 -5
  22. package/dist/{index-B7fHDt0B.d.ts → index-CTq5wU3m.d.ts} +5 -5
  23. package/dist/{index-BbVprU-9.d.ts → index-CxP-HBhX.d.ts} +2 -2
  24. package/dist/index.d.ts +63 -42
  25. package/dist/index.js +598 -56
  26. package/dist/index.js.map +1 -1
  27. package/dist/infrastructure/index.d.ts +6 -6
  28. package/dist/kernel/index.d.ts +11 -11
  29. package/dist/kernel/index.js.map +1 -1
  30. package/dist/{mcp-servers-B6fSRNC1.d.ts → mcp-servers-BQaOE71z.d.ts} +3 -3
  31. package/dist/models/index.d.ts +5 -5
  32. package/dist/{models-registry-4C6Wr91w.d.ts → models-registry-BEcny4kP.d.ts} +1 -1
  33. package/dist/{multi-agent-coordinator-q1skFeNP.d.ts → multi-agent-coordinator-Bx8EFkv2.d.ts} +1 -1
  34. package/dist/{null-fleet-bus-C9rrgQwc.d.ts → null-fleet-bus-BC5ZXCQw.d.ts} +6 -6
  35. package/dist/observability/index.d.ts +2 -2
  36. package/dist/{parallel-eternal-engine-CtXly2Sf.d.ts → parallel-eternal-engine-C345TI3n.d.ts} +9 -9
  37. package/dist/{path-resolver-Bim6G5Jz.d.ts → path-resolver-C-W_wzkF.d.ts} +3 -3
  38. package/dist/{permission-CC7XFYWG.d.ts → permission-CsBGZkxp.d.ts} +1 -1
  39. package/dist/{permission-policy-cYR4RJmw.d.ts → permission-policy-g3Sg0GdZ.d.ts} +2 -2
  40. package/dist/{pipeline-CNVKuQDQ.d.ts → pipeline-xnw_24Z8.d.ts} +2 -2
  41. package/dist/{plan-templates-C4wXMmiM.d.ts → plan-templates-DGaiYEcS.d.ts} +31 -5
  42. package/dist/{provider-model-resolve-DFd3IPpw.d.ts → provider-model-resolve-Cz6OlIOp.d.ts} +3 -3
  43. package/dist/{provider-runner-BpM0mdBE.d.ts → provider-runner-7J0HqF6B.d.ts} +3 -3
  44. package/dist/{retry-policy-BV7nzeAd.d.ts → retry-policy-kqXJOVkX.d.ts} +1 -1
  45. package/dist/sdd/index.d.ts +9 -9
  46. package/dist/{secret-vault-eMBKfheR.d.ts → secret-vault-CMQUr-eB.d.ts} +1 -1
  47. package/dist/security/index.d.ts +5 -5
  48. package/dist/{selector-C4ORTOid.d.ts → selector-B4r34PWR.d.ts} +1 -1
  49. package/dist/{session-event-bridge-CeNpUL9w.d.ts → session-event-bridge-BD3LoyLC.d.ts} +1 -1
  50. package/dist/{session-reader-BepLSnGL.d.ts → session-reader-DjrKGD9c.d.ts} +1 -1
  51. package/dist/storage/index.d.ts +11 -11
  52. package/dist/storage/index.js +377 -11
  53. package/dist/storage/index.js.map +1 -1
  54. package/dist/tools/index.d.ts +2 -2
  55. package/dist/types/index.d.ts +19 -19
  56. package/dist/types/index.js +28 -0
  57. package/dist/types/index.js.map +1 -1
  58. package/dist/utils/index.d.ts +2 -2
  59. package/dist/{worktree-manager-BDuXTaWL.d.ts → worktree-manager-DHdrWQ_7.d.ts} +1 -1
  60. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8260,6 +8260,34 @@ var DefaultSessionReader = class {
8260
8260
  ids = filtered.map((s) => s.id);
8261
8261
  }
8262
8262
  const hits = [];
8263
+ const streaming = this.store.searchEvents?.bind(this.store);
8264
+ if (streaming) {
8265
+ for (const id of ids) {
8266
+ const matched = await streaming(
8267
+ id,
8268
+ (ev) => {
8269
+ if (allowedTypes && !allowedTypes.has(ev.type)) return false;
8270
+ const text = eventText(ev);
8271
+ if (text === null) return false;
8272
+ return matcher(text) !== null;
8273
+ },
8274
+ { limit: limit - hits.length }
8275
+ );
8276
+ for (const m of matched) {
8277
+ const text = expectDefined(eventText(m.event));
8278
+ const hit = expectDefined(matcher(text));
8279
+ hits.push({
8280
+ sessionId: id,
8281
+ eventIndex: m.eventIndex,
8282
+ ts: m.ts,
8283
+ type: m.event.type,
8284
+ snippet: snippetOf(text, hit.start, hit.end)
8285
+ });
8286
+ if (hits.length >= limit) return hits;
8287
+ }
8288
+ }
8289
+ return hits;
8290
+ }
8263
8291
  for (const id of ids) {
8264
8292
  let data;
8265
8293
  try {
@@ -8798,6 +8826,91 @@ var DefaultSessionStore = class _DefaultSessionStore {
8798
8826
  }
8799
8827
  }
8800
8828
  }
8829
+ /**
8830
+ * Streaming search over a session's JSONL. Walks the file once, parses
8831
+ * each event lazily, and yields only the events that match `predicate`.
8832
+ * Stops as soon as `opts.limit` matches are collected.
8833
+ *
8834
+ * Why this exists: `load()` parses the entire file into memory and
8835
+ * rebuilds `messages`/`toolCallEnds` for every caller. `search()` only
8836
+ * needs to know which events contain matching text — a per-line
8837
+ * predicate is enough. The full parse work (and the `_loadCache` poll)
8838
+ * is wasted in that case.
8839
+ *
8840
+ * Memory: O(hits) regardless of file size. Disk: one linear scan,
8841
+ * terminated at `limit` if the caller asked for one.
8842
+ *
8843
+ * Errors: missing file yields []. Corrupt lines are skipped (same
8844
+ * policy as `load()`). Aborting via `signal` rejects with `AbortError`.
8845
+ */
8846
+ async searchEvents(id, predicate, opts) {
8847
+ const file = this.sessionPath(id, ".jsonl");
8848
+ const limit = opts?.limit;
8849
+ const signal = opts?.signal;
8850
+ const out = [];
8851
+ let stat16;
8852
+ try {
8853
+ stat16 = await fsp3.stat(file);
8854
+ } catch (err) {
8855
+ if (err.code === "ENOENT") return [];
8856
+ throw err;
8857
+ }
8858
+ if (stat16.size === 0) return [];
8859
+ let fh;
8860
+ try {
8861
+ fh = await fsp3.open(file, "r");
8862
+ const CHUNK = 64 * 1024;
8863
+ const buf = Buffer.alloc(CHUNK);
8864
+ let leftover = "";
8865
+ let eventIndex = 0;
8866
+ for (let position = 0; ; position += buf.byteLength) {
8867
+ if (signal?.aborted) {
8868
+ const reason = signal.reason ?? new DOMException("Aborted", "AbortError");
8869
+ throw reason;
8870
+ }
8871
+ const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
8872
+ if (bytesRead === 0) break;
8873
+ const text = leftover + buf.subarray(0, bytesRead).toString("utf8");
8874
+ const parts = text.split("\n");
8875
+ leftover = parts.pop() ?? "";
8876
+ for (const line of parts) {
8877
+ if (!line) continue;
8878
+ let ev;
8879
+ try {
8880
+ const parsed = JSON.parse(line);
8881
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string" || typeof parsed.ts !== "string") {
8882
+ continue;
8883
+ }
8884
+ ev = parsed;
8885
+ } catch {
8886
+ continue;
8887
+ }
8888
+ if (predicate(ev, eventIndex, ev.ts)) {
8889
+ out.push({ event: ev, eventIndex, ts: ev.ts });
8890
+ if (limit !== void 0 && out.length >= limit) {
8891
+ return out;
8892
+ }
8893
+ }
8894
+ eventIndex++;
8895
+ }
8896
+ }
8897
+ if (leftover.trim()) {
8898
+ try {
8899
+ const parsed = JSON.parse(leftover);
8900
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
8901
+ const ev = parsed;
8902
+ if (predicate(ev, eventIndex, ev.ts)) {
8903
+ out.push({ event: ev, eventIndex, ts: ev.ts });
8904
+ }
8905
+ }
8906
+ } catch {
8907
+ }
8908
+ }
8909
+ return out;
8910
+ } finally {
8911
+ if (fh) await fh.close().catch(() => void 0);
8912
+ }
8913
+ }
8801
8914
  async list(limit = 20) {
8802
8915
  try {
8803
8916
  await ensureDir(this.dir);
@@ -11139,7 +11252,8 @@ var BEHAVIOR_DEFAULTS = {
11139
11252
  hardThreshold: 0.9,
11140
11253
  autoCompact: true,
11141
11254
  preserveK: DEFAULT_CONTEXT_CONFIG.preserveK,
11142
- eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold
11255
+ eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold,
11256
+ strategy: "hybrid"
11143
11257
  },
11144
11258
  tools: {
11145
11259
  defaultExecutionStrategy: DEFAULT_TOOLS_CONFIG.defaultExecutionStrategy,
@@ -11162,6 +11276,13 @@ var BEHAVIOR_DEFAULTS = {
11162
11276
  allowOutsideProjectRoot: true
11163
11277
  },
11164
11278
  mcpServers: {},
11279
+ fallbackAuto: true,
11280
+ maxConcurrent: 4,
11281
+ yolo: false,
11282
+ nextPrediction: false,
11283
+ hints: true,
11284
+ debugStream: false,
11285
+ configScope: "global",
11165
11286
  indexing: {
11166
11287
  onSessionStart: true,
11167
11288
  onEdit: true,
@@ -11169,8 +11290,55 @@ var BEHAVIOR_DEFAULTS = {
11169
11290
  debounceMs: 400
11170
11291
  },
11171
11292
  session: { ...DEFAULT_SESSION_LOGGING_CONFIG },
11172
- autonomy: { autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs }
11293
+ autonomy: {
11294
+ defaultMode: "off",
11295
+ autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs,
11296
+ autoProceedMaxIterations: 50,
11297
+ autonomyNextPrompt: "auto {{suggestion}}",
11298
+ terminalTitleAnimation: true,
11299
+ yolo: false,
11300
+ streamFleet: true,
11301
+ chime: false,
11302
+ confirmExit: true,
11303
+ mouseMode: false,
11304
+ enhance: true,
11305
+ enhanceDelayMs: 6e4,
11306
+ enhanceLanguage: "original",
11307
+ statuslineMode: "detailed",
11308
+ thinkingWord: DEFAULT_TUI_THINKING_WORD
11309
+ },
11310
+ circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
11311
+ modelRuntime: {
11312
+ reasoning: { mode: "auto", effort: "high", preserve: false },
11313
+ cache: {}
11314
+ }
11173
11315
  };
11316
+ function isPlainRecord(value) {
11317
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11318
+ }
11319
+ function cloneJsonValue(value) {
11320
+ return structuredClone(value);
11321
+ }
11322
+ function fillMissingDefaults(target, defaults) {
11323
+ const value = cloneJsonValue(target);
11324
+ const changed = fillMissingDefaultsInPlace(value, defaults);
11325
+ return { value, changed };
11326
+ }
11327
+ function fillMissingDefaultsInPlace(target, defaults) {
11328
+ let changed = false;
11329
+ for (const [key, defaultValue] of Object.entries(defaults)) {
11330
+ if (!Object.prototype.hasOwnProperty.call(target, key)) {
11331
+ target[key] = cloneJsonValue(defaultValue);
11332
+ changed = true;
11333
+ continue;
11334
+ }
11335
+ const current = target[key];
11336
+ if (isPlainRecord(current) && isPlainRecord(defaultValue)) {
11337
+ changed = fillMissingDefaultsInPlace(current, defaultValue) || changed;
11338
+ }
11339
+ }
11340
+ return changed;
11341
+ }
11174
11342
  function envBool(v) {
11175
11343
  return !/^(0|false|no|off)$/i.test(v.trim());
11176
11344
  }
@@ -11222,27 +11390,139 @@ var defaultIndexing = {
11222
11390
  watchExternal: true,
11223
11391
  debounceMs: 400
11224
11392
  };
11225
- var IN_PROJECT_FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
11393
+ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
11394
+ "version",
11395
+ "model",
11396
+ "cwd",
11397
+ "context",
11398
+ "tools",
11399
+ "features",
11400
+ "autonomy",
11401
+ "indexing",
11402
+ "session",
11403
+ "log",
11404
+ "launch",
11405
+ "nextPrediction",
11406
+ "hints",
11407
+ "debugStream",
11408
+ "configScope",
11409
+ "maxConcurrent",
11410
+ "fallbackModels",
11411
+ "fallbackAuto",
11412
+ "models",
11413
+ "modelMatrix",
11414
+ "circuitBreaker",
11415
+ "adaptiveConcurrency",
11416
+ "modelRuntime"
11417
+ ]);
11418
+ var KNOWN_DENIED_IN_PROJECT = [
11419
+ { key: "provider", reason: "Provider id override; can intercept prompts/responses." },
11420
+ { key: "apiKey", reason: "Overrides user API key; exfiltrates prompts." },
11421
+ { key: "baseUrl", reason: "Redirects provider endpoint; leaks real API key." },
11422
+ { key: "providers", reason: "Per-provider apiKey/baseUrl/oauthConfig; same redirect/exfil." },
11423
+ { key: "mcpServers", reason: "Arbitrary command/args/env spawned at boot (RCE)." },
11424
+ { key: "hooks", reason: "Shell command arrays on lifecycle events (RCE)." },
11425
+ { key: "plugins", reason: "Dynamic npm package load at boot (RCE)." },
11426
+ { key: "sync", reason: "Carries githubToken credential and target repo." },
11427
+ { key: "yolo", reason: "Disables all permission confirmation prompts." },
11428
+ { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
11429
+ { key: "hq", reason: "Carries HQ client token credential and endpoint URL." }
11430
+ ];
11431
+ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
11432
+ "version",
11226
11433
  "provider",
11434
+ "model",
11227
11435
  "apiKey",
11228
11436
  "baseUrl",
11437
+ "maxConcurrent",
11229
11438
  "providers",
11439
+ "models",
11440
+ "modelMatrix",
11441
+ "context",
11442
+ "tools",
11230
11443
  "mcpServers",
11444
+ "fallbackModels",
11445
+ "fallbackAuto",
11231
11446
  "hooks",
11232
11447
  "plugins",
11233
- "sync",
11448
+ "log",
11449
+ "features",
11234
11450
  "yolo",
11451
+ "nextPrediction",
11452
+ "cwd",
11453
+ "autonomy",
11454
+ "hints",
11455
+ "debugStream",
11456
+ "configScope",
11457
+ "indexing",
11458
+ "circuitBreaker",
11459
+ "adaptiveConcurrency",
11460
+ "launch",
11461
+ "session",
11462
+ "modelRuntime",
11463
+ "hq",
11464
+ "sync",
11235
11465
  "extensions"
11236
11466
  ]);
11467
+ function assertInProjectAllowListComplete() {
11468
+ const missingFromBoth = [];
11469
+ for (const key of KNOWN_CONFIG_TOP_LEVEL_KEYS) {
11470
+ if (IN_PROJECT_ALLOWED_KEYS.has(key)) continue;
11471
+ const denied = KNOWN_DENIED_IN_PROJECT.find((d) => d.key === key);
11472
+ if (!denied) missingFromBoth.push(key);
11473
+ }
11474
+ const staleDenials = KNOWN_DENIED_IN_PROJECT.filter((d) => !KNOWN_CONFIG_TOP_LEVEL_KEYS.has(d.key)).map((d) => d.key);
11475
+ const duplicate = KNOWN_DENIED_IN_PROJECT.filter((d) => IN_PROJECT_ALLOWED_KEYS.has(d.key)).map((d) => d.key);
11476
+ const problems = [];
11477
+ if (missingFromBoth.length > 0) {
11478
+ problems.push(
11479
+ `new Config field(s) not classified as allowed or denied for in-project config: ` + missingFromBoth.join(", ") + ". Add each to IN_PROJECT_ALLOWED_KEYS (if safe) or KNOWN_DENIED_IN_PROJECT (with a reason)."
11480
+ );
11481
+ }
11482
+ if (staleDenials.length > 0) {
11483
+ problems.push(
11484
+ `KNOWN_DENIED_IN_PROJECT references keys that no longer exist on Config: ` + staleDenials.join(", ") + ". Remove them or restore the field on Config."
11485
+ );
11486
+ }
11487
+ if (duplicate.length > 0) {
11488
+ problems.push(
11489
+ `field(s) appear in BOTH IN_PROJECT_ALLOWED_KEYS and KNOWN_DENIED_IN_PROJECT: ` + duplicate.join(", ") + ". The allow-list wins at runtime; remove from one of the two."
11490
+ );
11491
+ }
11492
+ if (problems.length > 0) {
11493
+ throw new Error(
11494
+ `stripUnsafeInProjectFields drift check failed:
11495
+ - ${problems.join("\n - ")}`
11496
+ );
11497
+ }
11498
+ }
11499
+ var driftChecked = false;
11237
11500
  function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
11501
+ if (!driftChecked) {
11502
+ assertInProjectAllowListComplete();
11503
+ driftChecked = true;
11504
+ }
11238
11505
  const stripped = [];
11239
11506
  const out = {};
11240
11507
  for (const [k, v] of Object.entries(inProject)) {
11241
- if (IN_PROJECT_FORBIDDEN_KEYS.has(k)) {
11242
- stripped.push(k);
11508
+ if (IN_PROJECT_ALLOWED_KEYS.has(k)) {
11509
+ out[k] = v;
11243
11510
  continue;
11244
11511
  }
11245
- out[k] = v;
11512
+ stripped.push(k);
11513
+ }
11514
+ const outTools = out["tools"];
11515
+ if (outTools && typeof outTools === "object") {
11516
+ const execCfg = outTools["exec"];
11517
+ if (execCfg && typeof execCfg === "object" && "allow" in execCfg) {
11518
+ const clonedExec = { ...execCfg };
11519
+ delete clonedExec["allow"];
11520
+ out["tools"] = {
11521
+ ...outTools,
11522
+ exec: clonedExec
11523
+ };
11524
+ stripped.push("tools.exec.allow");
11525
+ }
11246
11526
  }
11247
11527
  if (stripped.length > 0) {
11248
11528
  warn(
@@ -11251,7 +11531,7 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
11251
11531
  event: "config.in_project_unsafe_fields_ignored",
11252
11532
  path: sourcePath,
11253
11533
  ignoredKeys: stripped,
11254
- message: `Ignored ${stripped.length} unsafe field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. These can only be set in your personal ~/.wrongstack/config.json, not in a project-committed file.`,
11534
+ message: `Ignored ${stripped.length} field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. Only a small allow-list of benign preferences (model, context, tools limits, features, \u2026) may be set by <project>/.wrongstack/config.json. Everything else must live in your personal ~/.wrongstack/config.json.`,
11255
11535
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11256
11536
  })
11257
11537
  );
@@ -11295,6 +11575,7 @@ var DefaultConfigLoader = class {
11295
11575
  }
11296
11576
  async load(opts = {}) {
11297
11577
  let cfg = { ...BEHAVIOR_DEFAULTS };
11578
+ await this.ensureGlobalDefaults();
11298
11579
  const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
11299
11580
  const [global, local, inProject] = await Promise.all([
11300
11581
  this.readJson(this.paths.globalConfig),
@@ -11359,6 +11640,80 @@ var DefaultConfigLoader = class {
11359
11640
  }
11360
11641
  return Object.freeze(cfg);
11361
11642
  }
11643
+ async ensureGlobalDefaults() {
11644
+ const fp = this.paths.globalConfig;
11645
+ const t0 = Date.now();
11646
+ try {
11647
+ await withFileLock(fp, async () => {
11648
+ let parsed;
11649
+ try {
11650
+ const raw = await fsp3.readFile(fp, "utf8");
11651
+ const result = safeParse(raw);
11652
+ if (!result.ok || !isPlainRecord(result.value)) {
11653
+ return;
11654
+ }
11655
+ parsed = result.value;
11656
+ } catch (err) {
11657
+ if (err.code !== "ENOENT") {
11658
+ this.events?.emit("storage.error", {
11659
+ sessionId: "~config~",
11660
+ store: "config",
11661
+ filePath: fp,
11662
+ operation: "ensure_defaults",
11663
+ outcome: "failure",
11664
+ error: storageErrorString(err),
11665
+ recoverable: false,
11666
+ durationMs: Date.now() - t0,
11667
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
11668
+ });
11669
+ console.warn(JSON.stringify({
11670
+ level: "warn",
11671
+ event: "config.defaults_read_failed",
11672
+ path: fp,
11673
+ message: toErrorMessage(err),
11674
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11675
+ }));
11676
+ return;
11677
+ }
11678
+ parsed = {};
11679
+ }
11680
+ const { value, changed } = fillMissingDefaults(
11681
+ parsed,
11682
+ BEHAVIOR_DEFAULTS
11683
+ );
11684
+ if (!changed) return;
11685
+ await atomicWrite(fp, JSON.stringify(value, null, 2), { mode: 384 });
11686
+ this.events?.emit("storage.write", {
11687
+ sessionId: "~config~",
11688
+ store: "config",
11689
+ filePath: fp,
11690
+ operation: "ensure_defaults",
11691
+ outcome: "success",
11692
+ durationMs: Date.now() - t0,
11693
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
11694
+ });
11695
+ });
11696
+ } catch (err) {
11697
+ this.events?.emit("storage.error", {
11698
+ sessionId: "~config~",
11699
+ store: "config",
11700
+ filePath: fp,
11701
+ operation: "ensure_defaults",
11702
+ outcome: "failure",
11703
+ error: storageErrorString(err),
11704
+ recoverable: false,
11705
+ durationMs: Date.now() - t0,
11706
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
11707
+ });
11708
+ console.warn(JSON.stringify({
11709
+ level: "warn",
11710
+ event: "config.defaults_write_failed",
11711
+ path: fp,
11712
+ message: toErrorMessage(err),
11713
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11714
+ }));
11715
+ }
11716
+ }
11362
11717
  /**
11363
11718
  * Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
11364
11719
  * by the vault (if provided). The file is isolated from the main config
@@ -14395,6 +14750,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14395
14750
  level,
14396
14751
  tokens,
14397
14752
  load,
14753
+ hardThreshold: adaptiveThresholds.hard,
14398
14754
  budget,
14399
14755
  signals: { repeatedReadCount: repetition }
14400
14756
  });
@@ -14442,6 +14798,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14442
14798
  }
14443
14799
  }
14444
14800
  async compact(ctx, aggressive, pressure) {
14801
+ let postCompactionOverflow = null;
14445
14802
  try {
14446
14803
  const report = await this.compactor.compact(ctx, { aggressive });
14447
14804
  this.recordAttempt(pressure.level, pressure.tokens, report);
@@ -14471,6 +14828,38 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14471
14828
  ...report.collapsedDigest ? { digest: truncateDigest(report.collapsedDigest) } : {}
14472
14829
  });
14473
14830
  ctx.clearFileTracking();
14831
+ const afterTokens = report.fullRequestTokensAfter ?? report.after;
14832
+ const afterLoad = this._maxContext > 0 ? afterTokens / this._maxContext : 0;
14833
+ const stillHard = afterLoad >= pressure.hardThreshold;
14834
+ const fatal = stillHard && (this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard");
14835
+ if (stillHard) {
14836
+ const error = new Error(
14837
+ `Auto-compaction left context above the hard threshold after ${pressure.level} compaction`
14838
+ );
14839
+ this.events?.emit("compaction.failed", {
14840
+ err: error,
14841
+ aggressive,
14842
+ level: pressure.level,
14843
+ tokens: afterTokens,
14844
+ maxContext: this._maxContext,
14845
+ budget: computeContextWindowBudget(ctx, afterTokens, this._maxContext),
14846
+ signals: pressure.signals,
14847
+ load: afterLoad,
14848
+ fatal
14849
+ });
14850
+ if (fatal) {
14851
+ postCompactionOverflow = new AgentError({
14852
+ message: `Auto-compaction did not reduce context below hard threshold`,
14853
+ code: ERROR_CODES.AGENT_CONTEXT_OVERFLOW,
14854
+ recoverable: true,
14855
+ context: {
14856
+ level: pressure.level,
14857
+ tokens: afterTokens,
14858
+ maxContext: this._maxContext
14859
+ }
14860
+ });
14861
+ }
14862
+ }
14474
14863
  } catch (err) {
14475
14864
  const error = err instanceof Error ? err : new Error(String(err));
14476
14865
  const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
@@ -14499,6 +14888,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14499
14888
  });
14500
14889
  }
14501
14890
  }
14891
+ if (postCompactionOverflow) throw postCompactionOverflow;
14502
14892
  }
14503
14893
  };
14504
14894
  function computeContextWindowBudget(ctx, inputTokens, maxContext) {
@@ -32905,6 +33295,10 @@ var AGENT_REAP_MS = 3e4;
32905
33295
  var AGENT_SWEEP_INTERVAL_MS = 1e4;
32906
33296
  var PARTIAL_TEXT_CAP = 1200;
32907
33297
  var PARTIAL_FLUSH_THROTTLE_MS = 300;
33298
+ function clampPct(pct) {
33299
+ if (!Number.isFinite(pct)) return 0;
33300
+ return Math.max(0, Math.min(100, pct));
33301
+ }
32908
33302
  var AgentStatusTracker = class {
32909
33303
  events;
32910
33304
  registry;
@@ -33056,7 +33450,7 @@ var AgentStatusTracker = class {
33056
33450
  this.events.onPattern("ctx.pct", (_e, payload) => {
33057
33451
  const p = payload;
33058
33452
  if (typeof p?.load === "number" && Number.isFinite(p.load)) {
33059
- this.leaderCtxPct = Math.round(p.load * 100);
33453
+ this.leaderCtxPct = clampPct(Math.round(p.load * 100));
33060
33454
  this.flush();
33061
33455
  }
33062
33456
  })
@@ -33098,7 +33492,7 @@ var AgentStatusTracker = class {
33098
33492
  const p = payload;
33099
33493
  if (!p?.subagentId) return;
33100
33494
  const entry = touch(p.subagentId);
33101
- if (typeof p.load === "number") entry.ctxPct = Math.round(p.load * 100);
33495
+ if (typeof p.load === "number") entry.ctxPct = clampPct(Math.round(p.load * 100));
33102
33496
  this.flush();
33103
33497
  })
33104
33498
  );
@@ -33259,7 +33653,7 @@ var AgentStatusTracker = class {
33259
33653
  const providerMax = c.provider?.capabilities?.maxContext;
33260
33654
  const maxContext = typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : void 0;
33261
33655
  if (typeof c.lastRequestTokens === "number" && c.lastRequestTokens > 0 && maxContext !== void 0) {
33262
- this.leaderCtxPct = Math.round(c.lastRequestTokens / maxContext * 100);
33656
+ this.leaderCtxPct = clampPct(Math.round(c.lastRequestTokens / maxContext * 100));
33263
33657
  }
33264
33658
  }
33265
33659
  };
@@ -38647,6 +39041,9 @@ var DefaultMailbox = class {
38647
39041
  await withFileLock(this.filePath, async () => {
38648
39042
  await fsp3.appendFile(this.filePath, line, "utf8");
38649
39043
  this._pushToCache(msg);
39044
+ const { mtime, size } = await this._statUnderLockOrAbsent();
39045
+ this._messageCacheMtime = mtime;
39046
+ this._messageCacheSize = size;
38650
39047
  });
38651
39048
  return msg;
38652
39049
  }
@@ -38725,7 +39122,8 @@ var DefaultMailbox = class {
38725
39122
  const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
38726
39123
  await fsp3.writeFile(this.filePath, serialized, "utf8");
38727
39124
  }
38728
- this._setMessageCache(all);
39125
+ const { mtime, size } = await this._statUnderLockOrAbsent();
39126
+ this._setMessageCache(all, mtime, size);
38729
39127
  });
38730
39128
  return updated;
38731
39129
  }
@@ -38782,7 +39180,8 @@ var DefaultMailbox = class {
38782
39180
  async clearAll() {
38783
39181
  await withFileLock(this.filePath, async () => {
38784
39182
  await fsp3.writeFile(this.filePath, "", "utf8");
38785
- this._setMessageCache([]);
39183
+ const { mtime, size } = await this._statUnderLockOrAbsent();
39184
+ this._setMessageCache([], mtime, size);
38786
39185
  });
38787
39186
  }
38788
39187
  async purgeStale(opts) {
@@ -38815,7 +39214,8 @@ var DefaultMailbox = class {
38815
39214
  const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
38816
39215
  await fsp3.writeFile(this.filePath, content, "utf8");
38817
39216
  }
38818
- this._setMessageCache(kept);
39217
+ const { mtime, size } = await this._statUnderLockOrAbsent();
39218
+ this._setMessageCache(kept, mtime, size);
38819
39219
  });
38820
39220
  return {
38821
39221
  completedPurged,
@@ -38894,6 +39294,23 @@ var DefaultMailbox = class {
38894
39294
  }
38895
39295
  return messages;
38896
39296
  }
39297
+ /**
39298
+ * Stat the mailbox file under the assumption that we are holding the
39299
+ * file lock, and that a write to the file has just completed. Returns
39300
+ * the (mtimeMs, size) pair, or (-1, -1) if the file does not exist
39301
+ * (e.g. ackMany/purgeStale on a session that has never sent a message).
39302
+ */
39303
+ async _statUnderLockOrAbsent() {
39304
+ try {
39305
+ const st = await fsp3.stat(this.filePath);
39306
+ return { mtime: st.mtimeMs, size: st.size };
39307
+ } catch (err) {
39308
+ if (err.code === "ENOENT") {
39309
+ return { mtime: -1, size: -1 };
39310
+ }
39311
+ throw err;
39312
+ }
39313
+ }
38897
39314
  async _readAllCached() {
38898
39315
  try {
38899
39316
  const st = await fsp3.stat(this.filePath);
@@ -38933,16 +39350,8 @@ var DefaultMailbox = class {
38933
39350
  }
38934
39351
  this._messageCache = messages;
38935
39352
  this._buildIndexes(messages);
38936
- if (mtime !== void 0 && size !== void 0) {
38937
- this._messageCacheMtime = mtime;
38938
- this._messageCacheSize = size;
38939
- return;
38940
- }
38941
- void fsp3.stat(this.filePath).then((st) => {
38942
- this._messageCacheMtime = st.mtimeMs;
38943
- this._messageCacheSize = st.size;
38944
- }).catch(() => {
38945
- });
39353
+ this._messageCacheMtime = mtime;
39354
+ this._messageCacheSize = size;
38946
39355
  }
38947
39356
  _pushToCache(msg) {
38948
39357
  if (this._messageCache === null) return;
@@ -44009,17 +44418,71 @@ function createAgentLoopHandler(a, handlers) {
44009
44418
  const msgCount = a.ctx.messages.length;
44010
44419
  const maxContext = currentMaxContext();
44011
44420
  if (_lastCompactionMsgCount === msgCount && _lastCompactionWasNoop && _lastCompactionMaxContext === maxContext && maxContext > 0) {
44012
- return;
44421
+ return false;
44013
44422
  }
44423
+ const beforeMessages = a.ctx.messages;
44424
+ const beforeMsgCount = a.ctx.messages.length;
44014
44425
  await a.pipelines.contextWindow.run(a.ctx);
44015
- _lastCompactionMsgCount = msgCount;
44426
+ _lastCompactionMsgCount = a.ctx.messages.length;
44016
44427
  _lastCompactionMaxContext = maxContext;
44017
- const stashed = a.ctx.lastRequestTokens;
44018
- const tokens = typeof stashed === "number" && stashed > 0 ? stashed : 0;
44428
+ const changed = a.ctx.messages !== beforeMessages || a.ctx.messages.length !== beforeMsgCount;
44429
+ const tokens = refreshContextRequestTokenStash({ force: changed });
44019
44430
  const load = maxContext > 0 ? tokens / maxContext : 0;
44020
44431
  _lastCompactionWasNoop = tokens > 0 && load < 0.5;
44432
+ if (changed) {
44433
+ _lastEmittedMsgCount = -1;
44434
+ _lastEmittedToolCount = -1;
44435
+ _lastEmittedMaxContext = -1;
44436
+ }
44437
+ return changed;
44021
44438
  }
44022
44439
  const calibrationKey = (model = a.ctx.model) => `${a.ctx.provider?.id ?? "unknown"}/${model}`;
44440
+ function stashRequestTokens(req) {
44441
+ const preFlight = estimateRequestTokens(
44442
+ req.messages,
44443
+ req.system,
44444
+ req.tools ?? [],
44445
+ calibrationKey(req.model)
44446
+ );
44447
+ a.ctx.lastRequestTokens = preFlight.total;
44448
+ _lastPreFlightMsgCount = req.messages.length;
44449
+ a.ctx.meta["lastRequestTokensAt"] = {
44450
+ msgCount: req.messages.length,
44451
+ toolCount: (req.tools ?? []).length
44452
+ };
44453
+ return preFlight;
44454
+ }
44455
+ function refreshContextRequestTokenStash(opts = {}) {
44456
+ const msgCount = a.ctx.messages.length;
44457
+ const toolCount = (a.ctx.tools ?? []).length;
44458
+ const stashed = a.ctx.lastRequestTokens;
44459
+ const stashedAt = a.ctx.meta?.["lastRequestTokensAt"];
44460
+ if (!opts.force && typeof stashed === "number" && stashed > 0 && typeof stashedAt === "object" && stashedAt !== null) {
44461
+ const meta = stashedAt;
44462
+ if (meta.msgCount === msgCount && (typeof meta.toolCount !== "number" || meta.toolCount === toolCount)) {
44463
+ return stashed;
44464
+ }
44465
+ }
44466
+ const refreshed = estimateRequestTokens(
44467
+ a.ctx.messages,
44468
+ a.ctx.systemPrompt,
44469
+ a.ctx.tools ?? [],
44470
+ calibrationKey()
44471
+ ).total;
44472
+ a.ctx.lastRequestTokens = refreshed;
44473
+ _lastPreFlightMsgCount = msgCount;
44474
+ a.ctx.meta["lastRequestTokensAt"] = { msgCount, toolCount };
44475
+ return refreshed;
44476
+ }
44477
+ async function buildRequestWithPreflightCompaction(opts) {
44478
+ let req = await handlers.response.buildAndRunRequestPipeline(opts);
44479
+ let preFlight = stashRequestTokens(req);
44480
+ if (await compactContextIfNeeded()) {
44481
+ req = await handlers.response.buildAndRunRequestPipeline(opts);
44482
+ preFlight = stashRequestTokens(req);
44483
+ }
44484
+ return { req, preFlight };
44485
+ }
44023
44486
  function emitContextPct() {
44024
44487
  const msgCount = a.ctx.messages.length;
44025
44488
  const toolCount = (a.ctx.tools ?? []).length;
@@ -44053,7 +44516,9 @@ function createAgentLoopHandler(a, handlers) {
44053
44516
  );
44054
44517
  total = est.total;
44055
44518
  }
44056
- a.events.emit("ctx.pct", { load: total / maxContext, tokens: total, maxContext });
44519
+ const rawLoad = maxContext > 0 ? total / maxContext : 0;
44520
+ const load = Math.max(0, Math.min(1, rawLoad));
44521
+ a.events.emit("ctx.pct", { load, rawLoad, tokens: total, maxContext });
44057
44522
  }
44058
44523
  function currentMaxContext() {
44059
44524
  const metaLimit = a.ctx.meta?.["effectiveMaxContext"];
@@ -44219,19 +44684,7 @@ function createAgentLoopHandler(a, handlers) {
44219
44684
  const reason = `interrupted: ${mailboxResult.interruptReason ?? "operator request"}`;
44220
44685
  return { status: "aborted", iterations, abortReason: reason, finalText };
44221
44686
  }
44222
- const req = await handlers.response.buildAndRunRequestPipeline(opts);
44223
- const preFlight = estimateRequestTokens(
44224
- req.messages,
44225
- req.system,
44226
- req.tools ?? [],
44227
- calibrationKey(req.model)
44228
- );
44229
- a.ctx.lastRequestTokens = preFlight.total;
44230
- _lastPreFlightMsgCount = req.messages.length;
44231
- a.ctx.meta["lastRequestTokensAt"] = {
44232
- msgCount: req.messages.length,
44233
- toolCount: (req.tools ?? []).length
44234
- };
44687
+ const { req, preFlight } = await buildRequestWithPreflightCompaction(opts);
44235
44688
  await a.ctx.session.append({
44236
44689
  type: "llm_request",
44237
44690
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -44343,10 +44796,10 @@ function createAgentLoopHandler(a, handlers) {
44343
44796
  toolLoopCount = 0;
44344
44797
  }
44345
44798
  if (toolUses.length === 0) {
44799
+ await compactContextIfNeeded();
44346
44800
  emitContextPct();
44347
44801
  a.events.emit("iteration.completed", { ctx: a.ctx, index: i });
44348
44802
  if (autonomousContinue && responseResult.directive === "continue") {
44349
- await compactContextIfNeeded();
44350
44803
  await a.extensions.runAfterIteration(a.ctx, i);
44351
44804
  continue;
44352
44805
  }
@@ -44364,15 +44817,15 @@ function createAgentLoopHandler(a, handlers) {
44364
44817
  throw toolErr;
44365
44818
  }
44366
44819
  if (autonomousContinue && consumeAutonomousContinue(a.ctx)) {
44820
+ await compactContextIfNeeded();
44367
44821
  emitContextPct();
44368
44822
  a.events.emit("iteration.completed", { ctx: a.ctx, index: i });
44369
- await compactContextIfNeeded();
44370
44823
  await a.extensions.runAfterIteration(a.ctx, i);
44371
44824
  continue;
44372
44825
  }
44826
+ await compactContextIfNeeded();
44373
44827
  emitContextPct();
44374
44828
  a.events.emit("iteration.completed", { ctx: a.ctx, index: i });
44375
- await compactContextIfNeeded();
44376
44829
  await a.extensions.runAfterIteration(a.ctx, i);
44377
44830
  if (autonomousContinue && responseResult.directive === "continue") {
44378
44831
  continue;
@@ -45486,6 +45939,52 @@ fresh suggestions via \`/suggest\`.`;
45486
45939
 
45487
45940
  // src/core/system-prompt-builder.ts
45488
45941
  var LAYER_1_IDENTITY = PROMPT;
45942
+ function effectiveShell(platform2, wrongstackShell) {
45943
+ if (platform2 !== "win32") return "posix";
45944
+ const v = wrongstackShell?.trim().toLowerCase();
45945
+ if (v === "powershell" || v === "powershell.exe") return "powershell";
45946
+ if (v === "pwsh" || v === "pwsh.exe") return "pwsh";
45947
+ if (v === "cmd" || v === "cmd.exe") return "cmd";
45948
+ return "cmd";
45949
+ }
45950
+ var SHELL_DISPLAY = {
45951
+ pwsh: "pwsh (PowerShell 7+) \u2014 write PowerShell syntax, not bash",
45952
+ powershell: "powershell (Windows PowerShell 5.1) \u2014 write PowerShell syntax, not bash",
45953
+ cmd: "cmd.exe (Command Prompt) \u2014 write cmd syntax, not bash"
45954
+ };
45955
+ function shellGuidanceBlock(shell, detail) {
45956
+ if (shell === "posix") return "";
45957
+ if (shell === "cmd") {
45958
+ if (detail === "short") {
45959
+ return "- Shell syntax: cmd.exe \u2014 use `%VAR%`, `2>nul`, `dir`/`type`/`del`/`where` (NOT bash `$VAR`, `/dev/null`, `ls`/`cat`/`rm`).";
45960
+ }
45961
+ return [
45962
+ "## Shell \u2014 cmd.exe",
45963
+ "The `bash` tool runs **cmd.exe** on this machine. Write cmd syntax, not bash/POSIX:",
45964
+ "- Env vars: `%NAME%` (NOT `$NAME`); set with `set NAME=value`.",
45965
+ "- Discard output: `2>nul` / `>nul` (NOT `2>/dev/null`).",
45966
+ "- No `ls`/`cat`/`rm`/`which`/`head` \u2014 use `dir`/`type`/`del`/`where` and `more`.",
45967
+ "- Chain with `&&` / `||` / `&`. Prefer the dedicated read/grep/glob tools over shell file ops."
45968
+ ].join("\n");
45969
+ }
45970
+ if (detail === "short") {
45971
+ return "- Shell syntax: PowerShell \u2014 use `$env:VAR`, `2>$null`, `Get-Content`/`Select-Object` (NOT bash `$VAR`, `/dev/null`, `cat`/`head`).";
45972
+ }
45973
+ const chain = shell === "pwsh" ? "- Chain with `&&` / `||` (supported in PowerShell 7)." : "- `&&` / `||` are NOT available in Windows PowerShell 5.1 \u2014 separate commands with `;` (and check `$LASTEXITCODE`).";
45974
+ return [
45975
+ `## Shell \u2014 PowerShell${shell === "pwsh" ? " 7+ (pwsh)" : " 5.1 (powershell)"}`,
45976
+ "The `bash` tool runs **PowerShell** on this machine. Write PowerShell syntax, not bash/POSIX:",
45977
+ "- Env vars: read `$env:NAME`, set `$env:NAME = 'value'` (NOT `$NAME`, `%NAME%`, or `export`).",
45978
+ "- Discard output: `... 2>$null` or `$null = ...` (NOT `2>/dev/null`).",
45979
+ "- No bash builtins \u2014 use cmdlets: `head -n N`\u2192`Select-Object -First N`, `tail`\u2192`-Last N`, `cat`\u2192`Get-Content`, `which x`\u2192`Get-Command x`, `rm -rf p`\u2192`Remove-Item -Recurse -Force p`, `touch f`\u2192`New-Item -ItemType File f`. Prefer the grep/glob tools over `Select-String`.",
45980
+ "- Read a line window of a file: `Get-Content path | Select-Object -Skip N -First M` (the `sed -n` / `head|tail` equivalent).",
45981
+ "- Pipes work normally; `rg`/`git`/`node` and other native exes run as-is \u2014 only the *shell builtins* differ. (`rg --files src | rg pattern` is fine.)",
45982
+ '- Call exes whose path has spaces via the call operator: `& "C:\\Program Files\\app.exe" args`.',
45983
+ "- Multi-line literals: single-quoted here-string `@'\u2026'@` with the closing `'@` at column 0.",
45984
+ "- Non-interactive only: no `Read-Host`/`Get-Credential`/`pause`; add `-Confirm:$false` to destructive cmdlets.",
45985
+ chain
45986
+ ].join("\n");
45987
+ }
45489
45988
  var DefaultSystemPromptBuilder = class {
45490
45989
  constructor(opts = {}) {
45491
45990
  this.opts = opts;
@@ -46059,7 +46558,8 @@ ${agentList}`;
46059
46558
  if (cached) return cached;
46060
46559
  const today = this.opts.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
46061
46560
  const platform2 = `${os6.platform()} ${os6.release()}`;
46062
- const shell = process.env.SHELL ?? process.env.ComSpec ?? "unknown";
46561
+ const effShell = effectiveShell(os6.platform(), process.env["WRONGSTACK_SHELL"]);
46562
+ const shell = effShell === "posix" ? process.env.SHELL ?? process.env.ComSpec ?? "unknown" : SHELL_DISPLAY[effShell];
46063
46563
  const node = process.version;
46064
46564
  const isGit = await this.dirExists(path3.join(ctx.projectRoot, ".git"));
46065
46565
  const [git, langs] = await Promise.all([
@@ -46107,6 +46607,10 @@ ${agentList}`;
46107
46607
  lines.push(`- Mode: ${this.opts.modeId}`);
46108
46608
  }
46109
46609
  }
46610
+ if (effShell !== "posix" && tier !== "minimal") {
46611
+ const guide = shellGuidanceBlock(effShell, tier === "light" ? "short" : "full");
46612
+ if (guide) lines.push("", guide);
46613
+ }
46110
46614
  if (this.skillCache) {
46111
46615
  lines.push(
46112
46616
  "",
@@ -47422,7 +47926,6 @@ var PhaseGraphBuilder = class _PhaseGraphBuilder {
47422
47926
  const phase = phaseArray[i];
47423
47927
  if (!phase) continue;
47424
47928
  phase.nextPhases = i < phaseArray.length - 1 ? [phaseArray[i + 1]?.id ?? ""] : [];
47425
- phase.dependsOn = i > 0 ? [phaseArray[i - 1]?.id ?? ""] : [];
47426
47929
  }
47427
47930
  const graph = {
47428
47931
  id: graphId,
@@ -47526,6 +48029,7 @@ var PhaseOrchestrator = class {
47526
48029
  async start() {
47527
48030
  this.stopped = false;
47528
48031
  this.paused = false;
48032
+ this.normalizeForResume();
47529
48033
  this.graph.startedAt = Date.now();
47530
48034
  this.graph.updatedAt = Date.now();
47531
48035
  let readyPhases = this.getReadyPhases();
@@ -47548,6 +48052,30 @@ var PhaseOrchestrator = class {
47548
48052
  this.tickInterval = setInterval(() => this.tick(), 1e3);
47549
48053
  }
47550
48054
  }
48055
+ /**
48056
+ * Make a (possibly resumed) graph runnable. A graph loaded from disk after an
48057
+ * interrupted run can carry transient state from the dead process: phases left
48058
+ * `running` and tasks left `in_progress`. The scheduler only starts `pending`
48059
+ * phases (getReadyPhases) and only runs `pending` tasks (getExecutableTasks),
48060
+ * so without this a resumed phase/task would stall forever. Reset that
48061
+ * transient state to `pending`; terminal phases (completed/failed/skipped) and
48062
+ * already-completed tasks are untouched, so completed work is never re-run.
48063
+ * For a freshly built graph this is a no-op.
48064
+ */
48065
+ normalizeForResume() {
48066
+ this.graph.activePhaseIds = [];
48067
+ for (const phase of this.graph.phases.values()) {
48068
+ if (phase.status === "running") {
48069
+ phase.status = "pending";
48070
+ phase.updatedAt = Date.now();
48071
+ }
48072
+ if (phase.status === "pending") {
48073
+ for (const task of phase.taskGraph.nodes.values()) {
48074
+ if (task.status === "in_progress") task.status = "pending";
48075
+ }
48076
+ }
48077
+ }
48078
+ }
47551
48079
  /** Wait for all pending phase merges, dependency-ordered and globally serialized. */
47552
48080
  async drainMerges() {
47553
48081
  await Promise.allSettled([...this.phaseMergePromise.values()]);
@@ -47754,6 +48282,24 @@ var PhaseOrchestrator = class {
47754
48282
  this.ctx.onPhaseFail?.(phase, new Error(error));
47755
48283
  await this.keepWorktreeForReview(phase);
47756
48284
  }
48285
+ /**
48286
+ * A phase whose tasks all succeeded was marked `completed` and queued for
48287
+ * merge, but the merge back into the base branch failed. Its work is NOT
48288
+ * integrated, so correct the graph: move the phase out of `completedPhaseIds`
48289
+ * into `failedPhaseIds` and flip its status to `failed`. Without this the
48290
+ * persisted graph (and the board) would claim the phase succeeded while a
48291
+ * `phase.failed` event fired — an inconsistency that hides un-merged work.
48292
+ * Idempotent: safe to call more than once for the same phase.
48293
+ */
48294
+ markPhaseMergeFailed(phase, error) {
48295
+ if (phase.status !== "failed") {
48296
+ this.graph.completedPhaseIds = this.graph.completedPhaseIds.filter((id) => id !== phase.id);
48297
+ this.updatePhaseStatus(phase, "failed");
48298
+ }
48299
+ if (!this.graph.failedPhaseIds.includes(phase.id)) this.graph.failedPhaseIds.push(phase.id);
48300
+ this.emit("phase.failed", { phaseId: phase.id, name: phase.name, error });
48301
+ this.ctx.onPhaseFail?.(phase, new Error(error));
48302
+ }
47757
48303
  /** Trim long verifier output so it fits cleanly in an event/error message. */
47758
48304
  truncate(text, max = 500) {
47759
48305
  const t2 = text.trim();
@@ -47785,7 +48331,7 @@ var PhaseOrchestrator = class {
47785
48331
  message: msg,
47786
48332
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
47787
48333
  }));
47788
- this.emit("phase.failed", { phaseId: phase.id, name: phase.name, error: msg });
48334
+ this.markPhaseMergeFailed(phase, msg);
47789
48335
  });
47790
48336
  await this.mergeQueue;
47791
48337
  })();
@@ -47830,11 +48376,7 @@ var PhaseOrchestrator = class {
47830
48376
  worktreeDir: handle.dir,
47831
48377
  error: toErrorMessage(err)
47832
48378
  });
47833
- this.emit("phase.failed", {
47834
- phaseId: phase.id,
47835
- name: phase.name,
47836
- error: `worktree merge failed: ${toErrorMessage(err)}`
47837
- });
48379
+ this.markPhaseMergeFailed(phase, `worktree merge failed: ${toErrorMessage(err)}`);
47838
48380
  }
47839
48381
  }
47840
48382
  async shouldAttemptConflictResolution(phase, info) {
@@ -47986,7 +48528,7 @@ var PhaseOrchestrator = class {
47986
48528
  const dep = this.graph.phases.get(depId);
47987
48529
  return dep?.status === "completed" || dep?.status === "skipped" || dep?.status === "failed";
47988
48530
  });
47989
- if (depsDone || phase.parallelizable) {
48531
+ if (depsDone) {
47990
48532
  ready.push(phase);
47991
48533
  }
47992
48534
  }
@@ -48364,8 +48906,8 @@ var AutoPhaseRunner = class {
48364
48906
  }
48365
48907
  this.maxRunTimer?.unref?.();
48366
48908
  if (this.opts.events) {
48367
- const events = this.opts.events;
48368
- const onUntyped = events.on;
48909
+ const bus = this.opts.events;
48910
+ const onUntyped = (event, handler) => bus.on(event, handler);
48369
48911
  this.unsubscribeCompleted = onUntyped("graph.completed", this.graphCompletedHandler);
48370
48912
  this.unsubscribeFailed = onUntyped("graph.failed", this.graphFailedHandler);
48371
48913
  }