@wrongstack/core 0.309.0 → 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 (129) 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/director.d.ts +7 -0
  13. package/dist/coordination/explore-companion.d.ts +9 -6
  14. package/dist/coordination/fleet-status-tool.d.ts +1 -1
  15. package/dist/coordination/index.d.ts +5 -1
  16. package/dist/coordination/index.js +2392 -405
  17. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  18. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  19. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  20. package/dist/coordination/mailbox-project-server.js +14 -9
  21. package/dist/coordination/mutation-engine.d.ts +5 -3
  22. package/dist/core/context.d.ts +35 -44
  23. package/dist/core/conversation-state.d.ts +16 -52
  24. package/dist/core/index.js +92 -25
  25. package/dist/core/provider-runner.d.ts +2 -2
  26. package/dist/core/run-env.d.ts +7 -28
  27. package/dist/core/streaming-response-builder.d.ts +2 -2
  28. package/dist/execution/index.js +138 -162
  29. package/dist/goal/index.js +100 -22
  30. package/dist/hq/auth-store.d.ts +9 -0
  31. package/dist/hq/cost-bridge.d.ts +1 -1
  32. package/dist/hq/index.js +69 -7
  33. package/dist/index.d.ts +12 -5
  34. package/dist/index.js +21185 -26807
  35. package/dist/infrastructure/index.js +232 -135
  36. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  37. package/dist/infrastructure/token-counter.d.ts +5 -1
  38. package/dist/kernel/events/file-events.d.ts +6 -0
  39. package/dist/kernel/events/provider-events.d.ts +10 -7
  40. package/dist/kernel/events/session-events.d.ts +4 -4
  41. package/dist/kernel/events/tool-events.d.ts +12 -2
  42. package/dist/observability/index.js +1 -1
  43. package/dist/plugin/index.js +218 -47
  44. package/dist/prompts/index.js +360 -3
  45. package/dist/security/auto-approve-policy.d.ts +2 -2
  46. package/dist/security/index.d.ts +1 -0
  47. package/dist/security/index.js +224 -68
  48. package/dist/security/kanban-boundary.d.ts +1 -1
  49. package/dist/security/kanban-governance-port.d.ts +28 -0
  50. package/dist/security/permission-helpers.d.ts +11 -0
  51. package/dist/security/permission-policy.d.ts +10 -1
  52. package/dist/security/yolo-risk.d.ts +17 -0
  53. package/dist/session-catalog/index.js +34 -5
  54. package/dist/session-catalog/project-server.js +34 -5
  55. package/dist/session-catalog/protocol.d.ts +1 -1
  56. package/dist/session-catalog/registry.d.ts +1 -1
  57. package/dist/session-catalog/store-schema.d.ts +1 -1
  58. package/dist/session-catalog/store.d.ts +1 -1
  59. package/dist/skills/index.js +39 -6
  60. package/dist/storage/annotations-store.d.ts +1 -1
  61. package/dist/storage/board-store-port.d.ts +45 -0
  62. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  63. package/dist/storage/config-loader/types.d.ts +1 -1
  64. package/dist/storage/event-bus-port.d.ts +27 -0
  65. package/dist/storage/file-session-writer.d.ts +47 -5
  66. package/dist/storage/goal-coordination.d.ts +1 -1
  67. package/dist/storage/goal-store.d.ts +1 -1
  68. package/dist/storage/index.d.ts +3 -4
  69. package/dist/storage/index.js +1107 -1462
  70. package/dist/storage/plan-store.d.ts +1 -1
  71. package/dist/storage/queue-store.d.ts +1 -1
  72. package/dist/storage/replay-log-store.d.ts +1 -1
  73. package/dist/storage/session-recovery.d.ts +10 -0
  74. package/dist/storage/session-resume-validation.d.ts +13 -1
  75. package/dist/storage/session-store/events.d.ts +1 -1
  76. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  77. package/dist/storage/session-store/rename-session.d.ts +1 -1
  78. package/dist/storage/session-store/resume-session.d.ts +3 -1
  79. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  80. package/dist/storage/session-store/types.d.ts +1 -1
  81. package/dist/storage/session-store.d.ts +70 -0
  82. package/dist/storage/session-summary-tracker.d.ts +8 -0
  83. package/dist/storage/session-write-buffer.d.ts +31 -2
  84. package/dist/storage/task-store.d.ts +1 -1
  85. package/dist/storage/todos-checkpoint.d.ts +1 -1
  86. package/dist/storage/tool-audit-log.d.ts +1 -1
  87. package/dist/tasking/index.js +100 -22
  88. package/dist/tasking/task-tracker.d.ts +24 -1
  89. package/dist/types/compactor.d.ts +2 -2
  90. package/dist/types/context.d.ts +212 -0
  91. package/dist/types/conversation-state.d.ts +109 -0
  92. package/dist/types/error-handler.d.ts +2 -2
  93. package/dist/types/file-event-record.d.ts +6 -0
  94. package/dist/types/index.d.ts +3 -3
  95. package/dist/types/index.js +17 -1
  96. package/dist/types/permission.d.ts +3 -3
  97. package/dist/types/plugin.d.ts +3 -3
  98. package/dist/types/provider-runner.d.ts +2 -2
  99. package/dist/types/provider.d.ts +10 -0
  100. package/dist/types/run-env.d.ts +32 -0
  101. package/dist/types/session.d.ts +3 -0
  102. package/dist/types/slash-command.d.ts +2 -2
  103. package/dist/types/token-counter.d.ts +30 -1
  104. package/dist/types/tool-executor.d.ts +2 -2
  105. package/dist/types/tool.d.ts +20 -5
  106. package/dist/utils/context-breakdown.d.ts +2 -2
  107. package/dist/utils/context-evidence.d.ts +12 -12
  108. package/dist/utils/crash-shield.d.ts +9 -0
  109. package/dist/utils/heap-watchdog.js +11 -3
  110. package/dist/utils/index.d.ts +2 -1
  111. package/dist/utils/index.js +126 -160
  112. package/dist/utils/regex-guard.d.ts +7 -30
  113. package/dist/utils/terminal-sanitize.d.ts +41 -0
  114. package/dist/utils/todos-format.d.ts +1 -1
  115. package/dist/utils/tool-subject.d.ts +1 -1
  116. package/dist/utils/tree-kill.d.ts +2 -0
  117. package/dist/utils/tree-kill.js +1 -0
  118. package/instructions/agents/chaos-monkey.md +5 -1
  119. package/instructions/coordination/subagent-baseline.md +10 -0
  120. package/instructions/system-lite.md +2 -0
  121. package/instructions/system-pro.md +40 -0
  122. package/instructions/system.md +15 -0
  123. package/package.json +7 -10
  124. package/dist/defaults/index.d.ts +0 -69
  125. package/dist/defaults/index.js +0 -37755
  126. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  127. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  128. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  129. /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
@@ -101,6 +101,21 @@ export interface Tool<I = unknown, O = unknown> {
101
101
  * fall back to the heuristic.
102
102
  */
103
103
  subjectKey?: string | undefined;
104
+ /**
105
+ * Extra input fields folded into the permission subject, after `subjectKey`.
106
+ *
107
+ * For a shell-style tool the subject is the whole command line, so the trust
108
+ * rule is as specific as the invocation. A tool whose parameters are NAMED
109
+ * FIELDS rather than an argv array has no such luck: `git` sets
110
+ * `subjectKey: 'command'`, whose value is an enum subcommand, so every
111
+ * `git push` — any branch, `force` or not — rendered to the subject `"push"`
112
+ * and one "always allow" covered them all (audit 2026-08-20).
113
+ *
114
+ * List the fields that change what the call actually does. Absent fields are
115
+ * skipped, so adding a field only narrows existing rules (they degrade to a
116
+ * confirm prompt) and never silently widens one.
117
+ */
118
+ subjectFields?: readonly string[] | undefined;
104
119
  maxOutputBytes?: number | undefined;
105
120
  timeoutMs?: number | undefined;
106
121
  /**
@@ -135,7 +150,7 @@ export interface Tool<I = unknown, O = unknown> {
135
150
  * Each UI maps this id to its own icon library.
136
151
  */
137
152
  icon?: ToolIconId | undefined;
138
- execute(input: I, ctx: Context, opts: {
153
+ execute(input: I, ctx: AgentContext, opts: {
139
154
  signal: AbortSignal;
140
155
  }): Promise<O>;
141
156
  /**
@@ -161,7 +176,7 @@ export interface Tool<I = unknown, O = unknown> {
161
176
  * have intermediate state shouldn't implement this; the default `execute`
162
177
  * path is more efficient.
163
178
  */
164
- executeStream?(input: I, ctx: Context, opts: {
179
+ executeStream?(input: I, ctx: AgentContext, opts: {
165
180
  signal: AbortSignal;
166
181
  }): AsyncIterable<ToolStreamEvent<O>>;
167
182
  /**
@@ -207,7 +222,7 @@ export interface Tool<I = unknown, O = unknown> {
207
222
  * Avoid double-free by gating one on the other's effect, or pick a single
208
223
  * teardown channel per resource.
209
224
  */
210
- cleanup?(input: I, ctx: Context): Promise<void>;
225
+ cleanup?(input: I, ctx: AgentContext): Promise<void>;
211
226
  /**
212
227
  * Optional custom output serializer. When present, the executor's output
213
228
  * serializer calls this INSTEAD of the central `renderToolObject()` switch
@@ -228,7 +243,7 @@ export interface ToolCallContext {
228
243
  tool: Tool;
229
244
  input: unknown;
230
245
  callId: string;
231
- ctx: Context;
246
+ ctx: AgentContext;
232
247
  signal: AbortSignal;
233
248
  }
234
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';
@@ -44,6 +44,7 @@ export { withSqliteExperimentalWarningSuppressed } from './sqlite-warning.js';
44
44
  export * from './string.js';
45
45
  export * from './task-format.js';
46
46
  export { buildSgrSequence, buildTitleSequence, type ColorDepth, detectTerminal, ESCAPE_TERMINATOR, type EscapeEmitResult, type EscapeSequence, isStdinTTY, type MouseProtocol, onResize, safeEmit, setOutputLineGuard, setRawMode, setTitle, type TerminalCapability, TerminalLifecycle, writeErr, writeOut, } from './term.js';
47
+ export { sanitizeTerminalPreview, sanitizeTerminalText } from './terminal-sanitize.js';
47
48
  export * from './todos-format.js';
48
49
  export { computeMessageTokens, estimateMessageTokens, estimateRequestTokens, estimateRequestTokensCalibrated, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, getCalibrationState, type RequestTokenBreakdown, recordActualUsage, resetCalibration, } from './token-estimate.js';
49
50
  export { applyToolDescriptionModes, applyToolDescriptionModeToTool, DEFAULT_TOOL_DESCRIPTION_MODE, getToolDescriptionMode, normalizeToolDescriptionMode, resolveToolDescriptionMode, setToolDescriptionMode, simplifyToolDescription, type ToolDescriptionRegistryLike, } from './tool-description-mode.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) {
@@ -4710,6 +4624,42 @@ function formatTaskList(tasks) {
4710
4624
  return lines.join("\n");
4711
4625
  }
4712
4626
 
4627
+ // src/utils/terminal-sanitize.ts
4628
+ var ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
4629
+ var ANSI_OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
4630
+ var ANSI_CONTROL_STRING_RE = /\x1b[P^_X][\s\S]*?\x1b\\/g;
4631
+ var ANSI_ESCAPE_RE = /\x1b[ -/]*[@-~]/g;
4632
+ var BIDI_AND_ZERO_WIDTH_RE = /[​-‏‪-‮⁦-⁩]/g;
4633
+ function sanitizeTerminalText(value, tabWidth = 2) {
4634
+ const tab = " ".repeat(Math.max(1, Math.min(8, Math.floor(tabWidth))));
4635
+ const withoutEscapes = value.replace(ANSI_OSC_RE, "").replace(ANSI_CONTROL_STRING_RE, "").replace(ANSI_RE, "").replace(ANSI_ESCAPE_RE, "").replace(BIDI_AND_ZERO_WIDTH_RE, "").replace(/\t/g, tab).replace(/\r/g, "");
4636
+ let safe = "";
4637
+ for (const char of withoutEscapes) {
4638
+ const code = char.codePointAt(0) ?? 0;
4639
+ if (char === "\n" || code >= 32 && code !== 127 && !(code >= 128 && code <= 159)) {
4640
+ safe += char;
4641
+ }
4642
+ }
4643
+ return safe;
4644
+ }
4645
+ function sanitizeTerminalPreview(value, opts = {}) {
4646
+ const maxLines = opts.maxLines ?? 40;
4647
+ const maxChars = opts.maxChars ?? 8e3;
4648
+ const safe = sanitizeTerminalText(value, opts.tabWidth);
4649
+ let truncated = false;
4650
+ let clipped = safe;
4651
+ if (clipped.length > maxChars) {
4652
+ clipped = clipped.slice(0, maxChars);
4653
+ truncated = true;
4654
+ }
4655
+ const lines = clipped.split("\n");
4656
+ if (lines.length > maxLines) {
4657
+ clipped = lines.slice(0, maxLines).join("\n");
4658
+ truncated = true;
4659
+ }
4660
+ return { text: clipped, truncated };
4661
+ }
4662
+
4713
4663
  // src/utils/tool-description-mode.ts
4714
4664
  var DEFAULT_TOOL_DESCRIPTION_MODE = "extend";
4715
4665
  var ORIGINAL_TOOL_DESCRIPTION = /* @__PURE__ */ Symbol.for("wrongstack.tool.originalDescription");
@@ -5589,9 +5539,21 @@ function renderCommandLine(command, args) {
5589
5539
  });
5590
5540
  return [command, ...rendered].join(" ");
5591
5541
  }
5592
- function subjectForToolInput(toolName, input, subjectKey) {
5542
+ function renderSubjectFields(obj, fields) {
5543
+ const parts = [];
5544
+ for (const field of fields) {
5545
+ const value = obj[field];
5546
+ if (value === void 0 || value === null || value === "" || value === false) continue;
5547
+ const str = String(value);
5548
+ parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
5549
+ }
5550
+ return parts.join(" ");
5551
+ }
5552
+ function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
5593
5553
  if (!input || typeof input !== "object") return void 0;
5594
5554
  const obj = input;
5555
+ const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
5556
+ const withExtra = (base) => extra ? `${base} ${extra}` : base;
5595
5557
  if (subjectKey) {
5596
5558
  const value = obj[subjectKey];
5597
5559
  if (Array.isArray(value)) {
@@ -5607,9 +5569,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
5607
5569
  if (subjectKey === "command") {
5608
5570
  const rendered = renderCommandLine(value, obj["args"]);
5609
5571
  if (value === "commit" && obj["dry_run"] === true) {
5610
- return `${escapeGlobSubject(rendered)}:dry-run`;
5572
+ return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
5611
5573
  }
5612
- return escapeGlobSubject(rendered);
5574
+ return escapeGlobSubject(withExtra(rendered));
5613
5575
  }
5614
5576
  if (subjectKey === "directory" && obj["dry_run"] === true) {
5615
5577
  return `${escapeGlobSubject(value)}:dry-run`;
@@ -5662,7 +5624,7 @@ var DEFAULT_WALK_IGNORE_DIRS = Object.freeze([
5662
5624
  var DEFAULT_WALK_IGNORE_SET = new Set(DEFAULT_WALK_IGNORE_DIRS);
5663
5625
 
5664
5626
  // src/utils/win32-cmd.ts
5665
- var WIN32_CMD_META = /[&|<>"\r\n\0]/;
5627
+ var WIN32_CMD_META = /[&|<>"%\r\n\0]/;
5666
5628
  function buildWin32CmdShimInvocation(command, args = []) {
5667
5629
  assertSafeWin32CmdArgs([command, ...args]);
5668
5630
  const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
@@ -5751,6 +5713,7 @@ export {
5751
5713
  WIRE_TOOL_NAME_MAX_LENGTH,
5752
5714
  WIRE_TOOL_NAME_PATTERN,
5753
5715
  activateProjectStateGuard,
5716
+ addFatalSalvageHook,
5754
5717
  applyToolDescriptionModeToTool,
5755
5718
  applyToolDescriptionModes,
5756
5719
  applyToolResultRenderModes,
@@ -5885,6 +5848,7 @@ export {
5885
5848
  resolveToolDescriptionMode,
5886
5849
  resolveToolResultRenderMode,
5887
5850
  resolveWstackPaths,
5851
+ runFatalSalvageSync,
5888
5852
  safeEmit,
5889
5853
  safeParse,
5890
5854
  safeProfileName,
@@ -5893,6 +5857,8 @@ export {
5893
5857
  sanitizeMemoryEvidenceBody,
5894
5858
  sanitizeMemoryEvidenceSource,
5895
5859
  sanitizeNodeOptions,
5860
+ sanitizeTerminalPreview,
5861
+ sanitizeTerminalText,
5896
5862
  sanitizeWireToolName,
5897
5863
  sessionScopedPath,
5898
5864
  setJsonPath,
@@ -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