@sema-agent/core 7.26.3 → 7.28.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 (78) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/agents/builtin-agents.d.ts +6 -15
  3. package/dist/agents/cascade.d.ts +29 -33
  4. package/dist/agents/observer.d.ts +18 -20
  5. package/dist/agents/subagent.d.ts +58 -70
  6. package/dist/agents/tool-filter.d.ts +10 -12
  7. package/dist/agents/verify.d.ts +44 -49
  8. package/dist/brain/input-too-long.d.ts +10 -12
  9. package/dist/brain/media-degrade.d.ts +4 -5
  10. package/dist/brain/reasoning.d.ts +10 -12
  11. package/dist/brain/repetition.d.ts +7 -9
  12. package/dist/brain/request-params.d.ts +20 -25
  13. package/dist/brain/route-adjudicator.d.ts +4 -4
  14. package/dist/brain/route-conformance.d.ts +2 -2
  15. package/dist/brain/status-sink.d.ts +12 -14
  16. package/dist/brain/terminal-cause.d.ts +12 -15
  17. package/dist/brain/timeout.d.ts +17 -19
  18. package/dist/core/auto-mode-directive.d.ts +6 -4
  19. package/dist/core/auto-mode-directive.js +1 -5
  20. package/dist/core/engine-notice.d.ts +13 -6
  21. package/dist/core/memory-engine/consolidation.d.ts +11 -15
  22. package/dist/core/memory-engine/engine.d.ts +45 -54
  23. package/dist/core/memory-engine/tools.d.ts +17 -23
  24. package/dist/core/permission-rule-model.js +5 -3
  25. package/dist/core/runner/contracts.d.ts +31 -7
  26. package/dist/core/runner/denial-limit-arms.d.ts +17 -15
  27. package/dist/core/runner/denial-limit-arms.js +4 -1
  28. package/dist/core/runner/prepare-gate-stations.d.ts +1 -2
  29. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -2
  30. package/dist/core/runner/prepare-policy-chain.d.ts +1 -2
  31. package/dist/core/runner/prepare-task.js +1 -1
  32. package/dist/core/runner/run-leg.js +5 -1
  33. package/dist/core/tool-policy.d.ts +23 -2
  34. package/dist/core/tool-policy.js +106 -103
  35. package/dist/index.d.ts +6 -1
  36. package/dist/shell/lex.d.ts +279 -0
  37. package/dist/shell/lex.js +1253 -0
  38. package/dist/stores/file/background-agent-store.d.ts +4 -0
  39. package/dist/stores/file/background-agent-store.js +10 -0
  40. package/dist/stores/file/checkpoint-store.d.ts +3 -0
  41. package/dist/stores/file/checkpoint-store.js +18 -0
  42. package/dist/stores/file/corrupt-read-sink.d.ts +20 -0
  43. package/dist/stores/file/corrupt-read-sink.js +15 -0
  44. package/dist/stores/file/file-history-store.d.ts +1 -2
  45. package/dist/stores/file/file-history-store.js +3 -8
  46. package/dist/stores/file/fs-atomic.d.ts +8 -3
  47. package/dist/stores/file/fs-atomic.js +19 -2
  48. package/dist/stores/file/index.js +4 -4
  49. package/dist/stores/file/mailbox-store.d.ts +3 -0
  50. package/dist/stores/file/mailbox-store.js +24 -18
  51. package/dist/stores/file/permission-rule-store.js +18 -1
  52. package/dist/stores/file/session-policy-store.d.ts +17 -1
  53. package/dist/stores/file/session-policy-store.js +3 -12
  54. package/dist/stores/file/session-store.d.ts +1 -5
  55. package/dist/stores/file/session-store.js +9 -14
  56. package/dist/stores/file/shared-ledger.d.ts +18 -0
  57. package/dist/stores/file/shared-ledger.js +16 -2
  58. package/dist/stores/file/tool-result-store.d.ts +9 -1
  59. package/dist/stores/file/tool-result-store.js +17 -2
  60. package/dist/stores/file/workflow-journal-store.d.ts +10 -1
  61. package/dist/stores/file/workflow-journal-store.js +8 -12
  62. package/dist/stores/file/workflow-run-store.d.ts +3 -0
  63. package/dist/stores/file/workflow-run-store.js +10 -0
  64. package/dist/tools/artifact/local-stub.js +16 -1
  65. package/dist/tools/fs/bash-program-position.d.ts +2 -4
  66. package/dist/tools/fs/bash-program-position.js +2 -1
  67. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
  68. package/dist/tools/fs/bash-readonly-classifier.js +1 -1
  69. package/dist/tools/fs/shell-read-check.js +1 -1
  70. package/dist/tools/fs/shell-read-establish.js +11 -1
  71. package/dist/tools/fs/shell-read-plan.d.ts +7 -2
  72. package/dist/tools/fs/shell-read-plan.js +48 -10
  73. package/package.json +2 -2
  74. package/test/export-surface.snapshot.json +3 -1
  75. package/wire-changes/core@7.27.0.json +152 -0
  76. package/wire-changes/core@7.28.0.json +594 -0
  77. package/dist/tools/fs/bash-lexer.d.ts +0 -152
  78. package/dist/tools/fs/bash-lexer.js +0 -627
@@ -3,12 +3,15 @@ import { readFileSync, readdirSync, unlinkSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance, ToolResultRefConflictError, } from "../../core/tool-result-store.js";
5
5
  import { ensureDir, sanitizePathComponent, writeThenLink } from "./fs-atomic.js";
6
+ import { guardCorruptSink } from "./corrupt-read-sink.js";
6
7
  import { assertAdoptionBootGate } from "./adoption/marker.js";
7
8
  export class FileToolResultStore {
8
9
  dir;
9
- constructor(root) {
10
+ discloseCorrupt;
11
+ constructor(root, opts) {
10
12
  assertAdoptionBootGate(root, "FileToolResultStore");
11
13
  this.dir = join(root, "tool-results");
14
+ this.discloseCorrupt = guardCorruptSink(opts?.onCorruptRead);
12
15
  ensureDir(this.dir);
13
16
  }
14
17
  locate(ref) {
@@ -59,15 +62,26 @@ export class FileToolResultStore {
59
62
  return this.readOwnerFile(ref, { loud: false });
60
63
  }
61
64
  readOwnerFile(ref, opts) {
65
+ const ownerPath = (() => {
66
+ try {
67
+ return this.ownerPathFor(ref);
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ })();
73
+ if (ownerPath === undefined)
74
+ return undefined;
62
75
  let raw;
63
76
  try {
64
- raw = readFileSync(this.ownerPathFor(ref), "utf8");
77
+ raw = readFileSync(ownerPath, "utf8");
65
78
  }
66
79
  catch (err) {
67
80
  if (err.code === "ENOENT")
68
81
  return undefined;
69
82
  if (opts.loud)
70
83
  throw err;
84
+ this.discloseCorrupt?.({ path: ownerPath, reason: `unreadable owner sidecar (${err.code ?? "unknown"}) — read as unowned` });
71
85
  return undefined;
72
86
  }
73
87
  const parsed = parseOwnerRecord(raw);
@@ -76,6 +90,7 @@ export class FileToolResultStore {
76
90
  if (opts.loud) {
77
91
  throw new Error(`tool-result store: owner record for ref ${JSON.stringify(ref)} is present but unreadable — refusing to write over it`);
78
92
  }
93
+ this.discloseCorrupt?.({ path: ownerPath, reason: "not-an-object: owner sidecar is present but is not an owner record — read as unowned" });
79
94
  return undefined;
80
95
  }
81
96
  writeOwnerFile(ref, provenance) {
@@ -1,4 +1,5 @@
1
1
  import { type WorkflowJournalEntry, type WorkflowJournalStore, type ResumeClaimArgs } from "../../core/workflow-journal-store.js";
2
+ import { type CorruptReadInfo } from "./corrupt-read-sink.js";
2
3
  /**
3
4
  * SVC-2 — the shared size bound for a journaled workflow-agent entry, so ALL backends answer an oversize
4
5
  * TaskResult with the SAME refusal (no dialect divergence). Inlined from service's `workflow-journal-limits` on
@@ -46,6 +47,12 @@ export declare const RESUME_CLAIM_SWEEP_INTERVAL_MS = 60000;
46
47
  * stays well clear of that handover rather than adding a second unlinker to it.
47
48
  */
48
49
  export declare const RESUME_CLAIM_REAP_GRACE_MS: number;
50
+ export interface FileWorkflowJournalStoreOptions {
51
+ /** Disclosure seat for a ledger line this store DROPPED on replay (not JSON, not a record, or a record
52
+ * with none of this ledger's members) — a resume that silently finds "nothing to replay" re-runs the
53
+ * work with no signal. Never fires on a missing ledger; swallow-guarded. */
54
+ onCorruptRead?: (info: CorruptReadInfo) => void;
55
+ }
49
56
  export declare class FileWorkflowJournalStore implements WorkflowJournalStore {
50
57
  private readonly fsyncEnabled;
51
58
  private readonly dir;
@@ -65,7 +72,9 @@ export declare class FileWorkflowJournalStore implements WorkflowJournalStore {
65
72
  * ledger per append (a journaled result can be MBs; a many-agent workflow would go O(n²) on disk). One replay
66
73
  * discovers it; every later append compares against the cache. */
67
74
  private get scopes();
68
- constructor(root: string, fsyncEnabled?: boolean);
75
+ /** The swallow-guarded corrupt-read seat (undefined = the old silent tolerance). */
76
+ private readonly discloseCorrupt;
77
+ constructor(root: string, fsyncEnabled?: boolean, opts?: FileWorkflowJournalStoreOptions);
69
78
  private pathFor;
70
79
  /** CC local form: the run's on-disk journal path for the notification diagnostics line.
71
80
  * Traversal-shaped ids (LLM-controlled on some paths) degrade to undefined, never throw. */
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync
2
2
  import { createHash } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
  import { callKeyOrdinal } from "../../core/workflow-journal-store.js";
5
- import { AppendLog } from "./fs-atomic.js";
5
+ import { AppendLog, readJsonlRecords } from "./fs-atomic.js";
6
+ import { guardCorruptSink } from "./corrupt-read-sink.js";
6
7
  import { canonicalStoreKey, sanitizePathComponent, writeThenLink } from "./fs-atomic.js";
7
8
  import { assertAdoptionBootGate } from "./adoption/marker.js";
8
9
  import { assertJournalEntryFits } from "../../core/workflow-journal-store.js";
@@ -32,8 +33,10 @@ export class FileWorkflowJournalStore {
32
33
  get scopes() {
33
34
  return this.shared.scopes;
34
35
  }
35
- constructor(root, fsyncEnabled = true) {
36
+ discloseCorrupt;
37
+ constructor(root, fsyncEnabled = true, opts) {
36
38
  this.fsyncEnabled = fsyncEnabled;
39
+ this.discloseCorrupt = guardCorruptSink(opts?.onCorruptRead);
37
40
  assertAdoptionBootGate(root, "FileWorkflowJournalStore");
38
41
  this.dir = join(root, "workflow-journal");
39
42
  mkdirSync(this.dir, { recursive: true, mode: 0o700 });
@@ -72,18 +75,11 @@ export class FileWorkflowJournalStore {
72
75
  return undefined;
73
76
  let scope;
74
77
  const byOrdinal = new Map();
75
- for (const line of readFileSync(path, "utf8").split("\n")) {
76
- if (!line.trim())
78
+ for (const rec of readJsonlRecords(path, this.discloseCorrupt)) {
79
+ if (typeof rec.callKey !== "string" || typeof rec.scope !== "string" || !Number.isFinite(rec.ordinal)) {
80
+ this.discloseCorrupt?.({ path, reason: "missing-required: a journal line carries no callKey/scope pair (skipped; the rest of the replay is intact)" });
77
81
  continue;
78
- let rec;
79
- try {
80
- rec = JSON.parse(line);
81
82
  }
82
- catch {
83
- continue;
84
- }
85
- if (typeof rec.callKey !== "string" || typeof rec.scope !== "string")
86
- continue;
87
83
  scope = scope ?? rec.scope;
88
84
  if (rec.scope !== scope)
89
85
  continue;
@@ -1,10 +1,13 @@
1
1
  import { type WorkflowRunListByScopeOptions, type WorkflowRunStore, type WorkflowRunSummary } from "../../core/workflow-run-store.js";
2
2
  import type { WorkflowRun } from "../../orchestration/workflow.js";
3
+ import { type CorruptReadInfo } from "./corrupt-read-sink.js";
3
4
  export interface FileWorkflowRunStoreOptions {
4
5
  /** When false, a ledger append is NOT fsync'd (the observability store is non-engine-critical). Default true. */
5
6
  fsync?: boolean;
6
7
  /** Compact the ledger into a snapshot once it exceeds this many events (then truncate). Default 1000. */
7
8
  compactEvery?: number;
9
+ /** The observability ledger's seat; see {@link import("./shared-ledger.js").LedgerPaths.onCorruptRead}. */
10
+ onCorruptRead?: (info: CorruptReadInfo) => void;
8
11
  }
9
12
  export declare class FileWorkflowRunStore implements WorkflowRunStore {
10
13
  private readonly fsyncEnabled;
@@ -2,7 +2,9 @@ import { join } from "node:path";
2
2
  import { assertRetentionPolicy } from "../../core/retention-policy.js";
3
3
  import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdate, queryWorkflowRuns, } from "../../core/workflow-run-store.js";
4
4
  import { SharedLedgerTable } from "./shared-ledger.js";
5
+ import { guardCorruptSink } from "./corrupt-read-sink.js";
5
6
  import { assertAdoptionBootGate } from "./adoption/marker.js";
7
+ const isRunRow = (x) => x !== null && typeof x === "object" && typeof x.id === "string";
6
8
  const runLedgers = new SharedLedgerTable({
7
9
  label: "workflow-run ledger",
8
10
  keyOf: (run) => run.id,
@@ -13,6 +15,13 @@ const runLedgers = new SharedLedgerTable({
13
15
  }
14
16
  runs.set(ev.id, ev.run);
15
17
  },
18
+ judgeRow: isRunRow,
19
+ judgeEvent: (x) => {
20
+ const ev = x;
21
+ if (typeof ev.id !== "string")
22
+ return false;
23
+ return ev.t === "delete" ? true : isRunRow(ev.run);
24
+ },
16
25
  });
17
26
  export class FileWorkflowRunStore {
18
27
  fsyncEnabled;
@@ -30,6 +39,7 @@ export class FileWorkflowRunStore {
30
39
  this.ledger = runLedgers.acquire({
31
40
  dir,
32
41
  tmpDir: join(root, "tmp"),
42
+ ...(opts.onCorruptRead !== undefined ? { onCorruptRead: guardCorruptSink(opts.onCorruptRead) } : {}),
33
43
  ledgerPath: join(dir, "runs.jsonl"),
34
44
  snapshotPath: join(dir, "runs.snapshot.jsonl"),
35
45
  });
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
- import {} from "../../core/artifact-host.js";
4
+ import { ARTIFACT_HOST_FAILURE_CODES, } from "../../core/artifact-host.js";
5
5
  import { physicalPathOf } from "../../core/physical-path.js";
6
6
  import { acquireStoreDirLock } from "../../stores/file/fs-atomic.js";
7
7
  import { normalizePublishedPath } from "./artifact-tool.js";
@@ -25,6 +25,18 @@ const isRow = (x, key) => {
25
25
  return false;
26
26
  return r.files.every((f) => f !== null && typeof f === "object" && typeof f.path === "string" && normalizePublishedPath(f.path).ok && Number.isSafeInteger(f.bytes));
27
27
  };
28
+ const isAttempt = (x) => {
29
+ if (x === null || typeof x !== "object" || Array.isArray(x))
30
+ return false;
31
+ const a = x;
32
+ if ("ref" in a) {
33
+ const r = a.ref;
34
+ return r !== null && typeof r === "object" && typeof r.url === "string" && typeof r.slug === "string" && typeof r.version === "string";
35
+ }
36
+ if (typeof a.code !== "string" || !ARTIFACT_HOST_FAILURE_CODES.includes(a.code))
37
+ return false;
38
+ return a.code === "conflict" ? typeof a.slug === "string" && typeof a.currentVersion === "string" : typeof a.message === "string";
39
+ };
28
40
  function parseLedger(text, path) {
29
41
  let parsed;
30
42
  try {
@@ -40,6 +52,9 @@ function parseLedger(text, path) {
40
52
  for (const [key, row] of Object.entries(l.artifacts))
41
53
  if (!isRow(row, key))
42
54
  throw new Error(`artifact stub ledger is corrupt (bad artifact row): ${path}`);
55
+ for (const attempt of Object.values(l.attempts))
56
+ if (!isAttempt(attempt))
57
+ throw new Error(`artifact stub ledger is corrupt (bad attempt record): ${path}`);
43
58
  return { v: 1, artifacts: { ...l.artifacts }, attempts: { ...l.attempts } };
44
59
  }
45
60
  function fsyncDirBestEffort(dir) {
@@ -8,7 +8,7 @@
8
8
  * readable, minted into one sentence by {@link programPositionRefusalSentence}. Reading less than this reader
9
9
  * does (an option skipped, a chain member missed) is how one face used to vouch for a program another refused.
10
10
  */
11
- import type { SegmentTokens } from "./bash-lexer.js";
11
+ import { SHELL_CONTROL_PREFIXES, type SegmentTokens } from "../../shell/lex.js";
12
12
  /** The verdict every unresolvable / out-of-root / unbounded finding ends on. The findings differ in what
13
13
  * they found; the consequence is one consequence, and a copy that drifts reads as a second rule. */
14
14
  export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
@@ -35,9 +35,7 @@ export declare function writtenAssignmentPrefix(token: string): {
35
35
  readonly appends: boolean;
36
36
  readonly settlesWholeName: boolean;
37
37
  } | undefined;
38
- /** Shell control words that stand BEFORE a command: transparent for the boundary walk (the command behind them is
39
- * judged). */
40
- export declare const SHELL_CONTROL_PREFIXES: ReadonlySet<string>;
38
+ export { SHELL_CONTROL_PREFIXES };
41
39
  /** Shell block words that name no command of their own (a loop head, a block close, a case head). */
42
40
  export declare const SHELL_BLOCK_WORDS: ReadonlySet<string>;
43
41
  /** A control word is a keyword only when bash sees it BARE: `'if'` / `"do"` / `\\!` are command names (a program
@@ -1,3 +1,4 @@
1
+ import { SHELL_CONTROL_PREFIXES } from "../../shell/lex.js";
1
2
  export const NOT_AUTO_ALLOWED = "— not auto-allowed";
2
3
  export function programBasename(word) {
3
4
  return word.includes("/") ? (word.split("/").pop() ?? word) : word;
@@ -12,7 +13,7 @@ export function writtenAssignmentPrefix(token) {
12
13
  const bracket = written.indexOf("[");
13
14
  return { prefix, name: bracket < 0 ? written : written.slice(0, bracket), appends, settlesWholeName: !appends && bracket < 0 };
14
15
  }
15
- export const SHELL_CONTROL_PREFIXES = new Set(["!", "if", "then", "elif", "else", "while", "until", "do", "time"]);
16
+ export { SHELL_CONTROL_PREFIXES };
16
17
  export const SHELL_BLOCK_WORDS = new Set(["for", "select", "case", "esac", "fi", "done", "function", "coproc", "[[", "]]", "in"]);
17
18
  export function isBareControlWord(toks, i, words) {
18
19
  const word = toks.folded[i];
@@ -1,4 +1,4 @@
1
- import { type SegmentTokens, type ShellSegmentConnector } from "./bash-lexer.js";
1
+ import { type SegmentTokens, type ShellSegmentConnector } 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
@@ -31,7 +31,7 @@ import { type SegmentTokens, type ShellSegmentConnector } from "./bash-lexer.js"
31
31
  * value-taking options defeat the stdin-floor operand count without a per-verb option-value model
32
32
  * — those are a follow-on slice, not a silent drop.
33
33
  */
34
- export type { ShellSegmentConnector } from "./bash-lexer.js";
34
+ export type { ShellSegmentConnector } from "../../shell/lex.js";
35
35
  export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
36
36
  /**
37
37
  * The verbs the CLASSIFY face admits ON TOP of the shared list ({@link bashClassifyDefaultAllow}) —
@@ -1,5 +1,5 @@
1
1
  import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf } from "./safety.js";
2
- import { lexShellText, literalTildeSpelling, ansiCQuoteClose, dollarOpensQuote, quoteMask, wordsOf, } from "./bash-lexer.js";
2
+ import { lexShellText, literalTildeSpelling, ansiCQuoteClose, dollarOpensQuote, quoteMask, wordsOf, } from "../../shell/lex.js";
3
3
  import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRefusalSentence, ASSIGNMENT_PREFIX, readProgramPosition, writtenAssignmentPrefix, } from "./bash-program-position.js";
4
4
  export const BASH_READONLY_DEFAULT_ALLOW = [
5
5
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
@@ -8,7 +8,7 @@ import { planShellReads } from "./shell-read-plan.js";
8
8
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
9
9
  import { allFindings, decideShellRead, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
10
10
  import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
11
- import { wordsOf, lexShellText } from "./bash-lexer.js";
11
+ import { wordsOf, lexShellText } from "../../shell/lex.js";
12
12
  function containmentDetails(paths) {
13
13
  return { type: "readonly_out_of_root", code: "readonly_out_of_root", paths };
14
14
  }
@@ -98,7 +98,17 @@ function lexicalReadsOf(candidate, boundary) {
98
98
  }
99
99
  const bases = basesFor(candidate, boundary);
100
100
  if (bases === "unmodelled") {
101
- return [{ candidate, established: "unestablishable", spellings: [candidate.spelled], cause: `"${candidate.raw}" is relative to a working directory this command's text does not settle` }];
101
+ const floors = candidate.floorBases ?? [boundary.cwd ?? boundary.roots[0] ?? ""];
102
+ const spellings = [candidate.spelled];
103
+ for (const floorBase of floors) {
104
+ const viaFloor = floorBase === "" ? undefined : resolveOperandSpellings(floorBase, candidate.spelled, boundary.homeDir);
105
+ if (viaFloor === undefined)
106
+ continue;
107
+ spellings.push(viaFloor.folded);
108
+ if (viaFloor.spelled !== undefined)
109
+ spellings.push(viaFloor.spelled);
110
+ }
111
+ return [{ candidate, established: "unestablishable", spellings, cause: `"${candidate.raw}" is relative to a working directory this command's text does not settle` }];
102
112
  }
103
113
  const out = [];
104
114
  const seen = new Set();
@@ -4,7 +4,7 @@
4
4
  * The read boundary today re-derives "which words may be read" at several sites (the compound walk, the
5
5
  * containment-only gate, the refused-command whitelist, the execute-time recheck), each with its own idea
6
6
  * of launchers, option slots, redirections and re-entry. This module is the single extraction those sites
7
- * are to consume: a pure, zero-I/O function over the lenient lexer (`bash-lexer.ts`) and the program-position
7
+ * are to consume: a pure, zero-I/O function over the lenient lexer (`src/shell/lex.ts`) and the program-position
8
8
  * reader (`bash-program-position.ts`), returning every path word a command MAY read, each tagged with its
9
9
  * PROVENANCE (why it is a candidate) and the working-directory candidates it is relative to. Establishment
10
10
  * (`shell-read-establish.ts`) and judgment are downstream; nothing here decides allow/ask/deny.
@@ -26,7 +26,7 @@
26
26
  * otherwise loses the base for what follows.
27
27
  * Every ambiguity resolves toward MORE candidates, never fewer.
28
28
  */
29
- import { type LenientConnector, type LexedRedirect, type LexedWord } from "./bash-lexer.js";
29
+ import { type LenientConnector, type LexedRedirect, type LexedWord } from "../../shell/lex.js";
30
30
  import { type ShellScanDecline } from "./bash-program-position.js";
31
31
  /** WHY a word is a read candidate — a closed set, registered in `docs/CLOSED-SETS.md`. */
32
32
  export declare const READ_CANDIDATE_PROVENANCES: readonly ["operand", "option_value", "stdin_redirect", "write_redirect_target", "env_assignment", "unlisted_word", "traversal_root", "cd_target", "program_path", "script_slot_file"];
@@ -55,6 +55,11 @@ export interface ReadCandidate {
55
55
  * EMPTY for an absolute or home-rooted spelling; `"unmodelled"` for a relative word in a segment whose base the
56
56
  * text does not settle (an unfollowable move before it, a loop this parser cannot count) — it cannot be placed. */
57
57
  readonly bases: readonly string[] | "unmodelled";
58
+ /** Only beside `bases: "unmodelled"`: the working directories the text was certainly standing in when it LOST
59
+ * track (the reachable candidate set at the unprovable move; the declared cwd when nothing had moved yet).
60
+ * A deny row is a floor, so the judge resolves the spelling against these for deny rows ONLY — never to
61
+ * establish a read. `cd sub && cd $'\x2e' && cat <deny row under sub>` (bash: `cd .`) is refused through it. */
62
+ readonly floorBases?: readonly string[];
58
63
  }
59
64
  /** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
60
65
  export interface SegmentReach {
@@ -1,4 +1,4 @@
1
- import { lexShellText, substitutionsInHeredocBody, wordsOf, literalTildeSpelling, SUBSTITUTION_PLACEHOLDER, } from "./bash-lexer.js";
1
+ import { lexShell, PARSE_FAILURE_SENTENCE, UNRESOLVED_SENTENCE, UNRESOLVED_VIEW, substitutionsInHeredocBody, wordsOf, literalTildeSpelling, SUBSTITUTION_PLACEHOLDER, } from "../../shell/lex.js";
2
2
  import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, writtenAssignmentPrefix, } from "./bash-program-position.js";
3
3
  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, assignmentWordPayload, attachedOptionPayloads, foldHomeVariableSpelling, resolveOperandLexically, hasUnquotedExpansionMetachar, hasUnquotedGlobMetachar, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
4
4
  import { isAbsolutePathForm } from "./safety.js";
@@ -16,14 +16,25 @@ export const READ_CANDIDATE_PROVENANCES = [
16
16
  ];
17
17
  const NESTING_MAX = 4;
18
18
  const BASE_CANDIDATE_MAX = 256;
19
+ function firstRefusedUnresolvedSentence(lexed) {
20
+ for (const segment of lexed.segments) {
21
+ for (const token of segment.tokens) {
22
+ const word = token.kind === "word" ? token : token.kind === "redirect" ? token.target : undefined;
23
+ if (word?.unresolved !== undefined && UNRESOLVED_VIEW[word.unresolved] === "refuse")
24
+ return UNRESOLVED_SENTENCE[word.unresolved];
25
+ }
26
+ }
27
+ return undefined;
28
+ }
19
29
  export function planShellReads(command, decl, depth = 0) {
20
30
  const shape = { grouped: false, backgrounded: false, strayRedirection: false };
21
31
  if (depth > NESTING_MAX)
22
32
  return { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape };
23
33
  const homeReassigned = /(^|[\s;&|(){}`])(?:export\s+)?HOME\+?=/.test(command);
24
- const lexed = lexShellText(foldHomeVariableSpelling(command, homeReassigned ? undefined : decl.homeDir), { mode: "lenient" });
25
- if ("reject" in lexed)
26
- return { segments: [], unreadable: lexed.reject, shape };
34
+ const lexed = lexShell(foldHomeVariableSpelling(command, homeReassigned ? undefined : decl.homeDir), { mode: "lenient" });
35
+ if ("parseFailure" in lexed)
36
+ return { segments: [], unreadable: PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason], shape };
37
+ const unreadable = firstRefusedUnresolvedSentence(lexed);
27
38
  const nestedPlan = (text) => planShellReads(text, decl, depth + 1);
28
39
  const projected = lexed.segments.map((segment) => projectSegment(segment));
29
40
  const facts = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, nestedPlan));
@@ -34,6 +45,11 @@ export function planShellReads(command, decl, depth = 0) {
34
45
  const unmodelled = facts.map(() => false);
35
46
  let candidates = [{ base: decl.cwd, skippedCd: -1 }];
36
47
  let baseLost = false;
48
+ let floorBases;
49
+ const loseBase = (reachable) => {
50
+ baseLost = true;
51
+ floorBases ??= [...new Set(reachable.map((c) => c.base))];
52
+ };
37
53
  let andRunStart = 0;
38
54
  let crossedAllAnd = true;
39
55
  let crossedAnyOr = false;
@@ -68,7 +84,7 @@ export function planShellReads(command, decl, depth = 0) {
68
84
  if (move === undefined || baseLost)
69
85
  return;
70
86
  if (typeof move.target === "string" || move.target.expands !== false) {
71
- baseLost = true;
87
+ loseBase(reachable);
72
88
  return;
73
89
  }
74
90
  const target = move.target.spelled;
@@ -78,7 +94,7 @@ export function planShellReads(command, decl, depth = 0) {
78
94
  if (c.skippedCd < andRunStart) {
79
95
  const moved = joinBase(c.base, target, decl.homeDir);
80
96
  if (moved === undefined) {
81
- baseLost = true;
97
+ loseBase(reachable);
82
98
  return;
83
99
  }
84
100
  branched.push({ base: moved, skippedCd: c.skippedCd });
@@ -93,7 +109,7 @@ export function planShellReads(command, decl, depth = 0) {
93
109
  }
94
110
  const next = [...byBase.values()];
95
111
  if (next.length > BASE_CANDIDATE_MAX)
96
- baseLost = true;
112
+ loseBase(reachable);
97
113
  else
98
114
  candidates = next;
99
115
  };
@@ -116,7 +132,7 @@ export function planShellReads(command, decl, depth = 0) {
116
132
  if (frame.beats === undefined) {
117
133
  for (let k = frame.bodyStart; k < si; k++)
118
134
  unmodelled[k] = true;
119
- baseLost = true;
135
+ loseBase(candidates.filter((c) => c.skippedCd < andRunStart));
120
136
  continue;
121
137
  }
122
138
  for (let beat = 1; beat < frame.beats && !baseLost; beat++) {
@@ -133,7 +149,11 @@ export function planShellReads(command, decl, depth = 0) {
133
149
  if (f.empty)
134
150
  continue;
135
151
  const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]];
136
- const withBases = (c) => ({ ...c, bases: relativeSpelling(c.spelled) ? bases : [] });
152
+ const floor = unmodelled[si] && floorBases !== undefined && floorBases.length > 0 ? floorBases : undefined;
153
+ const withBases = (c) => {
154
+ const relative = relativeSpelling(c.spelled);
155
+ return { ...c, bases: relative ? bases : [], ...(relative && floor !== undefined ? { floorBases: floor } : {}) };
156
+ };
137
157
  segments.push({
138
158
  text: f.text,
139
159
  connector: f.connector,
@@ -151,7 +171,7 @@ export function planShellReads(command, decl, depth = 0) {
151
171
  reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false },
152
172
  });
153
173
  }
154
- return { segments, shape };
174
+ return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) };
155
175
  }
156
176
  function projectSegment(segment) {
157
177
  const tokens = [...segment.tokens];
@@ -178,6 +198,24 @@ function projectSegment(segment) {
178
198
  };
179
199
  }
180
200
  function segmentFacts(segment, p, decl, nestedPlan) {
201
+ const facts = segmentFactsRead(segment, p, decl, nestedPlan);
202
+ const unresolvedRaw = new Set();
203
+ for (const t of segment.tokens) {
204
+ const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
205
+ if (w?.unresolved !== undefined && UNRESOLVED_VIEW[w.unresolved] === "refuse")
206
+ unresolvedRaw.add(w.raw);
207
+ }
208
+ if (unresolvedRaw.size === 0)
209
+ return facts;
210
+ const unsettle = (c) => (unresolvedRaw.has(c.raw) && c.expands !== "unresolvable" ? { ...c, expands: "unresolvable" } : c);
211
+ const move = facts.baseMove;
212
+ return {
213
+ ...facts,
214
+ reads: facts.reads.map(unsettle),
215
+ ...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}),
216
+ };
217
+ }
218
+ function segmentFactsRead(segment, p, decl, nestedPlan) {
181
219
  const nested = [];
182
220
  for (const body of segment.substitutions)
183
221
  nested.push(nestedPlan(body));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.26.3",
3
+ "version": "7.28.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -52,7 +52,7 @@
52
52
  "pretest": "npm run gate:debts",
53
53
  "test": "vitest run --exclude \"**/*.local.test.ts\"",
54
54
  "test:watch": "vitest",
55
- "gate:skips": "vitest run --exclude \"**/node_modules/**\" --exclude \"**/*.local.test.ts\" --reporter=default --reporter=json --outputFile.json=.skip-report.json && node scripts/verify-skip-baseline.mjs --report .skip-report.json",
55
+ "gate:skips": "vitest run --exclude \"**/node_modules/**\" --exclude \"**/*.local.test.ts\" --reporter=default --reporter=json --outputFile.json=.skip-report.json && node scripts/verify-skip-baseline.mjs --report .skip-report.json --full",
56
56
  "gate:live": "node scripts/run-live-gate.mjs",
57
57
  "smoke": "tsx src/examples/smoke.ts",
58
58
  "eval": "tsx eval/run.ts",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
3
  "_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
4
- "count": 2449,
4
+ "count": 2450,
5
5
  "exports": {
6
6
  "A2ATaskState": "type",
7
7
  "A2ATaskStateReversal": "type",
@@ -84,6 +84,7 @@
84
84
  "AggregateBudgetOptions": "interface",
85
85
  "AnthropicBrainConfig": "interface",
86
86
  "AppendLog": "class",
87
+ "ApprovalSeatOutcome": "type",
87
88
  "ApproverPosture": "type",
88
89
  "ArtifactAction": "type",
89
90
  "ArtifactCallContext": "interface",
@@ -2535,6 +2536,7 @@
2535
2536
  "AggregateBudgetOptions": "advanced",
2536
2537
  "AnthropicBrainConfig": "advanced",
2537
2538
  "AppendLog": "advanced",
2539
+ "ApprovalSeatOutcome": "advanced",
2538
2540
  "ApproverPosture": "advanced",
2539
2541
  "ArtifactAction": "advanced",
2540
2542
  "ArtifactCallContext": "advanced",