@sema-agent/core 7.30.0 → 7.31.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 (90) hide show
  1. package/CHANGELOG.md +274 -0
  2. package/dist/agents/cross-session-judge.d.ts +4 -12
  3. package/dist/agents/cross-session-judge.js +4 -11
  4. package/dist/agents/peer-session-drain.js +0 -5
  5. package/dist/agents/send-message-tool.js +1 -0
  6. package/dist/agents/subagent.d.ts +1 -0
  7. package/dist/agents/subagent.js +1 -0
  8. package/dist/core/ask-origin.d.ts +6 -0
  9. package/dist/core/ask-origin.js +10 -0
  10. package/dist/core/checkpoint-store.d.ts +14 -0
  11. package/dist/core/checkpoint-store.js +1 -0
  12. package/dist/core/engine-notice.d.ts +4 -0
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +3 -0
  15. package/dist/core/memory-engine/data-plane.d.ts +3 -1
  16. package/dist/core/memory-engine/data-plane.js +1 -1
  17. package/dist/core/memory-engine/delegation-settlement.d.ts +44 -3
  18. package/dist/core/memory-engine/delegation-settlement.js +38 -3
  19. package/dist/core/memory-engine/engine.d.ts +123 -26
  20. package/dist/core/memory-engine/engine.js +426 -149
  21. package/dist/core/memory-engine/entry-scan.d.ts +23 -0
  22. package/dist/core/memory-engine/entry-scan.js +55 -0
  23. package/dist/core/memory-engine/file-backend.d.ts +16 -23
  24. package/dist/core/memory-engine/file-backend.js +122 -99
  25. package/dist/core/memory-engine/layer-observations.d.ts +109 -0
  26. package/dist/core/memory-engine/layer-observations.js +148 -0
  27. package/dist/core/memory-engine/layout.d.ts +11 -0
  28. package/dist/core/memory-engine/layout.js +20 -4
  29. package/dist/core/memory-engine/types.d.ts +26 -2
  30. package/dist/core/parent-spec-seat.d.ts +4 -1
  31. package/dist/core/parent-spec-seat.js +2 -0
  32. package/dist/core/park-selfcheck.js +1 -0
  33. package/dist/core/runner/announce-once-ledger.d.ts +26 -0
  34. package/dist/core/runner/announce-once-ledger.js +37 -0
  35. package/dist/core/runner/contracts.d.ts +22 -5
  36. package/dist/core/runner/denial-limit-arms.d.ts +2 -2
  37. package/dist/core/runner/prepare-acquire-reconcile.d.ts +0 -4
  38. package/dist/core/runner/prepare-acquire-reconcile.js +1 -3
  39. package/dist/core/runner/prepare-file-history.d.ts +13 -15
  40. package/dist/core/runner/prepare-file-history.js +57 -43
  41. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  42. package/dist/core/runner/prepare-listings.d.ts +3 -1
  43. package/dist/core/runner/prepare-listings.js +2 -2
  44. package/dist/core/runner/prepare-memory.js +16 -5
  45. package/dist/core/runner/prepare-park-ask.js +27 -16
  46. package/dist/core/runner/prepare-prompt-inputs.d.ts +8 -1
  47. package/dist/core/runner/prepare-prompt-inputs.js +5 -3
  48. package/dist/core/runner/prepare-task.js +7 -5
  49. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -0
  50. package/dist/core/runner/prepare-wiring-manifest.js +2 -1
  51. package/dist/core/runner/run-attachment-seats.js +1 -1
  52. package/dist/core/runner/run-leg.js +61 -52
  53. package/dist/core/runner/run-settle-and-teardown.js +4 -0
  54. package/dist/core/runner/run-turn-boundary.js +6 -6
  55. package/dist/core/runner/turn-attachments.d.ts +6 -6
  56. package/dist/core/runner-deps.d.ts +3 -1
  57. package/dist/core/secret-env.d.ts +11 -0
  58. package/dist/core/secret-env.js +22 -1
  59. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  60. package/dist/core/store-contracts/checkpoint-store-contract.js +22 -0
  61. package/dist/core/task-result.d.ts +1 -1
  62. package/dist/core/task-spec.d.ts +17 -8
  63. package/dist/core/tool-spec.d.ts +1 -1
  64. package/dist/core/wiring-manifest.d.ts +10 -0
  65. package/dist/core/wiring-manifest.js +7 -1
  66. package/dist/engine/execution-env/node-execution-env.d.ts +6 -0
  67. package/dist/engine/execution-env/node-execution-env.js +15 -17
  68. package/dist/engine/harness/agent-harness.js +7 -5
  69. package/dist/engine/lsp/node-lsp-manager.d.ts +7 -0
  70. package/dist/engine/lsp/node-lsp-manager.js +11 -3
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.js +1 -1
  73. package/dist/shell/lex.d.ts +8 -3
  74. package/dist/shell/lex.js +2 -2
  75. package/dist/tools/fs/bash-readonly-classifier.d.ts +7 -1
  76. package/dist/tools/fs/bash-readonly-classifier.js +7 -3
  77. package/dist/tools/fs/shell-read-establish.d.ts +1 -1
  78. package/dist/tools/fs/shell-read-establish.js +6 -34
  79. package/dist/tools/fs/shell-read-identity.d.ts +13 -0
  80. package/dist/tools/fs/shell-read-identity.js +39 -0
  81. package/dist/tools/fs/shell-read-judge.d.ts +8 -0
  82. package/dist/tools/fs/shell-read-judge.js +7 -0
  83. package/dist/tools/fs/shell-read-plan.d.ts +5 -1
  84. package/dist/tools/fs/shell-read-plan.js +13 -21
  85. package/dist/tools/fs/shell-read-word.d.ts +16 -2
  86. package/dist/tools/fs/shell-read-word.js +40 -5
  87. package/package.json +1 -1
  88. package/test/export-surface.snapshot.json +7 -1
  89. package/wire-changes/core@7.30.1.json +154 -0
  90. package/wire-changes/core@7.31.0.json +705 -0
@@ -7,7 +7,7 @@ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
9
9
  import { killProcessTree, shutdownDebug } from "./kill-tree.js";
10
- import { scrubSecretEnv } from "../../core/secret-env.js";
10
+ import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, scrubSecretEnv } from "../../core/secret-env.js";
11
11
  import { physicalPathOfAsync } from "../../core/physical-path.js";
12
12
  import { isUncSpelling } from "../../tools/fs/safety.js";
13
13
  import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
@@ -197,13 +197,12 @@ export async function getShellConfig(customShellPath) {
197
197
  return ok({ shell: "sh", args: ["-c"] });
198
198
  }
199
199
  const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
200
- let secretScrubConsoleAnnounced = false;
201
200
  let secretScrubSeatDefectAnnounced = false;
202
201
  export function __resetSecretScrubAnnouncement() {
203
- secretScrubConsoleAnnounced = false;
204
202
  secretScrubSeatDefectAnnounced = false;
203
+ __resetSecretEnvScrubNoticeAnnouncement();
205
204
  }
206
- function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub) {
205
+ function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub, onNotice) {
207
206
  let inherited;
208
207
  if (inheritEnv === "all") {
209
208
  inherited = process.env;
@@ -230,20 +229,17 @@ function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub) {
230
229
  catch {
231
230
  }
232
231
  }
233
- else {
234
- if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
235
- secretScrubSeatDefectAnnounced = true;
236
- try {
237
- console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
238
- }
239
- catch {
240
- }
232
+ else if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
233
+ secretScrubSeatDefectAnnounced = true;
234
+ try {
235
+ console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
241
236
  }
242
- if (!secretScrubConsoleAnnounced) {
243
- secretScrubConsoleAnnounced = true;
244
- console.warn(`secret_env_scrubbed: ${findings.length} env key(s) withheld from the shell child (${findings.map((f) => f.key).join(", ")}) — once per process; wire onSecretEnvScrub for per-exec telemetry`);
237
+ catch {
245
238
  }
246
239
  }
240
+ if (!(typeof onScrub === "function" && onNotice === undefined)) {
241
+ announceSecretEnvScrub(onNotice, findings);
242
+ }
247
243
  }
248
244
  }
249
245
  return {
@@ -302,6 +298,7 @@ export class NodeExecutionEnv {
302
298
  bgCounter = 0;
303
299
  retainBackgroundProcesses;
304
300
  onSecretEnvScrub;
301
+ onNotice;
305
302
  bgSpoolDir;
306
303
  backgroundCapabilities;
307
304
  constructor(options) {
@@ -312,6 +309,7 @@ export class NodeExecutionEnv {
312
309
  this.scheduler = options.scheduler;
313
310
  this.retainBackgroundProcesses = options.retainBackgroundProcesses ?? false;
314
311
  this.onSecretEnvScrub = options.onSecretEnvScrub;
312
+ this.onNotice = options.onNotice;
315
313
  this.backgroundCapabilities = {
316
314
  supported: true,
317
315
  maxConcurrent: 8,
@@ -469,7 +467,7 @@ export class NodeExecutionEnv {
469
467
  child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
470
468
  cwd,
471
469
  detached: process.platform !== "win32",
472
- env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub),
470
+ env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub, this.onNotice),
473
471
  stdio: execSpoolFds ? ["ignore", execSpoolFds[0], execSpoolFds[1]] : ["ignore", "pipe", "pipe"],
474
472
  windowsHide: true,
475
473
  });
@@ -1221,7 +1219,7 @@ export class NodeExecutionEnv {
1221
1219
  child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
1222
1220
  cwd: options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd,
1223
1221
  detached: process.platform !== "win32",
1224
- env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub),
1222
+ env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub, this.onNotice),
1225
1223
  stdio: spoolFds ? ["ignore", spoolFds[0], spoolFds[1]] : ["ignore", "pipe", "pipe"],
1226
1224
  windowsHide: true,
1227
1225
  });
@@ -858,7 +858,7 @@ export class AgentHarness {
858
858
  await this.handleAgentEvent({ type: "agent_end", messages: [failureMessage] }, signal);
859
859
  return [failureMessage];
860
860
  }
861
- async executeTurn(turnState, text, options) {
861
+ async executeTurn(turnState, text, options, abortController) {
862
862
  let activeTurnState = turnState;
863
863
  const userMessage = createUserMessage(text, options?.images, options);
864
864
  const beforeResult = await this.emitHook({
@@ -897,12 +897,10 @@ export class AgentHarness {
897
897
  }
898
898
  return messages;
899
899
  };
900
- const abortController = new AbortController();
901
900
  const getTurnState = () => activeTurnState;
902
901
  const setTurnState = (nextTurnState) => {
903
902
  activeTurnState = nextTurnState;
904
903
  };
905
- this.runAbortController = abortController;
906
904
  const runResultPromise = (async () => {
907
905
  try {
908
906
  return await runAgentLoop(takePrompts, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace, {
@@ -938,7 +936,7 @@ export class AgentHarness {
938
936
  return mergeTruncatedOutputChain(newMessages, i);
939
937
  }
940
938
  }
941
- if (this.stopRequested)
939
+ if (this.stopRequested || abortController.signal.aborted)
942
940
  return undefined;
943
941
  if (this.trailingBatchEndedOn !== undefined)
944
942
  return this.trailingBatchEndedOn;
@@ -976,17 +974,21 @@ export class AgentHarness {
976
974
  }
977
975
  }
978
976
  this.phase = "turn";
977
+ const abortController = new AbortController();
978
+ this.runAbortController = abortController;
979
979
  const finishRunPromise = this.startRunPromise();
980
980
  try {
981
981
  await this.flushPendingSessionWrites();
982
982
  const turnState = await this.createTurnState();
983
- return await this.executeTurn(turnState, text, options);
983
+ return await this.executeTurn(turnState, text, options, abortController);
984
984
  }
985
985
  catch (error) {
986
986
  this.phase = "idle";
987
987
  throw normalizeHarnessError(error, "unknown");
988
988
  }
989
989
  finally {
990
+ if (this.runAbortController === abortController)
991
+ this.runAbortController = undefined;
990
992
  finishRunPromise();
991
993
  }
992
994
  }
@@ -2,6 +2,7 @@ import { type SessionWarmup } from "../../core/lsp-session.js";
2
2
  import { type LspChildProcess } from "./stdio-lsp-transport.js";
3
3
  import { type LspServerManager, type LspSession, type LspReadText } from "../../core/lsp.js";
4
4
  import type { ExecutionEnv } from "../../internal/harness-types.js";
5
+ import type { EngineNotice } from "../../core/engine-notice.js";
5
6
  /**
6
7
  * Default language → server command table (the DETERMINED supported set). Each value is a `command [args…]`
7
8
  * whose binary must be on the host `PATH`; absent ⇒ that language degrades. Mirrors the servers service bakes
@@ -19,6 +20,12 @@ export interface NodeLspManagerOptions {
19
20
  servers?: Record<string, string | null>;
20
21
  /** Injectable spawn (tests supply a fake server; default = {@link defaultLspSpawn} over `child_process.spawn`). */
21
22
  spawn?: LspSpawn;
23
+ /** The language server's environment for the default spawn, verbatim. Omitted ⇒ the host environment scrubbed of
24
+ * credential-shaped names (`scrubSecretEnv`, a tool child's scrub). Unread when {@link spawn} is supplied. */
25
+ env?: Record<string, string>;
26
+ /** #1081 — the withheld-keys notice sink, SAME shared once-per-process latch/code as the shell-exec
27
+ * site's `NodeExecutionEnv.onNotice`. Absent ⇒ console only. Unread when {@link spawn} is supplied. */
28
+ onNotice?: (notice: EngineNotice) => void;
22
29
  /** Read a file's CURRENT text for the session's didOpen/didChange re-sync (default = local `fs.readFile` utf8). */
23
30
  readText?: LspReadText;
24
31
  /** Resolve the server `cwd`/`rootUri` for a file (default = env.cwd ?? nearest project marker ?? file dir). */
@@ -9,6 +9,7 @@ import { TransportLspSession } from "../../core/lsp-session.js";
9
9
  import { StdioLspTransport } from "./stdio-lsp-transport.js";
10
10
  import { MAX_LSP_FILE_BYTES, SharedAbortScope, settleOnAbort } from "../../core/lsp.js";
11
11
  import { asThrownError, thrownText } from "../../core/thrown-value.js";
12
+ import { announceSecretEnvScrub, scrubSecretEnv } from "../../core/secret-env.js";
12
13
  export const DEFAULT_LSP_SERVERS = {
13
14
  typescript: "typescript-language-server --stdio",
14
15
  javascript: "typescript-language-server --stdio",
@@ -66,7 +67,7 @@ export class NodeLspManager {
66
67
  disposed = false;
67
68
  constructor(opts = {}) {
68
69
  this.servers = { ...DEFAULT_LSP_SERVERS, ...opts.servers };
69
- this.spawnFn = opts.spawn ?? defaultLspSpawn;
70
+ this.spawnFn = opts.spawn ?? lspSpawnWithEnv(opts.env, opts.onNotice);
70
71
  this.readTextFn = opts.readText ?? defaultLspReadText;
71
72
  this.resolveRootFn = opts.resolveRoot ?? defaultResolveRoot;
72
73
  this.log = opts.log ?? (() => { });
@@ -328,10 +329,17 @@ export function lspSpawnOptions(cwd) {
328
329
  ? { cwd, stdio: ["pipe", "pipe", "ignore"], shell: true }
329
330
  : { cwd, stdio: ["pipe", "pipe", "ignore"] };
330
331
  }
331
- export const defaultLspSpawn = (command, args, cwd, signal) => new Promise((resolve, reject) => {
332
+ export const defaultLspSpawn = (command, args, cwd, signal) => lspSpawnWithEnv(undefined)(command, args, cwd, signal);
333
+ const lspSpawnWithEnv = (env, onNotice) => (command, args, cwd, signal) => new Promise((resolve, reject) => {
332
334
  let child;
333
335
  try {
334
- child = spawn(command, args, lspSpawnOptions(cwd));
336
+ let scrubbedEnv = env;
337
+ if (scrubbedEnv == null) {
338
+ const findings = [];
339
+ scrubbedEnv = scrubSecretEnv(process.env, findings);
340
+ announceSecretEnvScrub(onNotice, findings);
341
+ }
342
+ child = spawn(command, args, { ...lspSpawnOptions(cwd), env: scrubbedEnv });
335
343
  }
336
344
  catch (e) {
337
345
  return reject(asThrownError(e));
package/dist/index.d.ts CHANGED
@@ -229,7 +229,7 @@ export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootA
229
229
  export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, type AdoptionStatus, type AdoptLocalDataRootOptions, type AdoptLocalDataRootResult, type AdoptionCarriageLeg, type AdoptionCarriageLegContext, type AdoptionConfigWitnessReceipt, } from "./stores/file/adoption/adopt.js";
230
230
  export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, type PersistedRuleHit, type PersistedRuleHitRule, type PersistedRuleUnreadable, type PersistedRuleCoverage, type PersistedRuleAnswer, normalizePersistedRuleHit, type Hooks, type HookToolContext, type HookInvocationIdentity, type UserPromptSubmitContext, type PostToolBatchContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, } from "./core/hooks.js";
231
231
  export { HOOK_FAMILIES, HOOKS_INVALID_CODE, type HookFamily, type WiringManifestHookEntry } from "./core/hook-seats.js";
232
- export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, MemoryProjectionError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, } from "./core/memory.js";
232
+ export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, MemoryProjectionError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, MEMORY_PROJECT_KEY_ABSENT_REASONS, type MemoryProjectKeyAbsentReason, type MemoryProjectKey, } from "./core/memory.js";
233
233
  export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, writableRootOf, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_ORIGIN_CAUSES, committedOriginOf, originEquals, consolidationExposedFrontmatter, ambiguousOriginRepresentation, type OriginClearanceRow, type OriginClearanceEvent, type OriginClearanceShadow, committedDistilledOf, distilledEquals, type MemoryEntryDistilled, type MemoryEntryDistilledInput, CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, deriveShadowedSet, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, memoryConsolidationWithheldNotice, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, type SessionMemoryStatus, DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, type CleanArmLeakFinding, type CleanArmLeakVerdict, type MemoryDistillerPurityContract, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_RUN_STOP_REASONS, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type ConsolidationRunStopReason, type RunMemoryConsolidationOptions, isInstructionEntry, type MemoryEntryOrigin, type MemoryOriginCause, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_CAPTURE_OPTOUT_NOTICE, memoryCaptureOptedOutNotice, memoryCaptureOptOutUnpersistedNotice, SESSION_CAPTURE_OPTOUT_DIR, markSessionCaptureOptOut, readSessionCaptureOptOut, listSessionCaptureOptOut, fileSessionCaptureRecordStore, type SessionCaptureOptOutRecord, type SessionCaptureOptOutMarkOutcome, type SessionCaptureRecordStore, readV2HeaderHints, type V2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, memoryExposureIndexRow, type MemoryGetDetails, type MemoryIndexDetails, type MemoryIndexRow, type CleanMemoryIndexRow, type ExposedMemoryIndexRow, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, computeMemoryBundleHash, type MemoryExportBundle, type MemoryImportReport, type MemoryExportSnapshot, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type MemoryScopeEnumeration, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
234
234
  export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, type SharedMemoryStoreProvider, type SharedMemoryStoreReader, type SharedMemoryPagedList, type SharedMemoryStoreInfo, type SharedMemoryDocumentEntry, type SharedMemorySnapshot, type SharedMemoryRequestContext, type MemoryListDetails, type MemoryReadDetails, } from "./core/shared-memory/types.js";
235
235
  export { sharedMemoryStoreContract, type SharedMemoryFixture, type SharedMemoryStoreContractHooks, } from "./core/shared-memory/contract.js";
package/dist/index.js CHANGED
@@ -179,7 +179,7 @@ export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootA
179
179
  export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, } from "./stores/file/adoption/adopt.js";
180
180
  export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, normalizePersistedRuleHit, } from "./core/hooks.js";
181
181
  export { HOOK_FAMILIES, HOOKS_INVALID_CODE } from "./core/hook-seats.js";
182
- export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, MemoryProjectionError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, } from "./core/memory.js";
182
+ export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, MemoryProjectionError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, MEMORY_PROJECT_KEY_ABSENT_REASONS, } from "./core/memory.js";
183
183
  export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, writableRootOf, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_ORIGIN_CAUSES, committedOriginOf, originEquals, consolidationExposedFrontmatter, ambiguousOriginRepresentation, committedDistilledOf, distilledEquals, CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, deriveShadowedSet, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, memoryConsolidationWithheldNotice, DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_RUN_STOP_REASONS, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, isInstructionEntry, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_CAPTURE_OPTOUT_NOTICE, memoryCaptureOptedOutNotice, memoryCaptureOptOutUnpersistedNotice, SESSION_CAPTURE_OPTOUT_DIR, markSessionCaptureOptOut, readSessionCaptureOptOut, listSessionCaptureOptOut, fileSessionCaptureRecordStore, readV2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, memoryExposureIndexRow, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, erasureSelectHash, computeMemoryBundleHash, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
184
184
  export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, } from "./core/shared-memory/types.js";
185
185
  export { sharedMemoryStoreContract, } from "./core/shared-memory/contract.js";
@@ -25,8 +25,10 @@ export declare const SHELL_LEX_DIALECT = "bash";
25
25
  /** What holds a character quoted: the four quote forms and the backslash escape, one reason each. */
26
26
  export declare const QUOTE_KINDS: readonly ["'", "\"", "$'", "$\"", "\\"];
27
27
  export type QuoteKind = (typeof QUOTE_KINDS)[number];
28
- /** The expansions this lexer names. Extended globs are absent on purpose: `bash -c` runs with `extglob` off, so
29
- * `?(…)` is a syntax error there and `(` is a group token here. */
28
+ /** The expansions this lexer names. Extended globs are not among them: `extglob` is off when `bash -c` starts, where
29
+ * `?(…)` is a syntax error and `(` is a group token here — but a text may turn it on (`shopt -s extglob` on an earlier
30
+ * line), after which bash reads `pack@(age.json)` as one pattern. A reader that plans reads treats an unquoted opener
31
+ * as a pattern it does not expand (`extglobOpenerIn`, the read station's `unreadable` floor). */
30
32
  export declare const EXPANSION_KINDS: readonly ["parameter", "command", "process", "arithmetic", "tilde", "brace", "glob"];
31
33
  export type ExpansionKind = (typeof EXPANSION_KINDS)[number];
32
34
  /** Why a word's value cannot be read off the text. Each member is a spelling whose value the text does not
@@ -242,7 +244,10 @@ export declare function argvOf(segment: LexedSegment): readonly LexedWord[];
242
244
  export declare function wordsOf(segment: LexedSegment): SegmentTokens;
243
245
  /** The substitution bodies of an UNQUOTED here-document body: `$(…)` and backticks are live there, quotes are
244
246
  * ordinary characters, a backslash protects the next character. `undefined` when a substitution never closes. */
245
- export declare function substitutionsInHeredocBody(body: string): string[] | undefined;
247
+ export declare function substitutionsInHeredocBody(body: string): {
248
+ readonly body: string;
249
+ readonly kind: "command" | "arithmetic";
250
+ }[] | undefined;
246
251
  /** Lex a command: its segments, or the one reason bash (or this lexer) cannot delimit them. */
247
252
  export declare function lexShell(source: string): ShellLex;
248
253
  /**
package/dist/shell/lex.js CHANGED
@@ -305,7 +305,7 @@ export function substitutionsInHeredocBody(body) {
305
305
  const tick = backtickBody(body, i);
306
306
  if (tick === undefined)
307
307
  return undefined;
308
- out.push(tick.body);
308
+ out.push({ body: tick.body, kind: "command" });
309
309
  i = tick.close;
310
310
  continue;
311
311
  }
@@ -313,7 +313,7 @@ export function substitutionsInHeredocBody(body) {
313
313
  const end = parenBodyEnd(body, i + 1);
314
314
  if (typeof end !== "number")
315
315
  return undefined;
316
- out.push(body.slice(i + 2, end - 1));
316
+ out.push({ body: body.slice(i + 2, end - 1), kind: body[i + 2] === "(" ? "arithmetic" : "command" });
317
317
  i = end - 1;
318
318
  }
319
319
  }
@@ -1,4 +1,4 @@
1
- import { type LexedSegment, type LexedWord, type ShellLex, type ShellSegmentConnector, type WordPiece } from "../../shell/lex.js";
1
+ import { type LexedCommand, type LexedSegment, type LexedWord, type ShellLex, type ShellSegmentConnector, type WordPiece } from "../../shell/lex.js";
2
2
  /**
3
3
  * Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
4
4
  * The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
@@ -876,6 +876,12 @@ export declare function homeExpansionOf(w: LexedWord, home: string): {
876
876
  * Elsewhere a computed word is its own unresolvable candidate, and an escape the lexer does not decode is
877
877
  * unresolved on its own (asked). */
878
878
  export declare function homeMayBeRebound(text: string): boolean;
879
+ /** {@link hasUnquotedExtglobOpener} over an already-lexed text. A here-document delimiter counts too: the lexer ends
880
+ * it at the `(` (`<<@(x)` reads as the delimiter `@` and a group), where bash with `extglob` on reads the one word
881
+ * `@(x)` — a different delimiter, so the body the lexer read as data is not the body bash reads. The token keeps no
882
+ * per-character quoting, so a delimiter quoted anywhere (`<<'E'@(X)`) counts as well: a quoted opener is bash's
883
+ * syntax error, where reading it as one costs a card and runs nothing. */
884
+ export declare function extglobOpenerIn(lexed: LexedCommand, text: string): boolean;
879
885
  /**
880
886
  * {@link classifyCompoundReadonly} with the structured verdict. The name/operator/blocking arms
881
887
  * run FIRST and unchanged, so an existing demotion keeps its exact reason and never acquires the
@@ -1510,10 +1510,14 @@ export function homeMayBeRebound(text) {
1510
1510
  }
1511
1511
  function hasUnquotedExtglobOpener(text) {
1512
1512
  const lexed = lexShell(text);
1513
- if ("parseFailure" in lexed)
1514
- return true;
1513
+ return "parseFailure" in lexed || extglobOpenerIn(lexed, text);
1514
+ }
1515
+ export function extglobOpenerIn(lexed, text) {
1515
1516
  for (const seg of lexed.segments) {
1516
- for (const tok of seg.tokens) {
1517
+ for (const [k, tok] of seg.tokens.entries()) {
1518
+ const next = seg.tokens[k + 1];
1519
+ if (tok.kind === "heredoc" && /[?*+@!]$/.test(tok.delimiter) && next?.kind === "group" && next.op === "(")
1520
+ return true;
1517
1521
  const t = tok.kind === "word" ? tok : tok.kind === "redirect" ? tok.target : undefined;
1518
1522
  if (t === undefined)
1519
1523
  continue;
@@ -35,7 +35,7 @@
35
35
  */
36
36
  import type { ExecutionEnv } from "../../internal/harness-types.js";
37
37
  import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
38
- import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js";
38
+ import { type EstablishedRead, type ShellReadBoundary } from "./shell-read-judge.js";
39
39
  /**
40
40
  * How many directories one glob operand (or one traversal root) may enumerate, and how many names it may
41
41
  * match, before the leg gives up and answers `unestablishable`.
@@ -1,6 +1,8 @@
1
1
  import { normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
2
2
  import { boundaryIsTheSandbox, canonicalPathIsAuthoritative } from "../../core/remote-env.js";
3
3
  import { resolveOperandSpellings } from "./bash-readonly-classifier.js";
4
+ import { denyFloorShadowsOf } from "./shell-read-judge.js";
5
+ import { movedReads } from "./shell-read-identity.js";
4
6
  const GLOB_METACHAR_RE = /[*?[]/;
5
7
  export const MAX_GLOB_DIRS = 256;
6
8
  export const MAX_GLOB_MATCHES = 8192;
@@ -94,7 +96,7 @@ function basesFor(candidate, boundary) {
94
96
  }
95
97
  function lexicalReadsOf(candidate, boundary) {
96
98
  if (candidate.expands === "unresolvable") {
97
- return [{ candidate, established: "unestablishable", spellings: [candidate.spelled, ...(candidate.floorSpelled !== undefined ? [candidate.floorSpelled] : [])], cause: `"${candidate.raw}" is rewritten by the shell before the program sees it` }];
99
+ return [{ candidate, established: "unestablishable", spellings: [candidate.spelled], cause: `"${candidate.raw}" is rewritten by the shell before the program sees it` }];
98
100
  }
99
101
  const bases = basesFor(candidate, boundary);
100
102
  if (bases === "unmodelled") {
@@ -130,7 +132,8 @@ function lexicalReadsOf(candidate, boundary) {
130
132
  return out;
131
133
  }
132
134
  function judgedCandidatesOf(segment) {
133
- return segment.reads.filter((c) => c.provenance !== "write_redirect_target");
135
+ const reads = segment.reads.filter((c) => c.provenance !== "write_redirect_target");
136
+ return [...reads, ...denyFloorShadowsOf(reads)];
134
137
  }
135
138
  export function establishShellReadsLexically(plan, boundary) {
136
139
  const reads = new Map();
@@ -424,38 +427,7 @@ export function createShellReadEstablisher(env, readRoots) {
424
427
  if (lexicalOnly)
425
428
  return { changed: [] };
426
429
  const again = await establishPlan(previous.plan, { ...boundary, roots: previous.roots }, previous.roots, signal);
427
- const settled = (e) => {
428
- const out = [];
429
- const walk = (p) => {
430
- for (const reads of p.reads.values())
431
- for (const r of reads)
432
- if ((r.established === "canonical" || r.established === "expansion") && r.target !== undefined)
433
- out.push(r.target);
434
- for (const n of p.nested.values())
435
- walk(n);
436
- };
437
- walk(e);
438
- return out.sort();
439
- };
440
- const before = settled(previous);
441
- const after = new Set(settled(again));
442
- const beforeSet = new Set(before);
443
- const moved = before.filter((t) => !after.has(t));
444
- const appeared = [...after].filter((t) => !beforeSet.has(t));
445
- if (moved.length === 0 && appeared.length === 0)
446
- return { changed: [] };
447
- const changed = [];
448
- const collect = (p) => {
449
- for (const reads of p.reads.values())
450
- for (const r of reads)
451
- if (r.target !== undefined && (moved.includes(r.target) || appeared.includes(r.target)))
452
- changed.push(r);
453
- for (const n of p.nested.values())
454
- collect(n);
455
- };
456
- collect(previous);
457
- collect(again);
458
- return { changed };
430
+ return { changed: movedReads(previous, again) };
459
431
  },
460
432
  };
461
433
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The pre-spawn identity comparison: which established reads of one plan resolve somewhere else now.
3
+ *
4
+ * Compared PER CANDIDATE, never as one pooled set of targets: a candidate is keyed by where it stands in the plan
5
+ * (segment, nested plan) and by what it is (deny-only or a read, provenance, spelling, expansion, raw word), and its
6
+ * set of established targets is compared with the same candidate's set from the re-establishment. A pooled set let
7
+ * one candidate's targets hide another's move — `head linkx` retargeted onto a path some other word's pattern already
8
+ * matched, or onto a home shadow's target, left the pool unchanged and the call was admitted unjudged.
9
+ */
10
+ import type { EstablishedPlan } from "./shell-read-establish.js";
11
+ import type { EstablishedRead } from "./shell-read-judge.js";
12
+ /** Every read of either establishment whose candidate's settled target set differs between the two. */
13
+ export declare function movedReads(previous: EstablishedPlan, again: EstablishedPlan): EstablishedRead[];
@@ -0,0 +1,39 @@
1
+ export function movedReads(previous, again) {
2
+ const before = targetsByCandidate(previous);
3
+ const after = targetsByCandidate(again);
4
+ const changed = [];
5
+ for (const key of new Set([...before.keys(), ...after.keys()])) {
6
+ const b = before.get(key);
7
+ const a = after.get(key);
8
+ if (b !== undefined && a !== undefined && sameTargets(b.targets, a.targets))
9
+ continue;
10
+ changed.push(...(b?.reads ?? []), ...(a?.reads ?? []));
11
+ }
12
+ return changed.filter((r) => r.target !== undefined);
13
+ }
14
+ function targetsByCandidate(e) {
15
+ const out = new Map();
16
+ const walk = (p, at) => {
17
+ p.plan.segments.forEach((segment, si) => {
18
+ for (const r of p.reads.get(segment) ?? []) {
19
+ const c = r.candidate;
20
+ const key = [at, si, c.denyOnly === true ? "floor" : "read", c.provenance, c.spelled, String(c.expands), c.raw].join("\0");
21
+ const landing = out.get(key) ?? { targets: new Set(), reads: [] };
22
+ out.set(key, landing);
23
+ landing.reads.push(r);
24
+ if ((r.established === "canonical" || r.established === "expansion") && r.target !== undefined)
25
+ landing.targets.add(r.target);
26
+ }
27
+ segment.nested.forEach((n, ni) => {
28
+ const nested = p.nested.get(n);
29
+ if (nested !== undefined)
30
+ walk(nested, `${at}/${si}.${ni}`);
31
+ });
32
+ });
33
+ };
34
+ walk(e, "");
35
+ return out;
36
+ }
37
+ function sameTargets(a, b) {
38
+ return a.size === b.size && [...a].every((t) => b.has(t));
39
+ }
@@ -173,6 +173,14 @@ export interface ReadFinding {
173
173
  * neither is a containment candidate — both still meet the deny judge, on every spelling.
174
174
  */
175
175
  export declare function judgeRead(read: EstablishedRead, boundary: ShellReadBoundary): ReadFinding;
176
+ /**
177
+ * The deny floor of every home word the plan does not place: each candidate carrying a `floorSpelled` (the word
178
+ * spelled with the DECLARED home) gets a SHADOW candidate at that spelling, established exactly as a read is — the
179
+ * canonical target, a pattern's matches (a glob the word carries unquoted), a traversal root's tree — and judged for
180
+ * deny rows only (`denyOnly`). A deny row is a floor the text's rebinding may never reach past; the shadow adds no
181
+ * other finding, so a read that is not a deny row still asks through its own unplaced candidate.
182
+ */
183
+ export declare function denyFloorShadowsOf(candidates: readonly ReadCandidate[]): ReadCandidate[];
176
184
  /** One segment's judged reads and declines, with its nested plans judged the same way. */
177
185
  export interface JudgedSegment {
178
186
  readonly segment: ShellReadSegment;
@@ -1,4 +1,6 @@
1
1
  import { hasWin32AliasedComponent, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
2
+ import { globInWord } from "./bash-readonly-classifier.js";
3
+ import { readOfText } from "./shell-read-word.js";
2
4
  export const SHELL_READ_FINDINGS = ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"];
3
5
  export const SHELL_READ_SEATS = ["bash_off", "bash_classify", "readonly", "rejudge"];
4
6
  export const SHELL_READ_FINDING_RANK = {
@@ -32,6 +34,8 @@ export function judgeRead(read, boundary) {
32
34
  }
33
35
  }
34
36
  const containment = containmentOf(read, boundary);
37
+ if (read.candidate.denyOnly === true)
38
+ return { read, kind: "none", containment };
35
39
  const provenance = read.candidate.provenance;
36
40
  if (provenance === "env_assignment" || (provenance === "program_path" && read.candidate.expands !== "unresolvable"))
37
41
  return { read, kind: "none", containment };
@@ -39,6 +43,9 @@ export function judgeRead(read, boundary) {
39
43
  return { read, kind: "unestablishable", containment };
40
44
  return { read, kind: containment === "outside" ? "outside_roots" : "none", containment };
41
45
  }
46
+ export function denyFloorShadowsOf(candidates) {
47
+ return candidates.flatMap((c) => (c.floorSpelled === undefined ? [] : [{ spelled: c.floorSpelled, raw: c.raw, provenance: c.provenance, expands: globInWord(readOfText(c.raw).word) ? "glob" : false, bases: [], denyOnly: true }]));
48
+ }
42
49
  function containmentOf(read, boundary) {
43
50
  if (boundary.face === "open")
44
51
  return "open";
@@ -65,6 +65,8 @@ export interface ReadCandidate {
65
65
  /** A home word the plan does not place (the text may rebind `HOME`, or it sits in a value): spelled with the DECLARED
66
66
  * home. A deny row is a floor: the judge reads it for deny rows ONLY, never to establish a read. */
67
67
  readonly floorSpelled?: string;
68
+ /** A deny-floor shadow of a `floorSpelled` spelling, established as a read and judged for deny rows only. */
69
+ readonly denyOnly?: true;
68
70
  }
69
71
  /** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
70
72
  export interface SegmentReach {
@@ -123,4 +125,6 @@ export interface ShellReadPlan {
123
125
  readonly strayRedirection: boolean;
124
126
  };
125
127
  }
126
- export declare function planShellReads(command: string, decl: ShellReadDeclarations, depth?: number, homeRebound?: boolean): ShellReadPlan;
128
+ /** Plan every read candidate of one command (the contract is this module's header). `arithmetic` = the text is an
129
+ * `$((…))` body, which bash evaluates and never globs (no extended-glob floor). */
130
+ export declare function planShellReads(command: string, decl: ShellReadDeclarations, depth?: number, homeRebound?: boolean, arithmetic?: boolean): ShellReadPlan;
@@ -1,5 +1,5 @@
1
- import { lexShell, PARSE_FAILURE_SENTENCE, UNRESOLVED_SENTENCE, substitutionsInHeredocBody, literalTildeSpelling, } from "../../shell/lex.js";
2
- import { compoundArrayElements, dropDefinitionHeads, expandsOf, readOfText, readsOf, rewritten, spelledWithHome } from "./shell-read-word.js";
1
+ import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTildeSpelling, } from "../../shell/lex.js";
2
+ import { compoundArrayElements, dropDefinitionHeads, expandsOf, readOfText, readsOf, rewritten, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
3
3
  import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
4
4
  import { ASSIGNMENT_VALUE_NOT_PATHS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, homeMayBeRebound, attachedOptionPayloads, resolveOperandLexically, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
5
5
  import { isAbsolutePathForm } from "./safety.js";
@@ -18,17 +18,7 @@ export const READ_CANDIDATE_PROVENANCES = [
18
18
  ];
19
19
  const NESTING_MAX = 4;
20
20
  const BASE_CANDIDATE_MAX = 256;
21
- function firstUnresolvedSentence(lexed) {
22
- for (const segment of lexed.segments) {
23
- for (const token of segment.tokens) {
24
- const word = token.kind === "word" ? token : token.kind === "redirect" ? token.target : undefined;
25
- if (word?.unresolved !== undefined)
26
- return UNRESOLVED_SENTENCE[word.unresolved];
27
- }
28
- }
29
- return undefined;
30
- }
31
- export function planShellReads(command, decl, depth = 0, homeRebound = false) {
21
+ export function planShellReads(command, decl, depth = 0, homeRebound = false, arithmetic = false) {
32
22
  const shape = { grouped: false, backgrounded: false, strayRedirection: false };
33
23
  if (depth > NESTING_MAX)
34
24
  return { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape };
@@ -36,8 +26,8 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false) {
36
26
  if ("parseFailure" in lexed)
37
27
  return { segments: [], unreadable: PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason], shape };
38
28
  const home = homeRebound || homeMayBeRebound(command) ? undefined : decl.homeDir;
39
- const unreadable = firstUnresolvedSentence(lexed);
40
- const nestedPlan = (text) => planShellReads(text, decl, depth + 1, home === undefined);
29
+ const unreadable = unreadableSentenceOf(lexed, command, !arithmetic);
30
+ const nestedPlan = (text, arithmetic = false) => planShellReads(text, decl, depth + 1, home === undefined, arithmetic);
41
31
  const projected = lexed.segments.map((segment) => projectSegment(segment));
42
32
  const facts = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, home, nestedPlan));
43
33
  shape.grouped = projected.some((p) => p.grouped);
@@ -86,7 +76,7 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false) {
86
76
  if (move === undefined || baseLost)
87
77
  return;
88
78
  if (typeof move.target === "string" || move.target.expands !== false) {
89
- loseBase(reachable, typeof move.target === "string" ? undefined : move.target.floorSpelled);
79
+ loseBase(reachable, typeof move.target !== "string" ? move.target.floorSpelled : move.target === "home" ? decl.homeDir : undefined);
90
80
  return;
91
81
  }
92
82
  const target = move.target.spelled;
@@ -214,8 +204,10 @@ function segmentFacts(segment, p, decl, home, nestedPlan) {
214
204
  }
215
205
  function segmentFactsRead(segment, p, decl, home, nestedPlan) {
216
206
  const nested = [];
207
+ const arithmetic = segment.tokens.flatMap((t) => (t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined)?.pieces ?? []).flatMap((q) => (q.expansion?.kind === "arithmetic" && q.expansion.body !== undefined && isArithmeticBody(q.expansion.body) ? [q.expansion.body] : []));
208
+ const count = (list, body) => list.filter((b) => b === body).length;
217
209
  for (const body of segment.substitutions)
218
- nested.push(nestedPlan(body));
210
+ nested.push(nestedPlan(body, count(arithmetic, body) === count(segment.substitutions, body)));
219
211
  let unreadableHeredoc = false;
220
212
  for (const t of segment.tokens) {
221
213
  if (t.kind !== "heredoc" || t.quoted)
@@ -225,8 +217,8 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
225
217
  unreadableHeredoc = true;
226
218
  continue;
227
219
  }
228
- for (const body of inner)
229
- nested.push(nestedPlan(body));
220
+ for (const s of inner)
221
+ nested.push(nestedPlan(s.body, s.kind === "arithmetic" && isArithmeticBody(s.body)));
230
222
  }
231
223
  if (unreadableHeredoc)
232
224
  nested.push({ segments: [], unreadable: "a here-document body carries a substitution this lexer cannot delimit", shape: { grouped: false, backgrounded: false, strayRedirection: false } });
@@ -470,7 +462,7 @@ function dedupe(reads) {
470
462
  const seen = new Set();
471
463
  const out = [];
472
464
  for (const c of reads) {
473
- const key = `${c.provenance}\u0000${c.spelled}\u0000${c.expands}`;
465
+ const key = `${c.provenance}\u0000${c.spelled}\u0000${c.expands}\u0000${c.denyOnly === true}`;
474
466
  if (seen.has(key))
475
467
  continue;
476
468
  seen.add(key);
@@ -515,7 +507,7 @@ function pushAssignment(reads, w, provenance, opensValue = true) {
515
507
  reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w) });
516
508
  if (a.form === "compound")
517
509
  for (const e of compoundArrayElements(a.value))
518
- reads.push({ spelled: e.folded, raw: e.raw, provenance, expands: e.expands });
510
+ reads.push({ spelled: e.folded, raw: e.raw, provenance, expands: e.expands, ...(e.denyOnly === true ? { denyOnly: true } : {}) });
519
511
  }
520
512
  function unlistedWords(reads, toks, from, launcherIndices, provenance, program) {
521
513
  const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks.map((r) => r.word));