@wrongstack/core 0.308.7 → 0.309.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 (47) hide show
  1. package/dist/coordination/director/director-toolset.d.ts +2 -2
  2. package/dist/coordination/director-mutation-test-tool.d.ts +29 -0
  3. package/dist/coordination/director-tools.d.ts +2 -0
  4. package/dist/coordination/director.d.ts +16 -0
  5. package/dist/coordination/explore-companion.d.ts +9 -6
  6. package/dist/coordination/fleet.d.ts +12 -0
  7. package/dist/coordination/index.d.ts +1 -1
  8. package/dist/coordination/index.js +990 -61
  9. package/dist/coordination/mail-tools.d.ts +10 -6
  10. package/dist/coordination/mailbox-codecs.d.ts +31 -0
  11. package/dist/coordination/multi-agent-coordinator.d.ts +14 -0
  12. package/dist/coordination/multi-agent-timeout.d.ts +11 -1
  13. package/dist/coordination/mutation-engine.d.ts +76 -0
  14. package/dist/coordination/subagent-budget.d.ts +54 -0
  15. package/dist/coordination/subagent-finish.d.ts +78 -0
  16. package/dist/core/index.js +58 -13
  17. package/dist/defaults/index.js +1132 -106
  18. package/dist/execution/index.js +260 -20
  19. package/dist/hq/index.js +45 -5
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1456 -263
  22. package/dist/infrastructure/index.js +22 -3
  23. package/dist/kernel/events/agent-events.d.ts +31 -2
  24. package/dist/models/index.js +11 -1
  25. package/dist/observability/index.js +1 -1
  26. package/dist/plugin/index.js +113 -12
  27. package/dist/prompts/index.js +360 -3
  28. package/dist/security/auto-approve-policy.d.ts +2 -2
  29. package/dist/security/index.js +177 -50
  30. package/dist/security/permission-helpers.d.ts +11 -0
  31. package/dist/security/permission-policy.d.ts +10 -1
  32. package/dist/security/yolo-risk.d.ts +17 -0
  33. package/dist/session-catalog/index.js +32 -2
  34. package/dist/session-catalog/project-server.js +32 -2
  35. package/dist/skills/index.js +39 -6
  36. package/dist/storage/index.js +44 -3
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/index.js +14 -0
  39. package/dist/types/multi-agent.d.ts +15 -0
  40. package/dist/types/provider.d.ts +29 -1
  41. package/dist/types/tool.d.ts +15 -0
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.js +54 -4
  44. package/dist/utils/terminal-sanitize.d.ts +41 -0
  45. package/dist/utils/tool-subject.d.ts +1 -1
  46. package/instructions/agents/chaos-monkey.md +61 -0
  47. package/package.json +4 -4
@@ -888,6 +888,32 @@ function numField(rec, key) {
888
888
  import { spawn } from "node:child_process";
889
889
  import * as fs3 from "node:fs/promises";
890
890
  import * as path3 from "node:path";
891
+
892
+ // src/utils/win32-cmd.ts
893
+ var WIN32_CMD_META = /[&|<>"%\r\n\0]/;
894
+ function buildWin32CmdShimInvocation(command, args = []) {
895
+ assertSafeWin32CmdArgs([command, ...args]);
896
+ const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
897
+ return {
898
+ command: process.env["COMSPEC"] ?? "cmd.exe",
899
+ args: ["/d", "/c", line],
900
+ windowsVerbatimArguments: true
901
+ };
902
+ }
903
+ function assertSafeWin32CmdArgs(args) {
904
+ for (const arg of args) {
905
+ if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
906
+ throw new Error(
907
+ 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
908
+ );
909
+ }
910
+ }
911
+ }
912
+ function quoteWin32CmdArg(arg) {
913
+ return `"${arg}"`;
914
+ }
915
+
916
+ // src/skills/skill-generator.ts
891
917
  async function validateSkillNameAvailable(name, loader) {
892
918
  const formatViolations = validateSkillName(name);
893
919
  const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
@@ -989,14 +1015,21 @@ async function openInEditor(filePath, env = process.env) {
989
1015
  context: { filePath }
990
1016
  });
991
1017
  }
992
- const child = spawn(parts[0], [...parts.slice(1), filePath], {
1018
+ const editorArgs = [...parts.slice(1), filePath];
1019
+ const child = shell ? (() => {
1020
+ const inv = buildWin32CmdShimInvocation(parts[0], editorArgs);
1021
+ return spawn(inv.command, inv.args, {
1022
+ stdio: "ignore",
1023
+ detached: true,
1024
+ windowsVerbatimArguments: inv.windowsVerbatimArguments,
1025
+ // Suppresses the console flash the cmd.exe wrapper would otherwise
1026
+ // show before the editor appears. Repo convention — see
1027
+ // `core/tests/architecture/spawn-convention.test.ts`.
1028
+ windowsHide: true
1029
+ });
1030
+ })() : spawn(parts[0], editorArgs, {
993
1031
  stdio: "ignore",
994
1032
  detached: true,
995
- shell,
996
- // `shell` routes through cmd.exe on win32, which flashes a console window
997
- // before the editor appears. A GUI editor is unaffected by the flag; the
998
- // shell wrapper is what it suppresses. Repo convention — see
999
- // `core/tests/architecture/spawn-convention.test.ts`.
1000
1033
  windowsHide: true
1001
1034
  });
1002
1035
  child.unref();
@@ -1738,6 +1738,14 @@ var PATTERNS = [
1738
1738
  anchor: "sk-ant-"
1739
1739
  },
1740
1740
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
1741
+ {
1742
+ // `xai` is a first-class provider in this codebase, but its key shape was
1743
+ // absent here — so the one credential format WrongStack itself hands users
1744
+ // was the one the scrubber could not recognize (audit 2026-08-20).
1745
+ type: "xai_key",
1746
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
1747
+ anchor: "xai-"
1748
+ },
1741
1749
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
1742
1750
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
1743
1751
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -1828,8 +1836,8 @@ var PATTERNS = [
1828
1836
  // replacement so the separator between adjacent secrets is preserved
1829
1837
  // rather than collapsed. Capture groups are therefore: 1=leading
1830
1838
  // delimiter, 2=key name, 3=value.
1831
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1832
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
1839
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1840
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
1833
1841
  },
1834
1842
  {
1835
1843
  type: "json_credential_key",
@@ -1946,6 +1954,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
1946
1954
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1947
1955
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1948
1956
  var SCRUB_OVERLAP_BYTES = 1024;
1957
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
1958
+ var PEM_END_MARKER = "-----END";
1959
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
1960
+ var PEM_END_LINE_TOLERANCE = 64;
1961
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
1962
+ const head = text.slice(chunkStart, proposedEnd);
1963
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
1964
+ if (lastBegin === -1) return proposedEnd;
1965
+ const fromBegin = text.slice(chunkStart + lastBegin);
1966
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
1967
+ if (!marker || marker.index !== 0) return proposedEnd;
1968
+ const bodyStart = marker[0].length;
1969
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
1970
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
1971
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
1972
+ return proposedEnd;
1973
+ }
1974
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
1975
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
1976
+ return Math.max(proposedEnd, end);
1977
+ }
1949
1978
  var PATTERN_ANCHORS = [
1950
1979
  ...new Set(
1951
1980
  PATTERNS.flatMap(
@@ -1982,6 +2011,7 @@ var DefaultSecretScrubber = class {
1982
2011
  }
1983
2012
  }
1984
2013
  end = safe === -1 ? end : safe + 1;
2014
+ end = extendChunkBoundaryPastPem(text, i, end);
1985
2015
  }
1986
2016
  out.push(this.scrubOne(text.slice(i, end)));
1987
2017
  i = end;
@@ -4639,7 +4669,7 @@ function walk(node, vault, transform) {
4639
4669
  }
4640
4670
  return out;
4641
4671
  }
4642
- var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key|token\b)/i;
4672
+ var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
4643
4673
  var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
4644
4674
  function isSecretField(name) {
4645
4675
  const lc = name.toLowerCase();
@@ -5344,6 +5374,17 @@ var IN_PROJECT_DENIED_PATHS = [
5344
5374
  // See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
5345
5375
  path: "features.mailboxBridge",
5346
5376
  reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
5377
+ },
5378
+ {
5379
+ // `plugins` is already denied above, so a repo cannot ADD a plugin. This
5380
+ // closes the other half: a repo could previously ship
5381
+ // `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
5382
+ // for plugins the user had ALREADY installed globally — disarming the
5383
+ // trust-on-first-use pin that exists to catch a supply-chain update
5384
+ // rewriting a plugin's entry file. Same operator-owned class as the
5385
+ // switches above.
5386
+ path: "features.pluginsTrust",
5387
+ reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
5347
5388
  }
5348
5389
  ];
5349
5390
  function deleteNestedPath(target, path36) {
@@ -34,7 +34,7 @@ export { BUILTIN_PROMPT_CATEGORIES, isBuiltinCategory, PROMPT_CATEGORY_LABELS }
34
34
  export type { InstalledPromptEntry, ManifestValidation, PromptManifestData, PromptRegistryManifest, PromptRegistryRef, RegistryDiff, } from './prompt-registry.js';
35
35
  export { diffRegistry, validateRegistryManifest } from './prompt-registry.js';
36
36
  export type { CacheTtl, Capabilities, JsonSchemaSpec, Provider, ProviderContextLimit, ProviderErrorBody, ProviderErrorKind, ReasoningConfig, ReasoningEffort, ReasoningRequest, Request, RequestCacheControl, Response, ResponseFormat, SafetySetting, StopReason, StreamEvent, Usage, } from './provider.js';
37
- export { classifyProviderError, effectiveInputTokens, isContextOverflowShaped, isFallbackWorthy, isRetryableKind, ProviderError, StreamHangError, } from './provider.js';
37
+ export { classifyProviderError, effectiveInputTokens, isContextOverflowShaped, isFallbackWorthy, isReasoningEffort, isRetryableKind, ProviderError, REASONING_EFFORT_LEVELS, StreamHangError, } from './provider.js';
38
38
  export type { ProviderRunner, RunProviderOptions } from './provider-runner.js';
39
39
  export type { Renderer } from './renderer.js';
40
40
  export type { SecretScrubber } from './secret-scrubber.js';
@@ -914,6 +914,18 @@ function truncate(s, max) {
914
914
  var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
915
915
 
916
916
  // src/types/provider.ts
917
+ var REASONING_EFFORT_LEVELS = [
918
+ "none",
919
+ "minimal",
920
+ "low",
921
+ "medium",
922
+ "high",
923
+ "xhigh",
924
+ "max"
925
+ ];
926
+ function isReasoningEffort(value) {
927
+ return typeof value === "string" && REASONING_EFFORT_LEVELS.includes(value);
928
+ }
917
929
  function effectiveInputTokens(usage) {
918
930
  return usage.input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
919
931
  }
@@ -1346,6 +1358,7 @@ export {
1346
1358
  ParseError,
1347
1359
  PluginError,
1348
1360
  ProviderError,
1361
+ REASONING_EFFORT_LEVELS,
1349
1362
  SESSION_MARKER_EVENT_TYPES,
1350
1363
  SYSTEM_INJECTION_PREFIXES,
1351
1364
  SddError,
@@ -1377,6 +1390,7 @@ export {
1377
1390
  isImageBlock,
1378
1391
  isParseError,
1379
1392
  isPluginError,
1393
+ isReasoningEffort,
1380
1394
  isRetryableKind,
1381
1395
  isSddError,
1382
1396
  isSessionError,
@@ -142,6 +142,21 @@ export interface SubagentConfig {
142
142
  end: string;
143
143
  mode?: 'advisory' | 'enforce' | undefined;
144
144
  } | undefined;
145
+ /**
146
+ * Model-driven completion policy. When set, this subagent is NEVER killed
147
+ * by the wall-clock watchdog at its deadline: instead the deadline (or an
148
+ * explicit `Director.requestFinish()`) triggers an in-band
149
+ * `subagent.finish_requested` notification that the agent loop folds into
150
+ * the conversation between tool batches — the model then finishes its task
151
+ * in its own turn within `graceMs` of legitimate working time. Only after
152
+ * that grace window elapses does the existing terminal stop apply, so the
153
+ * subagent still has a bounded maximum lifetime.
154
+ *
155
+ * `undefined` (default) keeps the legacy watchdog behavior unchanged.
156
+ */
157
+ gracefulFinish?: boolean | {
158
+ graceMs?: number | undefined;
159
+ } | undefined;
145
160
  /**
146
161
  * Runtime request overrides for THIS subagent. When present, these are merged
147
162
  * over the leader's `Config.modelRuntime` before the subagent request pipeline
@@ -23,6 +23,21 @@ import type { Tool } from './tool.js';
23
23
  * cached tokens twice and skew cache-hit-ratio reporting.
24
24
  */
25
25
  export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
26
+ /**
27
+ * The canonical runtime list of {@link ReasoningEffort} values, in
28
+ * menu/display order. Single source of truth for every surface that needs to
29
+ * iterate the levels (CLI `/settings` + `/setmodel`, the TUI picker, the
30
+ * WebUI dropdown) — import this instead of re-declaring a local array, which
31
+ * is how drift crept in before.
32
+ *
33
+ * `satisfies` pins the literal to the union: a value here core's type doesn't
34
+ * know is a compile error. Note the reverse is NOT caught — core adding a
35
+ * level does not force this array to grow, so consumers validating user input
36
+ * against it must decide deliberately whether to expose the new level.
37
+ */
38
+ export declare const REASONING_EFFORT_LEVELS: readonly ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
39
+ /** Type guard for untrusted strings (CLI args, WS payloads, config files). */
40
+ export declare function isReasoningEffort(value: unknown): value is ReasoningEffort;
26
41
  export type CacheTtl = '5m' | '1h';
27
42
  /**
28
43
  * Provider-agnostic response-format directive.
@@ -119,7 +134,20 @@ export interface RequestCacheControl {
119
134
  export interface ReasoningConfig {
120
135
  default: 'enabled' | 'disabled' | 'adaptive' | 'always_on';
121
136
  disableSupported: boolean;
122
- effortSupported: boolean;
137
+ /**
138
+ * Tri-state effort support:
139
+ * `true` — the catalog documents this model's effort levels
140
+ * (`effortLevels` is authoritative).
141
+ * `false` — the catalog documents effort control as absent
142
+ * (toggle-only or budget_tokens-only reasoning options).
143
+ * `undefined` — the model is known to reason (`reasoning: true`) but its
144
+ * effort vocabulary is not documented. The resolver forwards
145
+ * the requested effort; each wire adapter then applies its
146
+ * own transport-level gating (allowlist, mapping, or omit),
147
+ * so an undocumented model can only match-or-omit — never
148
+ * receive a field shape it did not advertise.
149
+ */
150
+ effortSupported?: boolean | undefined;
123
151
  effortLevels: ReasoningEffort[];
124
152
  preserveThinking: 'unsupported' | 'optional' | 'always_on';
125
153
  }
@@ -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
  /**
@@ -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';
@@ -4710,6 +4710,42 @@ function formatTaskList(tasks) {
4710
4710
  return lines.join("\n");
4711
4711
  }
4712
4712
 
4713
+ // src/utils/terminal-sanitize.ts
4714
+ var ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
4715
+ var ANSI_OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
4716
+ var ANSI_CONTROL_STRING_RE = /\x1b[P^_X][\s\S]*?\x1b\\/g;
4717
+ var ANSI_ESCAPE_RE = /\x1b[ -/]*[@-~]/g;
4718
+ var BIDI_AND_ZERO_WIDTH_RE = /[​-‏‪-‮⁦-⁩]/g;
4719
+ function sanitizeTerminalText(value, tabWidth = 2) {
4720
+ const tab = " ".repeat(Math.max(1, Math.min(8, Math.floor(tabWidth))));
4721
+ 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, "");
4722
+ let safe = "";
4723
+ for (const char of withoutEscapes) {
4724
+ const code = char.codePointAt(0) ?? 0;
4725
+ if (char === "\n" || code >= 32 && code !== 127 && !(code >= 128 && code <= 159)) {
4726
+ safe += char;
4727
+ }
4728
+ }
4729
+ return safe;
4730
+ }
4731
+ function sanitizeTerminalPreview(value, opts = {}) {
4732
+ const maxLines = opts.maxLines ?? 40;
4733
+ const maxChars = opts.maxChars ?? 8e3;
4734
+ const safe = sanitizeTerminalText(value, opts.tabWidth);
4735
+ let truncated = false;
4736
+ let clipped = safe;
4737
+ if (clipped.length > maxChars) {
4738
+ clipped = clipped.slice(0, maxChars);
4739
+ truncated = true;
4740
+ }
4741
+ const lines = clipped.split("\n");
4742
+ if (lines.length > maxLines) {
4743
+ clipped = lines.slice(0, maxLines).join("\n");
4744
+ truncated = true;
4745
+ }
4746
+ return { text: clipped, truncated };
4747
+ }
4748
+
4713
4749
  // src/utils/tool-description-mode.ts
4714
4750
  var DEFAULT_TOOL_DESCRIPTION_MODE = "extend";
4715
4751
  var ORIGINAL_TOOL_DESCRIPTION = /* @__PURE__ */ Symbol.for("wrongstack.tool.originalDescription");
@@ -5589,9 +5625,21 @@ function renderCommandLine(command, args) {
5589
5625
  });
5590
5626
  return [command, ...rendered].join(" ");
5591
5627
  }
5592
- function subjectForToolInput(toolName, input, subjectKey) {
5628
+ function renderSubjectFields(obj, fields) {
5629
+ const parts = [];
5630
+ for (const field of fields) {
5631
+ const value = obj[field];
5632
+ if (value === void 0 || value === null || value === "" || value === false) continue;
5633
+ const str = String(value);
5634
+ parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
5635
+ }
5636
+ return parts.join(" ");
5637
+ }
5638
+ function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
5593
5639
  if (!input || typeof input !== "object") return void 0;
5594
5640
  const obj = input;
5641
+ const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
5642
+ const withExtra = (base) => extra ? `${base} ${extra}` : base;
5595
5643
  if (subjectKey) {
5596
5644
  const value = obj[subjectKey];
5597
5645
  if (Array.isArray(value)) {
@@ -5607,9 +5655,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
5607
5655
  if (subjectKey === "command") {
5608
5656
  const rendered = renderCommandLine(value, obj["args"]);
5609
5657
  if (value === "commit" && obj["dry_run"] === true) {
5610
- return `${escapeGlobSubject(rendered)}:dry-run`;
5658
+ return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
5611
5659
  }
5612
- return escapeGlobSubject(rendered);
5660
+ return escapeGlobSubject(withExtra(rendered));
5613
5661
  }
5614
5662
  if (subjectKey === "directory" && obj["dry_run"] === true) {
5615
5663
  return `${escapeGlobSubject(value)}:dry-run`;
@@ -5662,7 +5710,7 @@ var DEFAULT_WALK_IGNORE_DIRS = Object.freeze([
5662
5710
  var DEFAULT_WALK_IGNORE_SET = new Set(DEFAULT_WALK_IGNORE_DIRS);
5663
5711
 
5664
5712
  // src/utils/win32-cmd.ts
5665
- var WIN32_CMD_META = /[&|<>"\r\n\0]/;
5713
+ var WIN32_CMD_META = /[&|<>"%\r\n\0]/;
5666
5714
  function buildWin32CmdShimInvocation(command, args = []) {
5667
5715
  assertSafeWin32CmdArgs([command, ...args]);
5668
5716
  const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
@@ -5893,6 +5941,8 @@ export {
5893
5941
  sanitizeMemoryEvidenceBody,
5894
5942
  sanitizeMemoryEvidenceSource,
5895
5943
  sanitizeNodeOptions,
5944
+ sanitizeTerminalPreview,
5945
+ sanitizeTerminalText,
5896
5946
  sanitizeWireToolName,
5897
5947
  sessionScopedPath,
5898
5948
  setJsonPath,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Sanitize untrusted text before it is written to a terminal.
3
+ *
4
+ * Any surface that renders model-supplied, file-supplied or MCP-supplied text
5
+ * into a TTY must run it through here first. Escape sequences in that text can
6
+ * paint outside the region that owns it: `\x1b[2J\x1b[H` clears the screen and
7
+ * homes the cursor, which lets a payload erase a permission prompt's header and
8
+ * repaint a convincing fake above the genuine key prompt. The user then answers
9
+ * the real prompt while reading the attacker's body.
10
+ *
11
+ * Bidi and zero-width controls are stripped for the same reason at a different
12
+ * layer: they reorder or hide characters so the rendered string differs from
13
+ * the string that will actually be executed (the "Trojan Source" class).
14
+ *
15
+ * This is the single source. `@wrongstack/tui` has its own copy for layout
16
+ * measurement; the CLI permission prompt and diff renderer call this one.
17
+ */
18
+ /**
19
+ * Strip terminal escapes, bidi/zero-width controls and non-printable characters
20
+ * from `value`, normalizing tabs to a fixed-width separator.
21
+ *
22
+ * Newlines are preserved; carriage returns are removed so a payload cannot
23
+ * return to the start of a line and overwrite what was already drawn.
24
+ */
25
+ export declare function sanitizeTerminalText(value: string, tabWidth?: number): string;
26
+ /**
27
+ * Sanitize and hard-cap untrusted text destined for a terminal preview.
28
+ *
29
+ * A line cap alone is not a bound: a single 200,000-character line passes a
30
+ * 40-line limit untouched and can scroll a prompt off screen. Callers that show
31
+ * a preview of attacker-influenced content should bound both dimensions.
32
+ */
33
+ export declare function sanitizeTerminalPreview(value: string, opts?: {
34
+ maxLines?: number;
35
+ maxChars?: number;
36
+ tabWidth?: number;
37
+ }): {
38
+ text: string;
39
+ truncated: boolean;
40
+ };
41
+ //# sourceMappingURL=terminal-sanitize.d.ts.map
@@ -1,5 +1,5 @@
1
1
  export declare function escapeGlobSubject(value: string): string;
2
2
  export declare function normalizePathSubject(value: string): string;
3
3
  export declare function isPathSubjectKey(subjectKey: string): boolean;
4
- export declare function subjectForToolInput(toolName: string, input: unknown, subjectKey?: string): string | undefined;
4
+ export declare function subjectForToolInput(toolName: string, input: unknown, subjectKey?: string, subjectFields?: readonly string[]): string | undefined;
5
5
  //# sourceMappingURL=tool-subject.d.ts.map
@@ -0,0 +1,61 @@
1
+ You are the Chaos Monkey ("Kaos Maymunu") — a mutation-testing saboteur for the
2
+ WrongStack fleet. Your job is to prove whether a test suite actually pins down
3
+ the code it claims to cover, by deliberately breaking that code and watching
4
+ which mutants survive.
5
+
6
+ Core belief: green tests prove nothing if they cannot detect sabotage. A mutant
7
+ that survives means the tests are fake or insufficient — and that is the most
8
+ valuable finding you can return.
9
+
10
+ ## Your task contract
11
+
12
+ The director hands you a mutation plan: an exact list of mutation ids, each with
13
+ file, line, column, kind, original token and replacement token. The plan is
14
+ authoritative — you NEVER invent, move, or "improve" mutations. Your freedom is
15
+ execution order and diagnosis, never the mutation set.
16
+
17
+ ## Check pass (per mutant)
18
+
19
+ 1. Apply exactly ONE mutation from the plan to its anchored (file, line, column).
20
+ If the anchored token no longer matches `original`, mark the mutant
21
+ `skipped` with the drift as evidence — do not hunt for a "similar" site.
22
+ 2. Run the provided test command exactly as given.
23
+ 3. Record the outcome:
24
+ - Tests fail → mutant `killed` (quote the first failing assertion).
25
+ - Tests pass → mutant `survived` (this is a weak-test finding, not your failure).
26
+ - The test command times out or is aborted → mutant `killed-by-hang`
27
+ (the mutation broke the suite by non-termination — a kill, NOT a
28
+ survivor; record the timeout as evidence). Never report a hung
29
+ command as `survived`.
30
+ 4. Restore the original source byte-for-byte before moving to the next mutant.
31
+ The suite is only honest if every mutant ran against pristine code except
32
+ its own single mutation.
33
+
34
+ ## Hard rules
35
+
36
+ - **One mutation at a time.** Never stack mutants; a stacked run measures nothing.
37
+ - **Always restore.** Your worktree must be clean of sabotage at the end of the
38
+ pass. If restore fails, stop and report which file is left mutated.
39
+ - **Stay inside the plan's files.** No refactors, no fixes, no formatting churn
40
+ — even when the mutated code looks wrong to you. You are the saboteur, not
41
+ the reviewer.
42
+ - **Deterministic.** Same plan + same suite → same report.
43
+ - **One-shot lifecycle.** Finish the assigned pass, submit the report, stop.
44
+
45
+ ## Report
46
+
47
+ Submit via `submit_result`, then repeat it as your final text (fenced JSON):
48
+
49
+ ```json
50
+ {
51
+ "summary": "<one line: N killed / M survived / K skipped>",
52
+ "mutants": [
53
+ { "id": "<plan id>", "file": "...", "line": 0, "kind": "...",
54
+ "status": "killed | survived | skipped | killed-by-hang",
55
+ "evidence": "<failing assertion, or 'suite green' for survivors>" }
56
+ ]
57
+ }
58
+ ```
59
+
60
+ Order survivors first — they are the actionable findings. For each survivor,
61
+ name the boundary or behavior the tests failed to assert.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/core",
3
- "version": "0.308.7",
3
+ "version": "0.309.1",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack core: kernel, types, defaults, and shared utilities for the WrongStack CLI agent.",
6
6
  "repository": {
@@ -182,8 +182,8 @@
182
182
  "wrongstackApiVersion": "0.1.10",
183
183
  "dependencies": {
184
184
  "zod": "4.4.3",
185
- "@wrongstack/persistence": "0.308.7",
186
- "@wrongstack/kanban": "0.308.7"
185
+ "@wrongstack/kanban": "0.309.1",
186
+ "@wrongstack/persistence": "0.309.1"
187
187
  },
188
188
  "devDependencies": {
189
189
  "@types/node": "^26.2.0",
@@ -193,7 +193,7 @@
193
193
  "access": "public"
194
194
  },
195
195
  "optionalDependencies": {
196
- "@datadog/pprof": "5.18.0"
196
+ "@datadog/pprof": "5.18.1"
197
197
  },
198
198
  "scripts": {
199
199
  "build": "node ../../scripts/build-package.mjs",