@wrongstack/core 0.309.1 → 0.310.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/chronicle/index.js +113 -41
  2. package/dist/chronicle/metrics-ingest.d.ts +7 -0
  3. package/dist/chronicle/metrics-schema.d.ts +4 -1
  4. package/dist/chronicle/project-server.js +94 -38
  5. package/dist/chronicle/query.d.ts +1 -0
  6. package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
  7. package/dist/chronicle/types.d.ts +2 -0
  8. package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
  9. package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
  10. package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
  11. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
  12. package/dist/coordination/fleet-status-tool.d.ts +1 -1
  13. package/dist/coordination/index.d.ts +5 -1
  14. package/dist/coordination/index.js +2061 -346
  15. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  16. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  17. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  18. package/dist/coordination/mailbox-project-server.js +14 -9
  19. package/dist/core/context.d.ts +35 -44
  20. package/dist/core/conversation-state.d.ts +16 -52
  21. package/dist/core/index.js +53 -16
  22. package/dist/core/provider-runner.d.ts +2 -2
  23. package/dist/core/run-env.d.ts +7 -28
  24. package/dist/core/streaming-response-builder.d.ts +2 -2
  25. package/dist/execution/index.js +111 -153
  26. package/dist/goal/index.js +100 -22
  27. package/dist/hq/auth-store.d.ts +9 -0
  28. package/dist/hq/cost-bridge.d.ts +1 -1
  29. package/dist/hq/index.js +24 -2
  30. package/dist/index.d.ts +12 -5
  31. package/dist/index.js +27806 -33937
  32. package/dist/infrastructure/index.js +210 -132
  33. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  34. package/dist/infrastructure/token-counter.d.ts +5 -1
  35. package/dist/kernel/events/file-events.d.ts +6 -0
  36. package/dist/kernel/events/provider-events.d.ts +10 -7
  37. package/dist/kernel/events/session-events.d.ts +4 -4
  38. package/dist/kernel/events/tool-events.d.ts +12 -2
  39. package/dist/plugin/index.js +105 -35
  40. package/dist/security/index.d.ts +1 -0
  41. package/dist/security/index.js +47 -18
  42. package/dist/security/kanban-boundary.d.ts +1 -1
  43. package/dist/security/kanban-governance-port.d.ts +28 -0
  44. package/dist/session-catalog/index.js +2 -3
  45. package/dist/session-catalog/project-server.js +2 -3
  46. package/dist/session-catalog/protocol.d.ts +1 -1
  47. package/dist/session-catalog/registry.d.ts +1 -1
  48. package/dist/session-catalog/store-schema.d.ts +1 -1
  49. package/dist/session-catalog/store.d.ts +1 -1
  50. package/dist/storage/annotations-store.d.ts +1 -1
  51. package/dist/storage/board-store-port.d.ts +45 -0
  52. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  53. package/dist/storage/config-loader/types.d.ts +1 -1
  54. package/dist/storage/event-bus-port.d.ts +27 -0
  55. package/dist/storage/file-session-writer.d.ts +47 -5
  56. package/dist/storage/goal-coordination.d.ts +1 -1
  57. package/dist/storage/goal-store.d.ts +1 -1
  58. package/dist/storage/index.d.ts +3 -4
  59. package/dist/storage/index.js +1063 -1459
  60. package/dist/storage/plan-store.d.ts +1 -1
  61. package/dist/storage/queue-store.d.ts +1 -1
  62. package/dist/storage/replay-log-store.d.ts +1 -1
  63. package/dist/storage/session-recovery.d.ts +10 -0
  64. package/dist/storage/session-resume-validation.d.ts +13 -1
  65. package/dist/storage/session-store/events.d.ts +1 -1
  66. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  67. package/dist/storage/session-store/rename-session.d.ts +1 -1
  68. package/dist/storage/session-store/resume-session.d.ts +3 -1
  69. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  70. package/dist/storage/session-store/types.d.ts +1 -1
  71. package/dist/storage/session-store.d.ts +70 -0
  72. package/dist/storage/session-summary-tracker.d.ts +8 -0
  73. package/dist/storage/session-write-buffer.d.ts +31 -2
  74. package/dist/storage/task-store.d.ts +1 -1
  75. package/dist/storage/todos-checkpoint.d.ts +1 -1
  76. package/dist/storage/tool-audit-log.d.ts +1 -1
  77. package/dist/tasking/index.js +100 -22
  78. package/dist/tasking/task-tracker.d.ts +24 -1
  79. package/dist/types/compactor.d.ts +2 -2
  80. package/dist/types/context.d.ts +212 -0
  81. package/dist/types/conversation-state.d.ts +109 -0
  82. package/dist/types/error-handler.d.ts +2 -2
  83. package/dist/types/file-event-record.d.ts +6 -0
  84. package/dist/types/index.d.ts +3 -3
  85. package/dist/types/index.js +17 -1
  86. package/dist/types/permission.d.ts +3 -3
  87. package/dist/types/plugin.d.ts +3 -3
  88. package/dist/types/provider-runner.d.ts +2 -2
  89. package/dist/types/provider.d.ts +10 -0
  90. package/dist/types/run-env.d.ts +32 -0
  91. package/dist/types/session.d.ts +3 -0
  92. package/dist/types/slash-command.d.ts +2 -2
  93. package/dist/types/token-counter.d.ts +30 -1
  94. package/dist/types/tool-executor.d.ts +2 -2
  95. package/dist/types/tool.d.ts +5 -5
  96. package/dist/utils/context-breakdown.d.ts +2 -2
  97. package/dist/utils/context-evidence.d.ts +12 -12
  98. package/dist/utils/crash-shield.d.ts +9 -0
  99. package/dist/utils/heap-watchdog.js +11 -3
  100. package/dist/utils/index.d.ts +1 -1
  101. package/dist/utils/index.js +72 -156
  102. package/dist/utils/regex-guard.d.ts +7 -30
  103. package/dist/utils/todos-format.d.ts +1 -1
  104. package/dist/utils/tree-kill.d.ts +2 -0
  105. package/dist/utils/tree-kill.js +1 -0
  106. package/instructions/coordination/subagent-baseline.md +10 -0
  107. package/instructions/system-lite.md +2 -0
  108. package/instructions/system-pro.md +40 -0
  109. package/instructions/system.md +15 -0
  110. package/package.json +6 -9
  111. package/dist/defaults/index.d.ts +0 -69
  112. package/dist/defaults/index.js +0 -38133
  113. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  114. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  115. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  116. /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { Context } from '../core/context.js';
1
+ import type { AgentContext } from './context.js';
2
2
  export type Permission = 'auto' | 'confirm' | 'deny';
3
3
  /**
4
4
  * Risk tier for tools in UI/audit surfaces. YOLO auto-approves non-denied
@@ -150,7 +150,7 @@ export interface Tool<I = unknown, O = unknown> {
150
150
  * Each UI maps this id to its own icon library.
151
151
  */
152
152
  icon?: ToolIconId | undefined;
153
- execute(input: I, ctx: Context, opts: {
153
+ execute(input: I, ctx: AgentContext, opts: {
154
154
  signal: AbortSignal;
155
155
  }): Promise<O>;
156
156
  /**
@@ -176,7 +176,7 @@ export interface Tool<I = unknown, O = unknown> {
176
176
  * have intermediate state shouldn't implement this; the default `execute`
177
177
  * path is more efficient.
178
178
  */
179
- executeStream?(input: I, ctx: Context, opts: {
179
+ executeStream?(input: I, ctx: AgentContext, opts: {
180
180
  signal: AbortSignal;
181
181
  }): AsyncIterable<ToolStreamEvent<O>>;
182
182
  /**
@@ -222,7 +222,7 @@ export interface Tool<I = unknown, O = unknown> {
222
222
  * Avoid double-free by gating one on the other's effect, or pick a single
223
223
  * teardown channel per resource.
224
224
  */
225
- cleanup?(input: I, ctx: Context): Promise<void>;
225
+ cleanup?(input: I, ctx: AgentContext): Promise<void>;
226
226
  /**
227
227
  * Optional custom output serializer. When present, the executor's output
228
228
  * serializer calls this INSTEAD of the central `renderToolObject()` switch
@@ -243,7 +243,7 @@ export interface ToolCallContext {
243
243
  tool: Tool;
244
244
  input: unknown;
245
245
  callId: string;
246
- ctx: Context;
246
+ ctx: AgentContext;
247
247
  signal: AbortSignal;
248
248
  }
249
249
  /**
@@ -1,4 +1,4 @@
1
- import type { Context } from '../core/context.js';
1
+ import type { AgentContext } from '../types/context.js';
2
2
  import { type SystemBlockSource } from '../core/system-prompt-builder.js';
3
3
  /**
4
4
  * Real, per-category token accounting for the live context window — the data
@@ -61,5 +61,5 @@ export interface ContextBreakdown {
61
61
  * interactively (e.g. from `/context`): it walks the full message array once, so
62
62
  * it is O(messages·blocks), not the O(1) cached path the per-turn bar uses.
63
63
  */
64
- export declare function getContextBreakdown(ctx: Context): ContextBreakdown;
64
+ export declare function getContextBreakdown(ctx: AgentContext): ContextBreakdown;
65
65
  //# sourceMappingURL=context-breakdown.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { Context } from '../core/context.js';
1
+ import type { AgentContext } from '../types/context.js';
2
2
  import { type TextBlock } from '../types/blocks.js';
3
3
  import type { CompactReport } from '../types/compactor.js';
4
4
  import type { CompletedWorkEvidence, CompletedWorkSource, ContextEvidenceState, ToolOutputMetadata } from '../types/context-evidence.js';
@@ -13,7 +13,7 @@ export interface RecordToolOutputEvidenceInput {
13
13
  outputTokens?: number | undefined;
14
14
  outputLines?: number | undefined;
15
15
  }
16
- export declare function recordUserIntentEvidence(ctx: Context, text: string): void;
16
+ export declare function recordUserIntentEvidence(ctx: AgentContext, text: string): void;
17
17
  /** Runtime steering/status blocks must not displace real conversation turns. */
18
18
  export declare function isRuntimeContextInput(text: string): boolean;
19
19
  /**
@@ -25,11 +25,11 @@ export declare function isRuntimeContextInput(text: string): boolean;
25
25
  * It is volatile provider evidence, so it never grows the durable chat log or
26
26
  * invalidates the cached base prompt.
27
27
  */
28
- export declare function buildConversationContinuityBlock(ctx: Pick<Context, 'contextEvidence' | 'messages'>): TextBlock | undefined;
29
- export declare function recordToolOutputEvidence(ctx: Context, input: RecordToolOutputEvidenceInput): ToolOutputMetadata;
30
- export declare function markAssistantReferencedEvidence(ctx: Context, text: string): void;
31
- export declare function buildContextEvidenceDigest(ctx: Context): string;
32
- export declare function repeatedReadPressure(ctx: Context): number;
28
+ export declare function buildConversationContinuityBlock(ctx: Pick<AgentContext, 'contextEvidence' | 'messages'>): TextBlock | undefined;
29
+ export declare function recordToolOutputEvidence(ctx: AgentContext, input: RecordToolOutputEvidenceInput): ToolOutputMetadata;
30
+ export declare function markAssistantReferencedEvidence(ctx: AgentContext, text: string): void;
31
+ export declare function buildContextEvidenceDigest(ctx: AgentContext): string;
32
+ export declare function repeatedReadPressure(ctx: AgentContext): number;
33
33
  /**
34
34
  * Deterministic post-compaction sanity check. Cheap and local: records whether
35
35
  * the compacted context still carries an intent anchor and a tool/path trail.
@@ -37,7 +37,7 @@ export declare function repeatedReadPressure(ctx: Context): number;
37
37
  * only HybridCompactor did). Advisory on its own — pair with
38
38
  * `injectEvidenceFloor` to actually repair a flagged loss.
39
39
  */
40
- export declare function checkCompactionQuality(ctx: Context, opts: {
40
+ export declare function checkCompactionQuality(ctx: AgentContext, opts: {
41
41
  collapsedDigest?: string | undefined;
42
42
  evidenceDigest?: string | undefined;
43
43
  reduced: boolean;
@@ -50,7 +50,7 @@ export declare function checkCompactionQuality(ctx: Context, opts: {
50
50
  * no-op when quality is fine or there is no evidence to inject. Returns true
51
51
  * when it injected a block (so the caller re-estimates tokens).
52
52
  */
53
- export declare function injectEvidenceFloor(ctx: Context, quality: CompactReport['quality'] | undefined): boolean;
53
+ export declare function injectEvidenceFloor(ctx: AgentContext, quality: CompactReport['quality'] | undefined): boolean;
54
54
  /** Marker prefixing the ledger's system-prompt block (also used to find/replace it). */
55
55
  export declare const COMPLETED_WORK_LEDGER_MARKER = "[completed_work_ledger]";
56
56
  export interface RecordCompletedWorkInput {
@@ -68,14 +68,14 @@ export interface RecordCompletedWorkInput {
68
68
  * composer renders this state as a volatile tail block; the stable system-prompt
69
69
  * prefix is never mutated here.
70
70
  */
71
- export declare function recordCompletedWorkEvidence(ctx: Context, input: RecordCompletedWorkInput): CompletedWorkEvidence;
71
+ export declare function recordCompletedWorkEvidence(ctx: AgentContext, input: RecordCompletedWorkInput): CompletedWorkEvidence;
72
72
  /** Render the ledger's system-prompt block text (marker + newest entries). */
73
73
  export declare function formatCompletedWorkLedger(items: readonly CompletedWorkEvidence[]): string;
74
74
  /** Build the current volatile completed-work block without mutating the prompt. */
75
- export declare function buildCompletedWorkLedgerBlock(ctx: Context): TextBlock | undefined;
75
+ export declare function buildCompletedWorkLedgerBlock(ctx: AgentContext): TextBlock | undefined;
76
76
  /**
77
77
  * @deprecated Volatile state must be composed at request time. Kept as a
78
78
  * compatibility no-op for embedders importing the old helper.
79
79
  */
80
- export declare function syncCompletedWorkLedgerBlock(_ctx: Context): void;
80
+ export declare function syncCompletedWorkLedgerBlock(_ctx: AgentContext): void;
81
81
  //# sourceMappingURL=context-evidence.d.ts.map
@@ -1,3 +1,12 @@
1
+ /** Register a synchronous salvage hook. Returns an unregister function. */
2
+ export declare function addFatalSalvageHook(hook: () => void): () => void;
3
+ /**
4
+ * Run every registered salvage hook once. Swallows individual hook failures
5
+ * so one broken hook cannot skip the remaining ones. Safe to call multiple
6
+ * times — hooks are expected to be idempotent (a drained buffer stays
7
+ * drained).
8
+ */
9
+ export declare function runFatalSalvageSync(): void;
1
10
  interface CrashShieldTarget {
2
11
  on(event: 'unhandledRejection' | 'uncaughtException', listener: (reason: unknown) => void): unknown;
3
12
  off(event: 'unhandledRejection' | 'uncaughtException', listener: (reason: unknown) => void): unknown;
@@ -2,7 +2,10 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as fsp2 from "node:fs/promises";
4
4
  import * as path3 from "node:path";
5
- import { PerformanceObserver, constants as performanceConstants } from "node:perf_hooks";
5
+ import {
6
+ PerformanceObserver,
7
+ constants as performanceConstants
8
+ } from "node:perf_hooks";
6
9
  import * as v8 from "node:v8";
7
10
 
8
11
  // src/utils/memory-flight-recorder.ts
@@ -308,7 +311,8 @@ function createMemoryFlightRecorder(options = {}) {
308
311
  const now = options.now ?? Date.now;
309
312
  const startedAt = now();
310
313
  const root = options.diagnosticsRoot ?? path2.join(wstackGlobalRoot(), "diagnostics", "memory");
311
- const enabled = options.enabled ?? (process.env["WRONGSTACK_MEMORY_FLIGHT_RECORDER"] !== "0" && process.env["NODE_ENV"] !== "test");
314
+ const isBunRuntime = typeof globalThis.Bun !== "undefined";
315
+ const enabled = options.enabled ?? (!isBunRuntime && process.env["WRONGSTACK_MEMORY_FLIGHT_RECORDER"] !== "0" && process.env["NODE_ENV"] !== "test");
312
316
  const writer = options.captureWriter ?? (enabled ? new ContinuousMemoryCaptureWriter(root) : disabledWriter(path2.join(root, `${timestampSlug()}-pid-${process.pid}`)));
313
317
  const observations = [];
314
318
  const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
@@ -418,7 +422,11 @@ function defaultHeapLogPath() {
418
422
  function takeHeapSample() {
419
423
  const m = process.memoryUsage();
420
424
  const heap = v8.getHeapStatistics();
421
- const spaces = v8.getHeapSpaceStatistics();
425
+ let spaces = [];
426
+ try {
427
+ spaces = v8.getHeapSpaceStatistics();
428
+ } catch {
429
+ }
422
430
  const spaceUsed = (name) => spaces.find((space) => space.space_name === name)?.space_used_size ?? 0;
423
431
  const limit = heap.heap_size_limit || 0;
424
432
  let activeResources = 0;
@@ -7,7 +7,7 @@ export * from './config-backup.js';
7
7
  export * from './config-json.js';
8
8
  export { checkConnectivity, resetConnectivityCache } from './connectivity.js';
9
9
  export { type ContextBreakdown, getContextBreakdown } from './context-breakdown.js';
10
- export { type CrashShieldOptions, installCrashShield } from './crash-shield.js';
10
+ export { addFatalSalvageHook, runFatalSalvageSync, type CrashShieldOptions, installCrashShield, } from './crash-shield.js';
11
11
  export { buildCompletedWorkLedgerBlock, buildContextEvidenceDigest, COMPLETED_WORK_LEDGER_MARKER, createContextEvidenceState, formatCompletedWorkLedger, markAssistantReferencedEvidence, type RecordCompletedWorkInput, type RecordToolOutputEvidenceInput, recordCompletedWorkEvidence, recordToolOutputEvidence, recordUserIntentEvidence, repeatedReadPressure, syncCompletedWorkLedgerBlock, } from './context-evidence.js';
12
12
  export { type DeepMergeOptions, deepMerge, FORBIDDEN_PROTO_KEYS, isPrimitiveArray, } from './deep-merge.js';
13
13
  export * from './diff.js';
@@ -1915,6 +1915,22 @@ function getContextBreakdown(ctx) {
1915
1915
  // src/utils/crash-shield.ts
1916
1916
  var CRASH_STORM_WINDOW_MS = 6e4;
1917
1917
  var CRASH_STORM_LIMIT = 20;
1918
+ var fatalSalvageHooks = [];
1919
+ function addFatalSalvageHook(hook) {
1920
+ fatalSalvageHooks.push(hook);
1921
+ return () => {
1922
+ const idx = fatalSalvageHooks.indexOf(hook);
1923
+ if (idx >= 0) fatalSalvageHooks.splice(idx, 1);
1924
+ };
1925
+ }
1926
+ function runFatalSalvageSync() {
1927
+ for (const hook of [...fatalSalvageHooks]) {
1928
+ try {
1929
+ hook();
1930
+ } catch {
1931
+ }
1932
+ }
1933
+ }
1918
1934
  function isBrokenOutputConsumer(error) {
1919
1935
  if (typeof error !== "object" || error === null || !("code" in error)) return false;
1920
1936
  return error.code === "EPIPE" || error.code === "ECONNRESET" || error.code === "ECONNABORTED";
@@ -1950,6 +1966,7 @@ function installCrashShield(options = {}) {
1950
1966
  `error storm: >${CRASH_STORM_LIMIT} in ${CRASH_STORM_WINDOW_MS / 1e3}s \u2014 exiting`
1951
1967
  )
1952
1968
  );
1969
+ runFatalSalvageSync();
1953
1970
  exit(1);
1954
1971
  }
1955
1972
  };
@@ -2232,49 +2249,57 @@ async function expandGlob(pattern) {
2232
2249
  async function walk2(dir, pat) {
2233
2250
  let entries;
2234
2251
  try {
2235
- entries = await fsp.readdir(dir);
2252
+ entries = await fsp.readdir(dir, { withFileTypes: true });
2236
2253
  } catch {
2237
2254
  return;
2238
2255
  }
2239
- const firstGlob = pat.search(/[*?[[]/);
2240
- if (firstGlob < 0) {
2241
- const re = globToRegex(pat);
2256
+ if (pat.startsWith("**/")) {
2257
+ const rest = pat.slice(3);
2258
+ await walk2(dir, rest);
2242
2259
  for (const e of entries) {
2243
- if (re.test(e)) {
2244
- const full = `${dir}${SEP}${e}`;
2245
- results.add(abs ? resolve2(full) : full);
2260
+ if (e.isDirectory()) {
2261
+ const subDir = `${dir}${SEP}${e.name}`;
2262
+ await walk2(subDir, pat);
2246
2263
  }
2247
2264
  }
2248
2265
  return;
2249
2266
  }
2250
- const before = pat.slice(0, firstGlob);
2251
- const rest = pat.slice(firstGlob);
2252
- if (before.endsWith("**")) {
2253
- await walk2(dir, rest);
2267
+ if (pat === "**") {
2254
2268
  for (const e of entries) {
2255
- const full = `${dir}${SEP}${e}`;
2256
- try {
2257
- const stat4 = await fsp.stat(full);
2258
- if (stat4.isDirectory()) await walk2(full, rest);
2259
- } catch {
2269
+ const full = `${dir}${SEP}${e.name}`;
2270
+ results.add(abs ? resolve2(full) : full);
2271
+ if (e.isDirectory()) {
2272
+ await walk2(full, "**");
2260
2273
  }
2261
2274
  }
2262
- } else if (before === "") {
2263
- const re = globToRegex(rest);
2275
+ return;
2276
+ }
2277
+ const firstSlash = pat.indexOf("/");
2278
+ if (firstSlash < 0) {
2279
+ const re = globToRegex(pat);
2264
2280
  for (const e of entries) {
2265
- if (re.test(e)) {
2266
- const full = `${dir}${SEP}${e}`;
2281
+ if (re.test(e.name)) {
2282
+ const full = `${dir}${SEP}${e.name}`;
2267
2283
  results.add(abs ? resolve2(full) : full);
2268
2284
  }
2269
2285
  }
2286
+ return;
2287
+ }
2288
+ const currentSeg = pat.slice(0, firstSlash);
2289
+ const remainingPat = pat.slice(firstSlash + 1);
2290
+ if (isGlob(currentSeg)) {
2291
+ const re = globToRegex(currentSeg);
2292
+ for (const e of entries) {
2293
+ if (e.isDirectory() && re.test(e.name)) {
2294
+ const subDir = `${dir}${SEP}${e.name}`;
2295
+ await walk2(subDir, remainingPat);
2296
+ }
2297
+ }
2270
2298
  } else {
2271
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
2272
- if (entries.includes(seg)) {
2273
- const full = `${dir}${SEP}${seg}`;
2274
- try {
2275
- const stat4 = await fsp.stat(full);
2276
- if (stat4.isDirectory()) await walk2(full, rest);
2277
- } catch {
2299
+ for (const e of entries) {
2300
+ if (e.isDirectory() && e.name === currentSeg) {
2301
+ const subDir = `${dir}${SEP}${e.name}`;
2302
+ await walk2(subDir, remainingPat);
2278
2303
  }
2279
2304
  }
2280
2305
  }
@@ -2379,7 +2404,10 @@ function matchAnyCommand(patterns, input) {
2379
2404
  import * as fs4 from "node:fs";
2380
2405
  import * as fsp3 from "node:fs/promises";
2381
2406
  import * as path5 from "node:path";
2382
- import { PerformanceObserver, constants as performanceConstants } from "node:perf_hooks";
2407
+ import {
2408
+ PerformanceObserver,
2409
+ constants as performanceConstants
2410
+ } from "node:perf_hooks";
2383
2411
  import * as v8 from "node:v8";
2384
2412
 
2385
2413
  // src/utils/memory-flight-recorder.ts
@@ -2696,8 +2724,8 @@ async function pruneOldSessions(root, currentDir) {
2696
2724
  const sessions = await Promise.all(
2697
2725
  entries.filter((entry) => entry.isDirectory() && entry.name !== path4.basename(currentDir)).map(async (entry) => {
2698
2726
  const fullPath = path4.join(root, entry.name);
2699
- const stat4 = await fsp2.stat(fullPath);
2700
- return { fullPath, mtimeMs: stat4.mtimeMs };
2727
+ const stat3 = await fsp2.stat(fullPath);
2728
+ return { fullPath, mtimeMs: stat3.mtimeMs };
2701
2729
  })
2702
2730
  );
2703
2731
  sessions.sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -2815,7 +2843,8 @@ function createMemoryFlightRecorder(options = {}) {
2815
2843
  const now = options.now ?? Date.now;
2816
2844
  const startedAt = now();
2817
2845
  const root = options.diagnosticsRoot ?? path4.join(wstackGlobalRoot(), "diagnostics", "memory");
2818
- const enabled = options.enabled ?? (process.env["WRONGSTACK_MEMORY_FLIGHT_RECORDER"] !== "0" && process.env["NODE_ENV"] !== "test");
2846
+ const isBunRuntime = typeof globalThis.Bun !== "undefined";
2847
+ const enabled = options.enabled ?? (!isBunRuntime && process.env["WRONGSTACK_MEMORY_FLIGHT_RECORDER"] !== "0" && process.env["NODE_ENV"] !== "test");
2819
2848
  const writer = options.captureWriter ?? (enabled ? new ContinuousMemoryCaptureWriter(root) : disabledWriter(path4.join(root, `${timestampSlug()}-pid-${process.pid}`)));
2820
2849
  const observations = [];
2821
2850
  const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
@@ -2925,7 +2954,11 @@ function defaultHeapLogPath() {
2925
2954
  function takeHeapSample() {
2926
2955
  const m = process.memoryUsage();
2927
2956
  const heap = v8.getHeapStatistics();
2928
- const spaces = v8.getHeapSpaceStatistics();
2957
+ let spaces = [];
2958
+ try {
2959
+ spaces = v8.getHeapSpaceStatistics();
2960
+ } catch {
2961
+ }
2929
2962
  const spaceUsed = (name) => spaces.find((space) => space.space_name === name)?.space_used_size ?? 0;
2930
2963
  const limit = heap.heap_size_limit || 0;
2931
2964
  let activeResources = 0;
@@ -4270,130 +4303,11 @@ function watchProjectTree(root, listener, opts) {
4270
4303
  }
4271
4304
 
4272
4305
  // src/utils/regex-guard.ts
4273
- var MAX_PATTERN_LEN = 256;
4274
- var DANGEROUS_PATTERNS = [
4275
- // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
4276
- /(\([^)]*[+*][^)]*\))[+*]/,
4277
- /(\(\?:[^)]*[+*][^)]*\))[+*]/,
4278
- // Adjacent quantifiers: a++ a*+
4279
- /[+*]{2,}/,
4280
- // Quantifier on alternation with length 2+
4281
- /\([^|)]+\|[^)]+\)[+*][+*]/,
4282
- // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
4283
- /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
4284
- ];
4285
- function hasAmbiguousQuantifiedAlternation(pattern) {
4286
- for (let i = 0; i < pattern.length; i++) {
4287
- if (pattern[i] !== "(") continue;
4288
- if (i > 0 && pattern[i - 1] === "\\") continue;
4289
- let depth = 0;
4290
- let inClass = false;
4291
- let j = i;
4292
- for (; j < pattern.length; j++) {
4293
- const ch = pattern[j];
4294
- if (ch === "\\") {
4295
- j++;
4296
- continue;
4297
- }
4298
- if (inClass) {
4299
- if (ch === "]") inClass = false;
4300
- continue;
4301
- }
4302
- if (ch === "[") {
4303
- inClass = true;
4304
- continue;
4305
- }
4306
- if (ch === "(") depth++;
4307
- else if (ch === ")") {
4308
- depth--;
4309
- if (depth === 0) break;
4310
- }
4311
- }
4312
- if (j >= pattern.length) return false;
4313
- const next = pattern[j + 1];
4314
- if (next !== "+" && next !== "*" && next !== "{") continue;
4315
- let inner = pattern.slice(i + 1, j);
4316
- inner = inner.replace(/^\?(?::|<?[=!])/u, "");
4317
- const branches = [];
4318
- let current = "";
4319
- let d = 0;
4320
- let cls = false;
4321
- for (let k = 0; k < inner.length; k++) {
4322
- const ch = inner[k];
4323
- if (ch === "\\") {
4324
- current += ch + (inner[k + 1] ?? "");
4325
- k++;
4326
- continue;
4327
- }
4328
- if (cls) {
4329
- if (ch === "]") cls = false;
4330
- current += ch;
4331
- continue;
4332
- }
4333
- if (ch === "[") {
4334
- cls = true;
4335
- current += ch;
4336
- continue;
4337
- }
4338
- if (ch === "(") d++;
4339
- if (ch === ")") d--;
4340
- if (ch === "|" && d === 0) {
4341
- branches.push(current);
4342
- current = "";
4343
- continue;
4344
- }
4345
- current += ch;
4346
- }
4347
- branches.push(current);
4348
- if (branches.length < 2) continue;
4349
- for (let a = 0; a < branches.length; a++) {
4350
- for (let b = a + 1; b < branches.length; b++) {
4351
- const x = branches[a];
4352
- const y = branches[b];
4353
- if (x === "" || y === "") return true;
4354
- if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
4355
- }
4356
- }
4357
- }
4358
- return false;
4359
- }
4360
- function compileUserRegex(pattern, flags) {
4361
- if (typeof pattern !== "string") {
4362
- return { ok: false, reason: "pattern must be a string" };
4363
- }
4364
- if (pattern.length === 0) {
4365
- return { ok: false, reason: "pattern is empty" };
4366
- }
4367
- if (pattern.length > MAX_PATTERN_LEN) {
4368
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
4369
- }
4370
- for (const rx of DANGEROUS_PATTERNS) {
4371
- if (rx.test(pattern)) {
4372
- return {
4373
- ok: false,
4374
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
4375
- };
4376
- }
4377
- }
4378
- if (hasAmbiguousQuantifiedAlternation(pattern)) {
4379
- return {
4380
- ok: false,
4381
- reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
4382
- };
4383
- }
4384
- try {
4385
- return { ok: true, regex: new RegExp(pattern, flags) };
4386
- } catch (err) {
4387
- return {
4388
- ok: false,
4389
- reason: err instanceof Error ? err.message : "invalid regex"
4390
- };
4391
- }
4392
- }
4393
- var MAX_SUBJECT_LEN = 64 * 1024;
4394
- function capSubject(line) {
4395
- return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
4396
- }
4306
+ import {
4307
+ capSubject,
4308
+ compileUserRegex,
4309
+ MAX_SUBJECT_LEN
4310
+ } from "@wrongstack/primitives";
4397
4311
 
4398
4312
  // src/utils/safe-json.ts
4399
4313
  function safeParse(input, maxBytes = 5e6) {
@@ -5799,6 +5713,7 @@ export {
5799
5713
  WIRE_TOOL_NAME_MAX_LENGTH,
5800
5714
  WIRE_TOOL_NAME_PATTERN,
5801
5715
  activateProjectStateGuard,
5716
+ addFatalSalvageHook,
5802
5717
  applyToolDescriptionModeToTool,
5803
5718
  applyToolDescriptionModes,
5804
5719
  applyToolResultRenderModes,
@@ -5933,6 +5848,7 @@ export {
5933
5848
  resolveToolDescriptionMode,
5934
5849
  resolveToolResultRenderMode,
5935
5850
  resolveWstackPaths,
5851
+ runFatalSalvageSync,
5936
5852
  safeEmit,
5937
5853
  safeParse,
5938
5854
  safeProfileName,
@@ -1,35 +1,12 @@
1
1
  /**
2
2
  * Compile a user-supplied regex with conservative bounds against ReDoS.
3
3
  *
4
- * Duplicated from @wrongstack/tools/_regex.ts to avoid a circular
5
- * dependency (tools depends on core, not vice versa). There is a THIRD copy
6
- * `@wrongstack/kanban/src/verification/safe-regex.ts`which sits below both
7
- * in the workspace DAG for the same reason. This header used to say "keep
8
- * both copies in sync", unaware of the third, and this copy had drifted the
9
- * furthest: 2 of the 5 heuristics, a 512-character cap instead of 256, and no
10
- * subject cap at all. `packages/tools/tests/regex-guard-parity.test.ts` now
11
- * holds all three to the same verdicts — change one, change all three.
12
- *
13
- * V8's regex engine is backtracking-based and cannot interrupt a
14
- * synchronous match — a pattern like `(a+)+$` against a sufficiently
15
- * long line will pin a worker for seconds.
16
- */
17
- export interface CompileResult {
18
- ok: true;
19
- regex: RegExp;
20
- }
21
- export interface CompileFail {
22
- ok: false;
23
- reason: string;
24
- }
25
- export declare function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail;
26
- /**
27
- * Truncate a subject line to a safe length for synchronous regex eval.
28
- *
29
- * The canonical copy has always shipped this; this one did not, so callers
30
- * here bounded the pattern but never the SUBJECT — and a linear-time pattern
31
- * over a multi-megabyte line is still a stall.
4
+ * Canonical home since card #5: `@wrongstack/primitives/src/regex-guard.ts`
5
+ * (a dependency leaf, so every workspace tier including kanban, which sits
6
+ * below core and tools can share one implementation). This module is now a
7
+ * re-export shim: importers of `@wrongstack/core/utils` keep working, and
8
+ * `tools/tests/regex-guard-parity.test.ts` still pins all three historical
9
+ * entry points to identical verdicts (now trivially true one code path).
32
10
  */
33
- export declare const MAX_SUBJECT_LEN: number;
34
- export declare function capSubject(line: string): string;
11
+ export { type CompileFail, type CompileResult, capSubject, compileUserRegex, MAX_SUBJECT_LEN, } from '@wrongstack/primitives';
35
12
  //# sourceMappingURL=regex-guard.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { TodoItem } from '../core/context.js';
1
+ import type { TodoItem } from '../types/context.js';
2
2
  /**
3
3
  * Canonical text rendering of the live todo list, shared by the CLI's
4
4
  * `/todos` slash command and the TUI's auto-echo (which prints the same
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export interface KillableChild {
7
7
  readonly pid?: number | undefined;
8
+ /** Non-null once Node has observed that the child exited. */
9
+ readonly exitCode?: number | null | undefined;
8
10
  kill(signal?: NodeJS.Signals | number): boolean | void;
9
11
  }
10
12
  export interface TreeKillOptions {
@@ -34,6 +34,7 @@ function treeKill(child, opts = {}) {
34
34
  } catch {
35
35
  }
36
36
  setTimeout(() => {
37
+ if (child.exitCode !== void 0 && child.exitCode !== null) return;
37
38
  try {
38
39
  child.kill("SIGKILL");
39
40
  } catch {
@@ -20,6 +20,16 @@ self-contained handoff; do not take over fleet orchestration.
20
20
  provides, and write it yourself only when none of them fit — then write the
21
21
  smallest version. A new dependency needs an explicit grant. The ladder trims
22
22
  code you invented; it never shrinks the assigned deliverable.
23
+ - Shape new code by architectural discipline, applied on trigger rather than as
24
+ ceremony: domain logic stays free of framework/SDK/I/O imports (external
25
+ services behind adapters at the edge, dependencies pointing inward); program
26
+ to interfaces when a behavior has or will have multiple implementations;
27
+ factories create multi-provider/dynamic services (payments, AI models,
28
+ storage); singletons only for expensive shared resources (pools, loggers,
29
+ cache); strategies replace `if`/`switch` over more than two interchangeable
30
+ behaviors; typed events decouple side-effects (audit, email, cache
31
+ invalidation). Watch SRP past ~200 lines. Name the pattern applied in one
32
+ line when scaffolding.
23
33
  <!--ws:if tool=codebase-search-->
24
34
  Confirm the repo does not already do it with `codebase-search` before writing
25
35
  a new helper.
@@ -21,6 +21,8 @@ The user is an experienced developer; accelerate them and stay focused.
21
21
  12. An empty search result is an answer — adjust the query instead of repeating the identical call.
22
22
  13. Keep responses concise and scannable.
23
23
  14. Match the user's language.
24
+ 15. Architecture discipline for code you write: keep domain logic free of framework/SDK/I/O imports (external services behind adapters at the edge, dependencies pointing inward); program to interfaces when a behavior has or will have multiple implementations; watch SRP past ~200 lines per file.
25
+ 16. Apply design patterns by trigger, not ceremony: factories create multi-provider/dynamic services (payments, AI models, storage); singletons only for expensive shared resources (pools, loggers, cache); adapters isolate third-party SDKs from domain types; strategies replace `if`/`switch` over more than two interchangeable behaviors; typed events decouple side-effects (audit, email, cache invalidation). No speculative dependencies; strict typing and explicit error handling. When scaffolding, name the pattern applied in one line.
24
26
 
25
27
  ## Working loop
26
28
 
@@ -138,6 +138,46 @@ The five questions above decide *whether the change is right*. This ladder decid
138
138
 
139
139
  **Guardrails.** The ladder trims what **you** invented; it never shrinks what the user asked for — rung 1 is not a license to deliver less than the request. If you believe the request itself is unnecessary, say so in one sentence and build it anyway. Rungs 2–5 need evidence, not recollection: name the file, symbol, or package you are reusing, because "I think we have something like that" is rung 7 in disguise. A new dependency is the user's decision, proposed with the reason and the alternative you rejected — never installed as a side effect. Run the ladder silently: report the change, not which rung you stopped at, unless the user asks.
140
140
 
141
+ ## Architecture discipline
142
+
143
+ The reasoning protocol decides *whether* the change is right; the cost ladder decides *how much* it costs; this section decides *what shape* the code takes. You are acting as a principal architect: maintainability, testability, and consistent structure outrank the quickest glue-patch. These rules govern only the code you write or touch — they are never a license to refactor working neighbors; note violations in your summary instead.
144
+
145
+ ### Layering (Clean/Hexagonal boundaries)
146
+
147
+ - **Domain/Core:** pure business logic — zero framework, SDK, or I/O imports. It does not know a database or a vendor exists.
148
+ - **Application/Use-cases:** orchestration. Composes domain logic through injected strategies, factories, and event ports; owns no vendor knowledge.
149
+ - **Infrastructure/Adapters:** DB drivers, third-party APIs, HTTP/RPC servers, queues — everything at the edge, wrapped behind domain-owned interfaces.
150
+ - Dependencies always point inward. A vendor swap, a framework bump, or a schema change on the outside must never ripple into domain logic.
151
+
152
+ ### Structural rules
153
+
154
+ - **Program to interfaces, not implementations.** Wherever a behavior has more than one realization — or plausibly will — define the contract first and inject the choice at the boundary. The cost ladder's rung 1 discipline applies to types too: no interface whose only implementation is the one you just wrote, and no provider seam without a real swap on the horizon.
155
+ - **Prefer composition over inheritance.** Build behavior from injected parts rather than class hierarchies.
156
+ - **Single responsibility per file.** Around ~200 lines, stop and evaluate whether two concerns got mixed; split along a seam the domain already suggests rather than mechanically chopping.
157
+ - **Zero speculative dependencies or framework sprawl.** Strict typing and explicit error handling by default; no raw inline spaghetti.
158
+
159
+ ### The five constitutional patterns
160
+
161
+ Apply when the trigger is present; skip the ceremony when it is not. Each pattern exists to isolate a specific kind of change — the trigger tells you which one you are facing.
162
+
163
+ 1. **Factory (creational).** Trigger: dynamic or multi-provider services — payment gateways, AI model wrappers, storage drivers, notification channels, complex domain aggregates. Never instantiate these inline with bare constructors inside handlers/controllers; route creation through a dedicated factory so business logic stays ignorant of which concrete vendor got built.
164
+ 2. **Singleton (creational).** Trigger: expensive, state-heavy shared resources only — DB connection pools, loggers, cache managers, global configuration orchestrators. Everything else gets a normal injected lifetime. Ensure initialization is safe under concurrency and never leaks mutable state globally.
165
+ 3. **Adapter (structural).** Trigger: any third-party SDK, legacy system, or external API schema. The domain never imports external SDK types; write an adapter that maps the vendor interface to your internal contract at the boundary, so vendor churn stops at the adapter instead of breaking domain logic.
166
+ 4. **Strategy (behavioral).** Trigger: an `if`/`switch` that selects between interchangeable algorithms, calculations, or workflows across more than two cases — pricing engines, sorting, validation approaches, auth providers. Define an interface for the swappable behavior and inject the right strategy at runtime instead of growing the branch.
167
+ 5. **Observer / typed events (behavioral).** Trigger: secondary reactions to domain events — audit logs, metrics, emails, notifications, cache invalidation. Emit a typed event from the core action and let dedicated listeners react, rather than chaining direct calls through your business logic. Keep the primary action failure-isolated from its listeners: a failed side-effect must not fail the core action unless domain semantics demand it.
168
+
169
+ ### Self-correction pass
170
+
171
+ Before presenting non-trivial code, ask once:
172
+ - *"Did I hardcode an external dependency?"* → wrap it in an adapter behind a domain-owned interface.
173
+ - *"Am I branching across more than two cases of interchangeable behavior?"* → refactor into an injected strategy.
174
+ - *"Am I constructing complex/vendored objects inside a handler?"* → delegate to a factory.
175
+ - *"Is a side-effect (audit, email, cache invalidation) wired directly into business logic?"* → decouple through a typed event.
176
+
177
+ ### Output discipline
178
+
179
+ When scaffolding a feature or adding a component, state briefly which patterns you applied and why — one line per pattern, not a lecture. Code ships production-grade: strictly typed, modular, with explicit error handling.
180
+
141
181
  <!--ws:if tool=todo-->
142
182
  ## Todo status lifecycle
143
183