@sema-agent/core 5.65.0 → 7.0.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 (165) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +13 -3
  33. package/dist/core/governance-codes.js +15 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +62 -2
  65. package/dist/core/permission-rule-consent.js +105 -14
  66. package/dist/core/permission-rule-model.d.ts +118 -16
  67. package/dist/core/permission-rule-model.js +57 -9
  68. package/dist/core/permission-rule-session.d.ts +124 -0
  69. package/dist/core/permission-rule-session.js +121 -0
  70. package/dist/core/permission-rule-store.d.ts +65 -2
  71. package/dist/core/permission-rule-store.js +75 -16
  72. package/dist/core/permission-rule-sync.d.ts +9 -0
  73. package/dist/core/permission-rule-sync.js +52 -19
  74. package/dist/core/retention-policy.d.ts +9 -0
  75. package/dist/core/retention-policy.js +5 -2
  76. package/dist/core/retention.d.ts +13 -2
  77. package/dist/core/runner/assemble-result.d.ts +19 -1
  78. package/dist/core/runner/assemble-result.js +17 -2
  79. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  80. package/dist/core/runner/compaction-call-options.js +3 -0
  81. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  82. package/dist/core/runner/memory-capture-optout.js +53 -0
  83. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  84. package/dist/core/runner/prepare-config-doors.js +16 -0
  85. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  86. package/dist/core/runner/prepare-hands-readface.js +99 -7
  87. package/dist/core/runner/prepare-memory.d.ts +88 -0
  88. package/dist/core/runner/prepare-memory.js +305 -24
  89. package/dist/core/runner/prepare-task.d.ts +141 -1
  90. package/dist/core/runner/prepare-task.js +478 -81
  91. package/dist/core/runner/runtask.d.ts +9 -20
  92. package/dist/core/runner/runtask.js +133 -96
  93. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  94. package/dist/core/runner/session-file-state-replay.js +52 -1
  95. package/dist/core/runner/tool-disclosure.js +2 -1
  96. package/dist/core/runner/turn-attachments.d.ts +22 -12
  97. package/dist/core/session-store.d.ts +1 -1
  98. package/dist/core/session-store.js +6 -1
  99. package/dist/core/session.d.ts +34 -1
  100. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  101. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  102. package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
  103. package/dist/core/task-registry-shared.js +11 -1
  104. package/dist/core/tool-errors.js +1 -0
  105. package/dist/core/tool-policy.d.ts +172 -1
  106. package/dist/core/tool-policy.js +32 -1
  107. package/dist/core/tool-result-store.js +2 -1
  108. package/dist/core/trace.d.ts +31 -2
  109. package/dist/core/types.d.ts +795 -89
  110. package/dist/core/types.js +4 -3
  111. package/dist/core/untrusted-text.d.ts +1 -1
  112. package/dist/core/untrusted-text.js +8 -0
  113. package/dist/core/workflow-run-store-contract.js +6 -2
  114. package/dist/core/workflow-run-store.d.ts +4 -1
  115. package/dist/engine/compaction/compaction.d.ts +88 -10
  116. package/dist/engine/compaction/compaction.js +109 -30
  117. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  118. package/dist/engine/execution-env/node-execution-env.js +28 -0
  119. package/dist/engine/harness/agent-harness.d.ts +52 -1
  120. package/dist/engine/harness/agent-harness.js +36 -1
  121. package/dist/engine/harness/types.d.ts +26 -1
  122. package/dist/engine/llm/types.d.ts +50 -4
  123. package/dist/engine/loop/agent-loop.d.ts +5 -1
  124. package/dist/engine/loop/agent-loop.js +25 -0
  125. package/dist/engine/loop/types.d.ts +19 -0
  126. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  127. package/dist/engine/session/session.js +1 -1
  128. package/dist/index.d.ts +20 -9
  129. package/dist/index.js +16 -7
  130. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  131. package/dist/orchestration/run-workflow-tool.js +22 -3
  132. package/dist/orchestration/workflow-governance.d.ts +59 -1
  133. package/dist/orchestration/workflow-governance.js +61 -8
  134. package/dist/orchestration/workflow-meta.d.ts +4 -2
  135. package/dist/orchestration/workflow-primitives.js +56 -13
  136. package/dist/orchestration/workflow-types.d.ts +78 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +163 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +7 -7
  141. package/dist/stores/file/file-history-store.d.ts +368 -0
  142. package/dist/stores/file/file-history-store.js +1248 -0
  143. package/dist/stores/file/index.d.ts +22 -13
  144. package/dist/stores/file/index.js +4 -4
  145. package/dist/stores/file/permission-rule-store.d.ts +11 -0
  146. package/dist/stores/file/permission-rule-store.js +23 -9
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +169 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import { type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
31
31
  import type { PermissionRuleStoreProvider, RuleOwner } from "./permission-rule-store.js";
32
+ import type { SessionRuleOverlay } from "./permission-rule-session.js";
32
33
  /** One candidate rule inside an approval record: the exact text and where it would apply. */
33
34
  export interface RuleCandidate {
34
35
  rule: string;
@@ -199,6 +200,15 @@ export interface RuleConsentDeps {
199
200
  * happened, and withholding the receipt helps no one.
200
201
  */
201
202
  cardEdits?: boolean;
203
+ /**
204
+ * design/382 §4.3 — the SESSION-RULE OVERLAY: where a `{kind:"session"}` scoped candidate lands at
205
+ * redemption (the store write leg is never taken for one), and where the prepare-time coverage read
206
+ * merges a session's standing rows from. Host-provided, session-lifetime, never synced — see
207
+ * `permission-rule-session.ts` for the contract and the reference implementation. Absent ⇒ a
208
+ * session-scope candidate's redemption refuses loudly (there is nowhere for it to land), and
209
+ * coverage reads see no session rows — both fail toward asking.
210
+ */
211
+ sessionRules?: SessionRuleOverlay;
202
212
  }
203
213
  /** In-memory approval records — the test backend and the reference CAS semantics, the stale-row
204
214
  * envelope read included. */
@@ -257,8 +267,27 @@ export declare function prepareCardApproval(opts: {
257
267
  /** The ask's call id and argument digest, recorded for reconciliation. */
258
268
  toolCallId?: string;
259
269
  boundInputHash?: string;
260
- /** Where a redeemed rule would apply. Defaults to global. */
270
+ /**
271
+ * Where a redeemed rule would apply — any of the three consent dimensions (design/382 §4.1).
272
+ *
273
+ * design/382 §4.4 (#490②, BREAKING B2) — the DEFAULT is no longer global:
274
+ * · present ⇒ used as given (the explicit channel; all three members legal, garbage refused loudly);
275
+ * · absent with `cwd` present ⇒ `{ kind: "project", root: cwd }`. Root = the adjudicated call's
276
+ * cwd is a deliberately NARROW default — the engine is a library and repository semantics are
277
+ * host business; a host wanting the true project root passes an explicit scope. The narrow cost
278
+ * is more asks (a nested-directory rule does not cover siblings), the safe direction;
279
+ * · both absent ⇒ a LOUD refusal (`config.missing_scope`): silent-global was the #490② defect,
280
+ * silent-project has no root to anchor, and "where does this consent land" is not guessable.
281
+ */
261
282
  scope?: RuleScope;
283
+ /**
284
+ * design/382 §4.3 — the SESSION IDENTITY of the adjudicated call, threaded by the caller from the
285
+ * original call context exactly like `cwd` (never inferred from the process). It is the coverage
286
+ * read's third eligibility axis: with it, a standing session rule of that session counts as
287
+ * coverage; without it, no session rule covers anything (fail-closed). Independent of `scope` on
288
+ * purpose — `scope` names where a NEW consent would land, this names where the CALL is running.
289
+ */
290
+ sessionId?: string;
262
291
  /**
263
292
  * The working directory of the ADJUDICATED CALL — the same task root the gate's lane judged
264
293
  * with — threaded by the caller from the original call context. Never inferred from `scope`
@@ -400,7 +429,12 @@ export type EditedRuleTextPrecheck = {
400
429
  * so the surface's move is to not offer the edit box at all.
401
430
  */
402
431
  export declare function precheckEditedRuleText(text: string, command: string): EditedRuleTextPrecheck;
403
- /** What a redemption produced. `alreadyRedeemed` marks the replay path — the same dot, no second rule. */
432
+ /** What a redemption produced. `alreadyRedeemed` marks the replay path — the same dot, no second rule.
433
+ *
434
+ * `rev` is the DURABLE store's revision. A session-scope redemption (design/382 §4.3) lands in the
435
+ * session overlay and does not move it: its `rev` is a best-effort read of the store's current
436
+ * revision (0 when the store could not be read — the landing is in the overlay either way, and the
437
+ * durable revision is reporting, not the landing's identity). */
404
438
  export type RedeemResult = {
405
439
  status: "redeemed";
406
440
  rule: string;
@@ -448,6 +482,26 @@ export interface ImportPreview {
448
482
  rule: string;
449
483
  reason: string;
450
484
  }>;
485
+ /**
486
+ * #490 件4 — the entries whose SPELLING was rewritten on the way in, `from` as the settings file
487
+ * writes it and `to` as this lane will hold it (`Bash(npm run *)` → `Bash(npm run:*)`).
488
+ *
489
+ * Exactly ONE row per candidate the translation produced, in candidate order, carrying that
490
+ * candidate's `scope`. The scope is load-bearing rather than decoration: the same file line can
491
+ * appear in two layers and become two candidates in two scopes, and a report keyed on the text alone
492
+ * would show one row for two consents (independent-review finding — the row count and the candidate
493
+ * count must be readable against each other).
494
+ *
495
+ * Always present, empty when nothing was translated: a person confirming this batch is being shown
496
+ * rule texts that are not the ones in their file, and a preview that let that pass silently would be
497
+ * asking for consent to something it had not shown them. Entries refused AFTER translation are not
498
+ * here — they are in `skipped`, named as the file writes them.
499
+ */
500
+ translated: Array<{
501
+ from: string;
502
+ to: string;
503
+ scope: RuleScope;
504
+ }>;
451
505
  layers: Array<{
452
506
  path: string;
453
507
  layer: ImportedSettingsLayer;
@@ -488,6 +542,12 @@ export interface ImportPreview {
488
542
  * `false` AND `deduped` — the two axes answer different questions and neither implies the other).
489
543
  * A refused row always carries its `reason`. `deduped` counts as LANDED: an equivalent rule
490
544
  * already standing means the consent is already in effect.
545
+ *
546
+ * design/382 §4.3 — on a SESSION-scope member the two landed words mean the same thing about a
547
+ * different home: landed in / already in the session's OVERLAY (a session rule's home store), never
548
+ * the persisted store. The words state that the landing happened; DURABILITY is what the member's own
549
+ * `scope` says, so a consumer never has to guess it off the status (deliberately no fourth status
550
+ * value — the closed set names landing outcomes, not storage classes).
491
551
  */
492
552
  export type RedeemedBatchMember = {
493
553
  readonly candidateIndex: number;
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
3
- import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
2
+ import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, translateImportedWildcardRule, } from "./permission-rule-model.js";
3
+ import { errText, isValidConsentScope, normalizePersistedRule, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
4
4
  export class InMemoryRuleApprovalRecordStore {
5
5
  rows = new Map();
6
6
  async get(id) {
@@ -121,6 +121,9 @@ function approvalRecordDamageOf(rec) {
121
121
  if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
122
122
  return `offer ${at} references candidate ${member}, which is not a candidate row`;
123
123
  }
124
+ if (!isValidConsentScope(cand.scope)) {
125
+ return `offer ${at} references candidate ${member}, whose scope is not a consent-face scope (global, project, or session)`;
126
+ }
124
127
  if (seen.has(member))
125
128
  return `batch offer ${at} references candidate ${member} twice`;
126
129
  seen.add(member);
@@ -196,11 +199,42 @@ export async function prepareCardApproval(opts) {
196
199
  }
197
200
  if (opts.toolName !== CARD_RULE_TOOL)
198
201
  return undefined;
199
- const scope = opts.scope ?? { kind: "global" };
202
+ const scope = (() => {
203
+ if (opts.scope !== undefined) {
204
+ if (!isValidConsentScope(opts.scope)) {
205
+ const e = new Error("prepareCardApproval got a scope that is not a consent-face scope (global, project with a non-empty root, or session with a non-empty sessionId) — refusing to guess where this consent should land");
206
+ e.code = "config.invalid_argument";
207
+ throw e;
208
+ }
209
+ return opts.scope;
210
+ }
211
+ if (opts.cwd !== undefined) {
212
+ if (typeof opts.cwd !== "string" || opts.cwd === "") {
213
+ const e = new Error("prepareCardApproval got a cwd that is not a usable path — it cannot anchor the derived project scope, and refusing beats minting a rule scoped to garbage");
214
+ e.code = "config.invalid_argument";
215
+ throw e;
216
+ }
217
+ return { kind: "project", root: opts.cwd };
218
+ }
219
+ const e = new Error("prepareCardApproval was given neither a scope nor a cwd — there is nowhere for this consent to land, and no direction of silence is honest (config.missing_scope): pass an explicit scope, or thread the adjudicated call's cwd");
220
+ e.code = "config.missing_scope";
221
+ throw e;
222
+ })();
200
223
  let coverage;
201
224
  try {
202
225
  const listed = await storeForOwner(opts.deps.provider, owner).list();
203
- coverage = segmentCoverageOf(opts.command, { persisted: listed.rules }, { tool: CARD_RULE_TOOL, cwd: opts.cwd });
226
+ let table = listed.rules;
227
+ const overlay = opts.deps.sessionRules;
228
+ if (overlay !== undefined && opts.sessionId !== undefined) {
229
+ const served = structuredClone(await overlay.read(opts.sessionId));
230
+ const sessionRows = served.filter((r) => {
231
+ const scope = r?.scope;
232
+ return scope?.kind === "session" && scope.sessionId === opts.sessionId && !("reject" in normalizePersistedRule(r));
233
+ });
234
+ if (sessionRows.length > 0)
235
+ table = [...sessionRows, ...listed.rules];
236
+ }
237
+ coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId });
204
238
  }
205
239
  catch {
206
240
  coverage = undefined;
@@ -497,6 +531,41 @@ async function applyRedemption(args) {
497
531
  if (writer === undefined)
498
532
  return { status: "refused", reason: "the resolved permission-rule store has no write face" };
499
533
  const origin = originOfRecordKind(args.kind);
534
+ if (args.candidate.scope.kind === "session") {
535
+ const overlay = args.deps.sessionRules;
536
+ if (overlay === undefined) {
537
+ return {
538
+ status: "refused",
539
+ reason: "this deployment wired no session-rule overlay (RuleConsentDeps.sessionRules) — a session-scope authorization has nowhere to land",
540
+ };
541
+ }
542
+ let outcome;
543
+ try {
544
+ outcome = await overlay.apply(args.candidate.scope.sessionId, {
545
+ rule: args.parsedRule.rule,
546
+ tool: args.parsedRule.tool,
547
+ match: args.parsedRule.match,
548
+ command: args.parsedRule.command,
549
+ add: { dot: args.dot, origin, createdAt: nowIso(args.deps) },
550
+ });
551
+ }
552
+ catch (err) {
553
+ return { status: "refused", reason: `the session-rule overlay refused the write: ${errText(err)}` };
554
+ }
555
+ if ("refused" in outcome) {
556
+ return {
557
+ status: "refused",
558
+ reason: "the session this authorization targets has ended — a session rule dies with its session, and a late or replayed redemption cannot revive it (re-trigger the command in a live session for a fresh card)",
559
+ };
560
+ }
561
+ let rev = 0;
562
+ try {
563
+ rev = (await store.list()).rev;
564
+ }
565
+ catch {
566
+ }
567
+ return { status: "redeemed", rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev, alreadyRedeemed: args.replay };
568
+ }
500
569
  for (let attempt = 0; attempt < REDEEM_MAX_ATTEMPTS; attempt++) {
501
570
  let rev;
502
571
  try {
@@ -538,6 +607,7 @@ export async function prepareCcImport(opts) {
538
607
  const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCcImport");
539
608
  const candidates = [];
540
609
  const skipped = [];
610
+ const translated = [];
541
611
  const layers = [];
542
612
  const denyAskBuckets = { deny: 0, ask: 0 };
543
613
  for (const layer of opts.layers) {
@@ -614,17 +684,25 @@ export async function prepareCcImport(opts) {
614
684
  skipped.push({ rule: entry, reason });
615
685
  continue;
616
686
  }
617
- const parsed = parseAllowRuleText(entry);
687
+ const wildcard = translateImportedWildcardRule(entry);
688
+ if (wildcard !== undefined && "skip" in wildcard) {
689
+ skipped.push({ rule: entry, reason: wildcard.skip });
690
+ continue;
691
+ }
692
+ const text = wildcard?.rule ?? entry;
693
+ const parsed = parseAllowRuleText(text);
618
694
  if ("reject" in parsed) {
619
695
  skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
620
696
  continue;
621
697
  }
622
698
  if (candidates.some((c) => c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
623
699
  continue;
700
+ if (wildcard !== undefined)
701
+ translated.push({ from: entry, to: parsed.rule.rule, scope });
624
702
  candidates.push({ rule: parsed.rule.rule, scope });
625
703
  }
626
704
  }
627
- const preview = { candidates, skipped, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
705
+ const preview = { candidates, skipped, translated, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
628
706
  if (candidates.length === 0)
629
707
  return { preview };
630
708
  const record = {
@@ -708,14 +786,27 @@ export async function redeemRuleBatch(opts) {
708
786
  const candidate = rec.candidates[candidateIndex];
709
787
  const row = { candidateIndex, rule: candidate.rule, scope: candidate.scope };
710
788
  let alreadyThere = false;
711
- try {
712
- const snap = await store.list();
713
- rev = snap.rev;
714
- alreadyThere = snap.rules.some((r) => r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
789
+ if (candidate.scope.kind === "session") {
790
+ const sessionScope = candidate.scope;
791
+ try {
792
+ const overlayRows = opts.deps.sessionRules === undefined ? [] : await opts.deps.sessionRules.read(sessionScope.sessionId);
793
+ alreadyThere = overlayRows.some((r) => r.rule === candidate.rule && sameScope(r.scope, sessionScope));
794
+ }
795
+ catch (err) {
796
+ members.push({ ...row, status: "refused", reason: `could not read the session-rule overlay: ${errText(err)}` });
797
+ continue;
798
+ }
715
799
  }
716
- catch (err) {
717
- members.push({ ...row, status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` });
718
- continue;
800
+ else {
801
+ try {
802
+ const snap = await store.list();
803
+ rev = snap.rev;
804
+ alreadyThere = snap.rules.some((r) => r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
805
+ }
806
+ catch (err) {
807
+ members.push({ ...row, status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` });
808
+ continue;
809
+ }
719
810
  }
720
811
  const res = await redeemRuleTicket({
721
812
  ticket: mintRuleTicket(rec.id, candidateIndex),
@@ -726,7 +817,7 @@ export async function redeemRuleBatch(opts) {
726
817
  members.push({ ...row, status: "refused", reason: res.reason });
727
818
  continue;
728
819
  }
729
- rev = res.rev;
820
+ rev = candidate.scope.kind === "session" ? Math.max(rev, res.rev) : res.rev;
730
821
  members.push({ ...row, status: alreadyThere ? "deduped" : "persisted", alreadyRedeemed: res.alreadyRedeemed, dot: res.dot });
731
822
  }
732
823
  return { members, rev };
@@ -69,18 +69,43 @@ export type PersistedRuleTool = "Bash";
69
69
  /** v1 match forms. `"wildcard"` is reserved for v2 and is not a value this version ever produces. */
70
70
  export type PersistedRuleMatch = "exact" | "prefix";
71
71
  /**
72
- * Where a rule applies. A rule read out of a project-local settings layer applies ONLY inside that
73
- * project's canonical root — project A's approval is not project B's. User-layer, starter and
74
- * user-minted rules are global.
75
- *
76
- * `root` is expected to be already canonicalized (realpath) by whoever constructs the scope; the
77
- * matcher compares with word-boundary containment, never a bare `startsWith` (`/a` must not reach `/ab`).
72
+ * Where a rule applies design/382 §4.1, the THREE consent dimensions.
73
+ *
74
+ * · `global` — the personal-global dimension: this OWNER's rules, everywhere (the bucket is already
75
+ * per-owner, {@link import("./permission-rule-store.js").RuleOwner}). The member keeps its historical
76
+ * spelling on purpose: renaming it `personal-global` would be a no-benefit migration of every stored
77
+ * row; the "personal" half lives in documentation and card wording, not in the wire byte.
78
+ * · `project` — applies ONLY inside `root`. `root` is expected to be already canonicalized (realpath)
79
+ * by whoever constructs the scope; the matcher compares with word-boundary containment, never a bare
80
+ * `startsWith` (`/a` must not reach `/ab`).
81
+ * · `session` — applies ONLY to calls of the ONE session it names, and lives in that session's own
82
+ * state (the host's session-rule overlay), NEVER in the persisted store: every durable entrance —
83
+ * the write deltas, sync in both directions, at-rest bytes — refuses or drops a session row
84
+ * (design/382 §4.3, the durable two-member face). `sessionId` uniqueness is the HOST's duty (the
85
+ * `principal` posture): reusing an id is a declaration that it is the same session. A session row
86
+ * cannot be individually deleted — it dies with its session (design/382 §10, ruled; the durable
87
+ * removal entry refuses it rather than answering a false no-op).
88
+ *
89
+ * ORG is deliberately NOT a member (design/382 §4.1): an approval card must be structurally unable to
90
+ * spell an org-wide grant — org governance is a deny/ask snapshot published by an admin surface, with
91
+ * no allow bucket at all. Unspellable beats runtime-refused; the consent validators refuse the spelling
92
+ * anyway if a foreign writer plants it.
93
+ *
94
+ * TWO NAMED VALIDATION FACES read this union (design/382 §4.3): the CONSENT face (prepare candidates,
95
+ * approval records, confirm/redeem, batch outcomes) admits all three members
96
+ * ({@link import("./permission-rule-store.js").isValidConsentScope}); the DURABLE face (the persisted
97
+ * store's write deltas, rule-sync in both directions, at-rest decode) admits only
98
+ * `{global, project}` ({@link import("./permission-rule-store.js").isValidDurableScope}). One table
99
+ * "for both" is exactly what would let a session row leak into the store silently.
78
100
  */
79
101
  export type RuleScope = {
80
102
  kind: "global";
81
103
  } | {
82
104
  kind: "project";
83
105
  root: string;
106
+ } | {
107
+ kind: "session";
108
+ sessionId: string;
84
109
  };
85
110
  /** The immutable causal identity of one add: a replica identity plus a monotonic counter. */
86
111
  export interface RuleDot {
@@ -308,7 +333,7 @@ export declare function escapeForDisclosure(value: unknown): string;
308
333
  * transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
309
334
  * price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
310
335
  * carrying seats keep unchanged: this function neither changes what those seats contain nor what any
311
- * of them means. A caller that must instead tell two hostile texts APART wants
336
+ * of them means. A caller that must instead tell two adversarial texts APART wants
312
337
  * {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
313
338
  * {@link hasUnrenderableCharacters} (refuse instead of render).
314
339
  *
@@ -348,6 +373,47 @@ export declare function parseAllowRuleText(text: string, opts?: {
348
373
  };
349
374
  /** The canonical spelling of a rule for a (already folded) command and match form. */
350
375
  export declare function formatAllowRuleText(command: string, match: PersistedRuleMatch): string;
376
+ /**
377
+ * #490 件4 — read ANOTHER product's `Bash(<body> *)` SPACE-STAR spelling as this lane's colon-star
378
+ * prefix rule, for the IMPORT door only.
379
+ *
380
+ * Why a translation is owed at all: the other product mints its prefix suggestions in that spelling
381
+ * (its own classifier reads `X:*` as its earlier prefix form and anything else containing an unescaped star as
382
+ * a wildcard), so a person migrating settings had EVERY prefix rule they had ever approved refused as
383
+ * `unsupported.wildcard` — the import silently emptied out exactly the rules that were the point of it.
384
+ *
385
+ * Translated ONLY for the ONE spelling whose semantics this lane can state exactly: content ending in
386
+ * ` *` with that star the only unescaped one. Upstream compiles a single trailing ` *` into an
387
+ * OPTIONAL suffix (`^body( .*)?$`), which is precisely "the body, or the body followed by a space and
388
+ * more" — this lane's prefix predicate. Any other star placement compiles to a real glob with no rule
389
+ * form here, and a bare `*` is a whole-tool grant that must never become a command rule; both are
390
+ * refused with a reason of their own so the import can DISCLOSE them rather than drop them.
391
+ *
392
+ * WHAT THE TRANSLATED RULE GRANTS, against the source spelling, axis by axis (the direction matters,
393
+ * and it is one-way with one named exception): identical on the bare body, on body-plus-arguments, on
394
+ * the word boundary (`npm run` never reaches `npm run-x`) and on compounds (both sides refuse a chain
395
+ * against a single-command body). NARROWER here on four axes, because upstream normalizes the
396
+ * CANDIDATE before matching and this lane never does: a candidate carrying a redirection, wrapped in
397
+ * `timeout`/`nice`/`nohup`-style words, fed through `xargs`, or prefixed with a `VAR=value` assignment
398
+ * matches there and matches nothing here. WIDER on exactly one: this lane folds runs of whitespace on
399
+ * both sides, so `npm run build` matches the imported rule where upstream's wildcard arm (which does
400
+ * not fold) would refuse it. That axis is NOT introduced by this translation — whitespace folding is
401
+ * this lane's universal reading and already applies to every imported EXACT rule, against an upstream
402
+ * exact arm that is byte-strict — so it is disclosed as a lane property rather than treated as a
403
+ * translation defect. Every other difference runs toward granting less.
404
+ *
405
+ * Returns `undefined` when the text is not a Bash wildcard spelling at all (nothing to translate; the
406
+ * caller proceeds with the text as written), `{ rule }` with the spelling to parse INSTEAD, or
407
+ * `{ skip }` with the reason a star spelling has no form here. Never bypasses the validator: the
408
+ * returned text goes through {@link parseAllowRuleText} exactly like a hand-written one, so the
409
+ * interpreter-head refusal, the control-character screen and the length bound all still apply — a
410
+ * translated `Bash(node *)` is refused `invalid.bare_interpreter_prefix` just as a typed one is.
411
+ */
412
+ export declare function translateImportedWildcardRule(text: string): {
413
+ rule: string;
414
+ } | {
415
+ skip: string;
416
+ } | undefined;
351
417
  /**
352
418
  * Does this rule's command pattern admit `command`?
353
419
  *
@@ -400,9 +466,16 @@ export declare function ruleLaneSegmentsOf(command: string): readonly string[] |
400
466
  * not contain `/ab`. Both sides are expected to be canonical already.
401
467
  */
402
468
  export declare function pathWithinRoot(path: string, root: string): boolean;
403
- /** Does a rule's scope cover a task running in `cwd`? A project rule needs a cwd to compare against;
404
- * without one it covers nothing (fail-closed). */
405
- export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined): boolean;
469
+ /** Does a rule's scope cover a task running in `cwd` (and, for the session dimension, in the session
470
+ * named by `sessionId`)? A project rule needs a cwd to compare against; without one it covers nothing
471
+ * (fail-closed). A session rule (design/382 §4.3) covers a call iff the call's `sessionId` equals the
472
+ * scope's — and a call that carries NO session identity is covered by no session rule, the same
473
+ * fail-closed arm as project-without-cwd: an axis the caller did not thread is an axis the rule
474
+ * cannot speak on. `sessionId` is threaded from the ORIGINAL call context by the caller (the 375
475
+ * r3-② cwd posture), never inferred from the process. ONE function on purpose: this is the scope
476
+ * conjunct of the single eligibility predicate ({@link eligibleContext}), and a sister predicate
477
+ * would be the second fact source the 375 §5.1 discipline exists to forbid. */
478
+ export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined, sessionId?: string): boolean;
406
479
  /** Is this rule live — i.e. does it still carry at least one add? Deleted adds are removed by the store
407
480
  * when tombstones are applied, so a rule with an empty `adds` is a rule that no longer exists. */
408
481
  export declare function isRuleLive(rule: PersistedAllowRule): boolean;
@@ -418,6 +491,12 @@ export declare function isRuleLive(rule: PersistedAllowRule): boolean;
418
491
  *
419
492
  * `scope` rides beside the rule rather than inside it for the PROPOSED half's sake: a
420
493
  * {@link ParsedAllowRule} carries no scope, so a proposal is judged against the scope it would LAND in.
494
+ *
495
+ * design/382 §4.3 — the call context carries a THIRD axis, `sessionId` (optional: an existing caller
496
+ * that threads only `{tool, cwd}` still compiles and still means what it meant — no session rule can
497
+ * cover its calls, the fail-closed direction). It is the session dimension's whole eligibility story:
498
+ * every consumer of this ONE predicate (the gate's conjunction arm, the coverage table, the consent
499
+ * prepare) inherits it without a second judge existing anywhere.
421
500
  */
422
501
  export declare function eligibleContext(rule: {
423
502
  tool: string;
@@ -425,6 +504,7 @@ export declare function eligibleContext(rule: {
425
504
  }, call: {
426
505
  tool: string;
427
506
  cwd: string | undefined;
507
+ sessionId?: string;
428
508
  }): boolean;
429
509
  /**
430
510
  * design/375 §5.1 — MAY this PERSISTED rule participate in admitting this call at all? The liveness
@@ -440,6 +520,7 @@ export declare function eligibleContext(rule: {
440
520
  export declare function eligiblePersisted(rule: PersistedAllowRule, call: {
441
521
  tool: string;
442
522
  cwd: string | undefined;
523
+ sessionId?: string;
443
524
  }): boolean;
444
525
  /**
445
526
  * Find the rules that admit this command for a task in `cwd`, or `undefined`.
@@ -474,12 +555,24 @@ export declare function findAdmittingRule(rules: readonly PersistedAllowRule[],
474
555
  tool: string;
475
556
  command: string;
476
557
  cwd: string | undefined;
558
+ sessionId?: string;
477
559
  }): readonly PersistedAllowRule[] | undefined;
478
560
  /**
479
561
  * design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
480
- * already admits, judged with the SAME split, the same MATCH reading and the same eligibility
481
- * predicates as the gate's conjunction arm — one instrument, so the suggestion face and the consent
482
- * prepare can never call a segment covered that the gate would ask about (or vice versa).
562
+ * already admits, judged with the SAME split and the same eligibility predicates as the gate's
563
+ * conjunction arm — one instrument, so the suggestion face and the consent prepare can never call a
564
+ * segment covered that the gate would ask about (or vice versa).
565
+ *
566
+ * The split is the OFFER reading ({@link OFFER_READING}, #490 修①), which is the MATCH reading on
567
+ * every axis but where a redirection is refused — this table exists to align, row for row, with
568
+ * {@link suggestRulesForCommand}'s own segmentation, and a table that stopped one command short of the
569
+ * face it aligns with is a table that face discards (its positional belt), leaving every segment
570
+ * reading UNCOVERED. Each ROW's verdict is still the matcher's, unchanged: `covered` is
571
+ * {@link ruleAdmitsCommand} under the MATCH reading, so a segment carrying a redirection is admitted
572
+ * by nothing and reports `false`. The gate's own conjunction arm keeps the whole-string floor and is
573
+ * not consulted here: for a redirection-bearing compound it has no segments at all, and this table
574
+ * saying "an eligible rule admits `npm test`" is a true statement about that segment either way — it
575
+ * is read only to keep an already-covered segment out of a batch offer, never as an admission.
483
576
  *
484
577
  * TWO SOURCES, two predicates, and the seat labels are the contract:
485
578
  * · `persisted` — rules already in the store, judged {@link eligiblePersisted} ∧ admit: a tombstoned
@@ -503,6 +596,7 @@ export declare function segmentCoverageOf(command: string, rules: {
503
596
  }, call: {
504
597
  tool: string;
505
598
  cwd: string | undefined;
599
+ sessionId?: string;
506
600
  }): readonly SegmentCoverage[] | undefined;
507
601
  /** One row of a per-segment coverage table (see {@link segmentCoverageOf}): the FOLDED, trimmed
508
602
  * segment text (a display/correlation seat, never adjudication input) and whether an eligible rule
@@ -632,9 +726,17 @@ export type RuleOffer = {
632
726
  * over-offering; every offered rule still passes the validator at redemption, so an over-offer widens
633
727
  * nothing).
634
728
  *
635
- * Returns an empty array for anything the rule lane cannot speak for (redirections, substitutions,
636
- * subshells, backgrounding) — the card then simply carries no "don't ask again" option, which is the
637
- * honest answer.
729
+ * Returns an empty array for anything the rule lane cannot speak for (substitutions, subshells,
730
+ * backgrounding, an unreadable segment) — the card then simply carries no "don't ask again" option,
731
+ * which is the honest answer.
732
+ *
733
+ * A REDIRECTION is the one member of that list judged per SEGMENT rather than over the whole string
734
+ * (#490 修①, {@link OFFER_READING}): `a && b 2>/dev/null && c` offers a batch for `a` and `c` with
735
+ * `uncoveredSegments: 1`, exactly as the same compound dirtied by a glob always has. A SIMPLE command
736
+ * carrying one still offers nothing — its only segment is the dirty one — and no offer this function
737
+ * mints ever admits the redirection-bearing text: the minting grammar and the matcher keep the
738
+ * whole-string floor, so the compound stays un-admitted and the honest `uncoveredSegments` count says
739
+ * so (design/382 §3.2: neither a sanitized rule text nor a widened matcher is on the table).
638
740
  */
639
741
  export declare function suggestRulesForCommand(command: string, ctx?: {
640
742
  readonly coverage?: readonly SegmentCoverage[];
@@ -1,5 +1,5 @@
1
1
  import { parsePermissionRule } from "./permission-rules.js";
2
- import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
2
+ import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
3
3
  import { inlineUntrusted } from "./untrusted-text.js";
4
4
  export const MAX_RULE_TEXT_CHARS = 512;
5
5
  export const BARE_INTERPRETER_NAMES = new Set([
@@ -117,17 +117,22 @@ function reject(code, message) {
117
117
  return { reject: { code, message } };
118
118
  }
119
119
  const RULE_LANE_FLOOR = { pathPrefixedNameIsText: true };
120
- const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false };
121
- const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true };
120
+ const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "reject" };
121
+ const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true, redirection: "reject" };
122
+ const OFFER_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "tolerate" };
122
123
  function commandBasename(name) {
123
124
  return name.slice(name.lastIndexOf("/") + 1);
124
125
  }
125
126
  function ruleLaneShapeOf(command, reading) {
126
- const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator });
127
+ const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator, redirection: reading.redirection });
127
128
  if ("reject" in split)
128
129
  return { reject: split.reject };
129
130
  const names = [];
130
131
  for (const segment of split.segments) {
132
+ if (reading.redirection === "tolerate" && carriesShellRedirection(segment)) {
133
+ names.push(undefined);
134
+ continue;
135
+ }
131
136
  const floor = parseLeadingCommandName(segment, { ...RULE_LANE_FLOOR, quotedOperatorsAreText: reading.quotedOperatorsAreText });
132
137
  if ("reject" in floor) {
133
138
  return {
@@ -219,6 +224,45 @@ export function parseAllowRuleText(text, opts) {
219
224
  export function formatAllowRuleText(command, match) {
220
225
  return `Bash(${command}${match === "prefix" ? ":*" : ""})`;
221
226
  }
227
+ function hasUnescapedStar(s) {
228
+ for (let i = 0; i < s.length; i++) {
229
+ if (s[i] !== "*")
230
+ continue;
231
+ let backslashes = 0;
232
+ for (let j = i - 1; j >= 0 && s[j] === "\\"; j--)
233
+ backslashes++;
234
+ if (backslashes % 2 === 0)
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+ function isImportedWildcardContent(content) {
240
+ return !content.endsWith(":*") && hasUnescapedStar(content);
241
+ }
242
+ export function translateImportedWildcardRule(text) {
243
+ const parsed = parsePermissionRule(text);
244
+ const content = parsed.ruleContent;
245
+ if (content === undefined || parsed.toolName !== "Bash" || !isImportedWildcardContent(content))
246
+ return undefined;
247
+ const body = /^(.+) \*$/.exec(content)?.[1];
248
+ if (body === undefined || body.trim() === "") {
249
+ return {
250
+ skip: "unsupported.wildcard: only the trailing `<command> *` form has a prefix-rule equivalent here — it stays in the settings file, unimported",
251
+ };
252
+ }
253
+ if (hasUnescapedStar(body)) {
254
+ return {
255
+ skip: "unsupported.wildcard: a `*` before the end of the pattern matches anything at that position, which has no rule form in this version — it stays in the settings file, unimported",
256
+ };
257
+ }
258
+ const bodyShape = ruleLaneShapeOf(body, MATCH_READING);
259
+ if (!("reject" in bodyShape) && bodyShape.segments.length > 1) {
260
+ return {
261
+ skip: "unsupported.wildcard: a `<chain> *` pattern names more than one command, and the trailing-star form has no equivalent for a chain — it stays in the settings file, unimported",
262
+ };
263
+ }
264
+ return { rule: formatAllowRuleText(body, "prefix") };
265
+ }
222
266
  export function ruleAdmitsCommand(rule, command) {
223
267
  return admitsUnder(rule, command, MATCH_READING);
224
268
  }
@@ -251,16 +295,18 @@ export function pathWithinRoot(path, root) {
251
295
  const base = root.endsWith("/") ? root : root + "/";
252
296
  return path.startsWith(base);
253
297
  }
254
- export function scopeCoversCwd(scope, cwd) {
298
+ export function scopeCoversCwd(scope, cwd, sessionId) {
255
299
  if (scope.kind === "global")
256
300
  return true;
301
+ if (scope.kind === "session")
302
+ return sessionId !== undefined && sessionId === scope.sessionId;
257
303
  return cwd !== undefined && pathWithinRoot(cwd, scope.root);
258
304
  }
259
305
  export function isRuleLive(rule) {
260
306
  return rule.adds.length > 0;
261
307
  }
262
308
  export function eligibleContext(rule, call) {
263
- return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd);
309
+ return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
264
310
  }
265
311
  export function eligiblePersisted(rule, call) {
266
312
  return isRuleLive(rule) && eligibleContext(rule, call);
@@ -297,7 +343,7 @@ export function segmentCoverageOf(command, rules, call) {
297
343
  const folded = foldSpacing(command);
298
344
  if (folded === undefined)
299
345
  return undefined;
300
- const shape = ruleLaneShapeOf(folded, MATCH_READING);
346
+ const shape = ruleLaneShapeOf(folded, OFFER_READING);
301
347
  if ("reject" in shape)
302
348
  return undefined;
303
349
  const proposed = rules.proposed ?? [];
@@ -308,7 +354,7 @@ export function segmentCoverageOf(command, rules, call) {
308
354
  }));
309
355
  }
310
356
  export function suggestRulesForCommand(command, ctx) {
311
- const shape = ruleLaneShapeOf(command, MATCH_READING);
357
+ const shape = ruleLaneShapeOf(command, OFFER_READING);
312
358
  if ("reject" in shape)
313
359
  return [];
314
360
  const folded = foldSpacing(command);
@@ -333,7 +379,7 @@ export function suggestRulesForCommand(command, ctx) {
333
379
  }
334
380
  return offers;
335
381
  }
336
- const foldedShape = ruleLaneShapeOf(folded, MATCH_READING);
382
+ const foldedShape = ruleLaneShapeOf(folded, OFFER_READING);
337
383
  if ("reject" in foldedShape || foldedShape.segments.length !== shape.segments.length)
338
384
  return offers;
339
385
  const segments = foldedShape.segments.map((s) => s.trim());
@@ -341,6 +387,8 @@ export function suggestRulesForCommand(command, ctx) {
341
387
  const alignedTable = table !== undefined && table.length === segments.length && table.every((row, i) => row.segment === segments[i]) ? table : undefined;
342
388
  const coveredAt = (i) => alignedTable?.[i]?.covered === true;
343
389
  const mintSegmentRule = (segment) => {
390
+ if (carriesShellRedirection(segment))
391
+ return undefined;
344
392
  const body = bodyOf(tokensOf(segment));
345
393
  if (body !== undefined) {
346
394
  const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));