@sema-agent/core 2.12.0 → 3.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 (115) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +1 -0
  2. package/dist/agents/agent-transcript-tool.js +1 -1
  3. package/dist/agents/cascade.d.ts +4 -0
  4. package/dist/agents/cascade.js +54 -9
  5. package/dist/agents/repair-loop.d.ts +2 -0
  6. package/dist/agents/repair-loop.js +101 -2
  7. package/dist/agents/retain-ledger.d.ts +18 -2
  8. package/dist/agents/retain-ledger.js +37 -8
  9. package/dist/agents/roster-store.d.ts +4 -0
  10. package/dist/agents/roster-store.js +22 -1
  11. package/dist/agents/teacher.js +4 -2
  12. package/dist/agents/team.js +7 -4
  13. package/dist/agents/verify.d.ts +15 -2
  14. package/dist/agents/verify.js +36 -1
  15. package/dist/bin/sema-tb.js +2 -2
  16. package/dist/brain/circuit-breaker.d.ts +1 -0
  17. package/dist/brain/circuit-breaker.js +26 -9
  18. package/dist/brain/degrading.js +13 -1
  19. package/dist/brain/failover.js +2 -0
  20. package/dist/brain/repetition.js +10 -1
  21. package/dist/brain/routing.js +5 -1
  22. package/dist/brain/status-sink.js +5 -1
  23. package/dist/brain/stream-engine.js +4 -1
  24. package/dist/brain/tool-call-repair.js +32 -14
  25. package/dist/core/auto-compaction.js +17 -7
  26. package/dist/core/auto-promote.js +2 -1
  27. package/dist/core/checkpoint-store.d.ts +1 -0
  28. package/dist/core/checkpoint-store.js +28 -10
  29. package/dist/core/consolidate-scope.js +4 -3
  30. package/dist/core/context-edit.js +3 -0
  31. package/dist/core/context-guard.js +3 -0
  32. package/dist/core/git-worktree-env.d.ts +6 -1
  33. package/dist/core/git-worktree-env.js +17 -1
  34. package/dist/core/hooks.d.ts +1 -0
  35. package/dist/core/hooks.js +40 -2
  36. package/dist/core/lsp-diagnostics.js +13 -7
  37. package/dist/core/lsp-protocol.d.ts +1 -1
  38. package/dist/core/lsp-protocol.js +20 -8
  39. package/dist/core/lsp-session.d.ts +12 -2
  40. package/dist/core/lsp-session.js +114 -49
  41. package/dist/core/lsp.d.ts +13 -1
  42. package/dist/core/lsp.js +64 -9
  43. package/dist/core/mcp.d.ts +1 -0
  44. package/dist/core/mcp.js +4 -3
  45. package/dist/core/memory-engine/data-plane.js +4 -0
  46. package/dist/core/memory-engine/engine.d.ts +2 -1
  47. package/dist/core/memory-engine/engine.js +82 -7
  48. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  49. package/dist/core/memory-engine/file-backend.js +13 -3
  50. package/dist/core/memory-engine/layout.js +12 -1
  51. package/dist/core/memory.js +3 -0
  52. package/dist/core/permission-rules.d.ts +2 -1
  53. package/dist/core/permission-rules.js +24 -3
  54. package/dist/core/runner/active-skill-scope.js +34 -6
  55. package/dist/core/runner/assemble-result.d.ts +1 -1
  56. package/dist/core/runner/assemble-result.js +0 -1
  57. package/dist/core/runner/prepare-task.js +23 -3
  58. package/dist/core/runner/runtask.js +48 -30
  59. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  60. package/dist/core/runner/tool-disclosure.js +26 -7
  61. package/dist/core/runner/usage-accounting.d.ts +6 -0
  62. package/dist/core/runner/usage-accounting.js +9 -8
  63. package/dist/core/secret-env.js +5 -1
  64. package/dist/core/sensitive-path-policy.js +9 -3
  65. package/dist/core/session-store.js +15 -4
  66. package/dist/core/skill-tool-specifier.d.ts +8 -0
  67. package/dist/core/skill-tool-specifier.js +58 -0
  68. package/dist/core/skills-directory.d.ts +1 -1
  69. package/dist/core/skills-directory.js +16 -4
  70. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  71. package/dist/core/task-registry-monitor.js +7 -1
  72. package/dist/core/tool-policy.js +65 -17
  73. package/dist/core/trace.d.ts +1 -0
  74. package/dist/core/types.d.ts +3 -5
  75. package/dist/core/with-retry.js +0 -1
  76. package/dist/engine/compaction/compaction.d.ts +1 -0
  77. package/dist/engine/compaction/compaction.js +14 -5
  78. package/dist/engine/lsp/frame-decoder.js +6 -3
  79. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  80. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  81. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  82. package/dist/index.d.ts +1 -1
  83. package/dist/internal/harness.d.ts +1 -1
  84. package/dist/internal/harness.js +1 -1
  85. package/dist/internal/llm.d.ts +1 -1
  86. package/dist/orchestration/workflow-governance.js +43 -9
  87. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  88. package/dist/orchestration/workflow-script-store.js +34 -4
  89. package/dist/orchestration/workflow.js +10 -6
  90. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  91. package/dist/prompt-assembly/artifact-store.js +39 -24
  92. package/dist/prompt-assembly/assemble.js +38 -7
  93. package/dist/prompt-assembly/epoch.js +19 -2
  94. package/dist/prompt-assembly/event-registry.js +3 -3
  95. package/dist/prompt-assembly/explain.js +1 -1
  96. package/dist/prompt-assembly/tool-catalog.js +16 -1
  97. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  98. package/dist/stores/file/fs-atomic.d.ts +3 -0
  99. package/dist/stores/file/fs-atomic.js +53 -8
  100. package/dist/stores/file/index.d.ts +1 -0
  101. package/dist/stores/file/index.js +7 -0
  102. package/dist/stores/file/session-store.d.ts +2 -0
  103. package/dist/stores/file/session-store.js +28 -1
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  105. package/dist/tools/fs/bash-readonly-classifier.js +57 -8
  106. package/dist/tools/fs/fs-bash.d.ts +1 -1
  107. package/dist/tools/fs/fs-bash.js +93 -5
  108. package/dist/tools/loop-tick.js +1 -1
  109. package/dist/tools/monitor.js +55 -9
  110. package/dist/tools/scheduler-tools.js +9 -3
  111. package/dist/tools/web.d.ts +8 -2
  112. package/dist/tools/web.js +46 -17
  113. package/dist/tools/worktree.d.ts +2 -0
  114. package/dist/tools/worktree.js +233 -143
  115. package/package.json +5 -2
@@ -2,6 +2,7 @@ import { uuidv7 } from "../../internal/harness.js";
2
2
  import { firstSentence } from "../memory.js";
3
3
  import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
4
4
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
5
+ import { MEMORY_INDEX_FILENAME } from "./file-backend.js";
5
6
  import { scopeDirName } from "./layout.js";
6
7
  export const REMOTE_HARVEST_PER_FILE_BYTES = 100 * 1024;
7
8
  export const REMOTE_MASS_DELETION_FUSE_RATIO = 0.5;
@@ -97,6 +98,9 @@ export function harvestFilesToPatches(baseline, files, opts = {}) {
97
98
  out.rejections.push({ path: f.relPath, code: "outside_root", reason: "file path escapes the memory root (containment gate, fail-closed)" });
98
99
  continue;
99
100
  }
101
+ const baseName = f.relPath.split("/").pop() ?? f.relPath;
102
+ if (baseName === MEMORY_INDEX_FILENAME || !baseName.endsWith(".md") || baseName.startsWith("."))
103
+ continue;
100
104
  if (inReadonlyDir(f.relPath)) {
101
105
  if (base === undefined) {
102
106
  out.rejections.push({ path: f.relPath, code: "readonly_layer", reason: "a file inside a read-only inherited layer's directory is not writable from this session" });
@@ -56,9 +56,10 @@ export declare class MemoryEngine {
56
56
  };
57
57
  harvest(handle: MemorySessionHandle): Promise<HarvestReport>;
58
58
  private harvestCore;
59
- rebaseline(handle: MemorySessionHandle): Promise<void>;
59
+ rebaseline(handle: MemorySessionHandle, keepBaseline?: ReadonlySet<string>): Promise<void>;
60
60
  private rebuildIndex;
61
61
  private committedContentFor;
62
+ private gateDerivedIndex;
62
63
  private quarantineFile;
63
64
  private readonlyDirNamesUnderRoot;
64
65
  private writeIfChanged;
@@ -147,6 +147,18 @@ export class MemoryEngine {
147
147
  handle.baseIds.set(path, entry.id);
148
148
  }
149
149
  }
150
+ const indexGate = this.gateDerivedIndex(handle);
151
+ if (indexGate !== undefined) {
152
+ try {
153
+ enqueueMemoryAnnouncement(this.controlDir, {
154
+ kind: "gate",
155
+ at: this.now(),
156
+ items: [`memory index rejected and rebuilt: ${inlineUntrusted(indexGate.reason)}`],
157
+ });
158
+ }
159
+ catch {
160
+ }
161
+ }
150
162
  const indexText = this.rebuildIndex(handle, headers, { write: writeScope !== null });
151
163
  handle.indexBaselineLines = countIndexLines(indexText);
152
164
  handle.indexText = indexText;
@@ -364,6 +376,7 @@ export class MemoryEngine {
364
376
  basePathById.set(id, p);
365
377
  const patches = [];
366
378
  const pendingProjections = [];
379
+ const idsAddedThisHarvest = new Set();
367
380
  let processed = 0;
368
381
  for (let i = 0; i < kept.length; i++) {
369
382
  const f = kept[i];
@@ -465,7 +478,28 @@ export class MemoryEngine {
465
478
  patches.push({ op: "update", id: parsed.id, entry: { ...entry, id: parsed.id, rev: computeEntryRev({ id: parsed.id, frontmatter: fm, body: parsed.body }) }, ...(revByBasePath.get(renamedFrom) !== undefined ? { baseRev: revByBasePath.get(renamedFrom) } : {}) });
466
479
  continue;
467
480
  }
468
- pendingProjections.push({ path: f.canonical, entry, needed: minted || needsCompletion(parsed, fm) });
481
+ let reboundId;
482
+ if (parsed.id !== undefined && (renamedFrom !== undefined || idsAddedThisHarvest.has(parsed.id))) {
483
+ reboundId = parsed.id;
484
+ const idless = serializeEntryFile({ id: "", frontmatter: fm, body: parsed.body }).replace(/^id:[ \t]*$\r?\n/m, "");
485
+ try {
486
+ writeFileSync(f.canonical, idless, "utf8");
487
+ }
488
+ catch (err) {
489
+ report.rejections.push({
490
+ path: rel,
491
+ code: "filename",
492
+ reason: `memory write blocked: this file carries the entry id of another memory file (a copy is not a rename) and its frontmatter could not be rewritten (${err instanceof Error ? err.message : String(err)}) — remove the "id:" line so it can be committed as a new entry`,
493
+ });
494
+ continue;
495
+ }
496
+ id = uuidv7();
497
+ entry.id = id;
498
+ entry.rev = computeEntryRev(entry);
499
+ report.warnings.push(`${rel}: frontmatter id ${reboundId} already has a projection (${renamedFrom !== undefined ? relative(handle.memoryDir, renamedFrom) : "another new file in this harvest"}) — a copy is not a rename, so this file was committed as a NEW entry with a fresh id; the existing entry was left untouched`);
500
+ }
501
+ pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || needsCompletion(parsed, fm) });
502
+ idsAddedThisHarvest.add(entry.id);
469
503
  patches.push({ op: "add", id: entry.id, entry });
470
504
  }
471
505
  let patchReport;
@@ -503,12 +537,15 @@ export class MemoryEngine {
503
537
  const drained = this.backend.drainInboundFindings?.();
504
538
  if (drained !== undefined && drained.length > 0)
505
539
  report.inboundFindings = drained;
540
+ const indexRejection = this.gateDerivedIndex(handle);
541
+ if (indexRejection !== undefined)
542
+ report.rejections.push(indexRejection);
506
543
  const headers = await this.backend.listHeaders([...new Set([...handle.scopes, writeScope])]);
507
544
  handle.indexText = this.rebuildIndex(handle, headers, { write: true }, report.warnings);
508
- await this.rebaseline(handle);
545
+ await this.rebaseline(handle, new Set(report.degraded?.pending ?? []));
509
546
  return report;
510
547
  }
511
- async rebaseline(handle) {
548
+ async rebaseline(handle, keepBaseline = new Set()) {
512
549
  const writeScope = handle.writeScope;
513
550
  if (writeScope === null)
514
551
  return;
@@ -535,12 +572,17 @@ export class MemoryEngine {
535
572
  if (id === undefined)
536
573
  continue;
537
574
  const stub = stubs.get(path);
538
- const rev = revOfText(text, id);
575
+ const relPath = relative(handle.memoryDir, path);
576
+ const deferred = keepBaseline.has(relPath);
577
+ const priorRev = deferred ? handle.baseRevs.get(path) : undefined;
578
+ const priorId = deferred ? handle.baseIds.get(path) : undefined;
579
+ const rev = priorRev ?? revOfText(text, priorId ?? id);
580
+ const baselineId = priorId ?? id;
539
581
  next.push({
540
582
  path,
541
- relPath: relative(handle.memoryDir, path),
583
+ relPath,
542
584
  scope: writeScope,
543
- id,
585
+ id: baselineId,
544
586
  slug: f.slug,
545
587
  rev,
546
588
  stub: stub !== undefined && stub.rev === rev,
@@ -548,7 +590,7 @@ export class MemoryEngine {
548
590
  });
549
591
  fileToScope.set(path, writeScope);
550
592
  baseRevs.set(path, rev);
551
- baseIds.set(path, id);
593
+ baseIds.set(path, baselineId);
552
594
  }
553
595
  handle.materialized = next;
554
596
  handle.fileToScope = fileToScope;
@@ -650,6 +692,39 @@ export class MemoryEngine {
650
692
  return viaBackend;
651
693
  return readSafe(join(this.controlDir, "shadow", `${id}.md`));
652
694
  }
695
+ gateDerivedIndex(handle) {
696
+ if (handle.writeScope === null)
697
+ return undefined;
698
+ const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
699
+ const text = readSafe(indexPath);
700
+ if (text === undefined || text === "")
701
+ return undefined;
702
+ const finding = scanMemoryWrite(text).find((f) => f.code === "secret");
703
+ if (finding === undefined)
704
+ return undefined;
705
+ let detail = "";
706
+ try {
707
+ const dest = join(this.controlDir, "quarantine", `${this.now()}-${MEMORY_INDEX_FILENAME}`);
708
+ mkdirSync(dirname(dest), { recursive: true });
709
+ writeFileSync(dest, text, "utf8");
710
+ }
711
+ catch (err) {
712
+ detail = `; quarantine capture failed: ${err instanceof Error ? err.message : String(err)}`;
713
+ }
714
+ let cleared = false;
715
+ try {
716
+ writeFileSync(indexPath, "", "utf8");
717
+ cleared = true;
718
+ }
719
+ catch (err) {
720
+ detail = `${detail}; the derived index could NOT be cleared: ${err instanceof Error ? err.message : String(err)}`;
721
+ }
722
+ return {
723
+ path: MEMORY_INDEX_FILENAME,
724
+ code: finding.code,
725
+ reason: `memory index blocked: ${finding.reason} (${cleared ? "index cleared and rebuilt from entry frontmatter; the offending prose was captured to quarantine" : "index still carries the offending content"})${detail}`,
726
+ };
727
+ }
653
728
  quarantineFile(path, content) {
654
729
  const dest = join(this.controlDir, "quarantine", `${this.now()}-${basename(path)}`);
655
730
  let captured;
@@ -30,6 +30,7 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
30
30
  private loadLedger;
31
31
  private saveLedger;
32
32
  private recoverJournal;
33
+ private txnInFlight;
33
34
  private sweepStagedFiles;
34
35
  private excludedSubdirNames;
35
36
  private scopeDir;
@@ -79,10 +79,10 @@ export class FileMemoryEngineBackend {
79
79
  this.controlPlaneRoot = opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), dir);
80
80
  ensureDirExists(dir);
81
81
  ensureDirExists(this.controlPlaneRoot);
82
- this.recoverJournal();
82
+ this.recoverJournal({ unlocked: true });
83
83
  }
84
84
  checkControlPlane() {
85
- this.recoverJournal();
85
+ this.recoverJournal({ unlocked: true });
86
86
  this.ledger = undefined;
87
87
  this.loadLedger();
88
88
  registeredScopes(this.controlPlaneRoot);
@@ -164,8 +164,10 @@ export class FileMemoryEngineBackend {
164
164
  return;
165
165
  atomicWriteFileSync(join(this.controlPlaneRoot, LEDGER_FILE), `${JSON.stringify(this.ledger, null, 2)}\n`);
166
166
  }
167
- recoverJournal() {
167
+ recoverJournal(opts = {}) {
168
168
  const jp = join(this.controlPlaneRoot, JOURNAL_FILE);
169
+ if (opts.unlocked && this.txnInFlight())
170
+ return;
169
171
  const raw = readSafe(jp);
170
172
  if (raw !== undefined) {
171
173
  let journal;
@@ -205,6 +207,14 @@ export class FileMemoryEngineBackend {
205
207
  }
206
208
  this.sweepStagedFiles();
207
209
  }
210
+ txnInFlight() {
211
+ try {
212
+ return this.now() - statSync(this.txnLockDir()).mtimeMs < TXN_LOCK_STALE_MS;
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ }
208
218
  sweepStagedFiles() {
209
219
  const walk = (d, depth) => {
210
220
  let names;
@@ -216,11 +216,22 @@ function acquireSidecarLock(lockDir, opts = {}) {
216
216
  try {
217
217
  mkdirSync(lockDir);
218
218
  const token = `${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
219
+ let fd;
219
220
  try {
220
- writeSync(openSync(join(lockDir, "owner"), "w"), token);
221
+ fd = openSync(join(lockDir, "owner"), "w");
222
+ writeSync(fd, token);
221
223
  }
222
224
  catch {
223
225
  }
226
+ finally {
227
+ if (fd !== undefined) {
228
+ try {
229
+ closeSync(fd);
230
+ }
231
+ catch {
232
+ }
233
+ }
234
+ }
224
235
  return token;
225
236
  }
226
237
  catch {
@@ -32,6 +32,9 @@ const SECRET_PATTERNS = [
32
32
  { label: "slack_token", re: /\bxox[baprs]-(?:[A-Za-z0-9]+-){1,3}[A-Za-z0-9]{16,}\b/ },
33
33
  { label: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
34
34
  { label: "private_key", re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/ },
35
+ { label: "jwt", re: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{16,}\b/ },
36
+ { label: "azure_storage_account_key", re: /AccountKey=[A-Za-z0-9+/]{40,}={0,2}/ },
37
+ { label: "bearer_token", re: /\bAuthorization:\s*Bearer\s+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{20,}=*/ },
35
38
  ];
36
39
  export function detectSecret(text) {
37
40
  if (!text)
@@ -11,7 +11,7 @@ export interface ParsedPermissionRule {
11
11
  }
12
12
  export interface PermissionRuleIssue {
13
13
  rule: string;
14
- code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.file_glob" | "unsupported.mcp_paren";
14
+ code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.file_glob" | "unsupported.mcp_paren" | "unsupported.unknown_param";
15
15
  message: string;
16
16
  }
17
17
  export interface PermissionRuleCaps {
@@ -30,5 +30,6 @@ export interface PermissionRulePolicyOptions {
30
30
  }
31
31
  export declare function parsePermissionRule(rule: string): ParsedPermissionRule;
32
32
  export declare function wildcardMatch(pattern: string, value: string): boolean;
33
+ export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
33
34
  export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps">): PermissionRuleIssue[];
34
35
  export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): ToolPolicy;
@@ -90,6 +90,18 @@ const PRIMARY_FIELDS = {
90
90
  WebSearch: new Set(["query"]),
91
91
  };
92
92
  const FILE_TOOLS = new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Grep", "Glob"]);
93
+ const FILE_TOOL_PARAMS = {
94
+ Read: new Set(["file_path", "path", "offset", "limit", "pages"]),
95
+ Edit: new Set(["file_path", "path", "old_string", "new_string", "replace_all"]),
96
+ Write: new Set(["file_path", "path", "content"]),
97
+ NotebookEdit: new Set(["notebook_path", "path", "cell_id", "cell_type", "edit_mode", "new_source"]),
98
+ Grep: new Set([
99
+ "pattern", "path", "glob", "type", "output_mode", "head_limit", "max_results", "offset",
100
+ "multiline", "context", "ignore_case", "-i", "-n", "-o", "-A", "-B", "-C",
101
+ ]),
102
+ Glob: new Set(["pattern", "path", "max_results"]),
103
+ };
104
+ export const fileToolParamVocabulary = (canonicalTool) => FILE_TOOL_PARAMS[canonicalTool];
93
105
  function countStars(s) {
94
106
  let n = 0;
95
107
  for (const c of s)
@@ -151,9 +163,18 @@ function compile(rules, caps, primaryFieldGeneric) {
151
163
  continue;
152
164
  }
153
165
  }
154
- else if (FILE_TOOLS.has(toolName) && colon <= 0) {
155
- bad(text, "unsupported.file_glob", "file-tool glob rules (`Edit(src/**)`) are the tool-specific lane — not supported by the generic param DSL");
156
- continue;
166
+ else if (FILE_TOOLS.has(toolName)) {
167
+ if (colon <= 0) {
168
+ bad(text, "unsupported.file_glob", "file-tool glob rules (`Edit(src/**)`) are the tool-specific lane — not supported by the generic param DSL");
169
+ continue;
170
+ }
171
+ const paramPart = content.slice(0, colon).trim();
172
+ const vocabulary = FILE_TOOL_PARAMS[toolName];
173
+ if (vocabulary !== undefined && !vocabulary.has(paramPart)) {
174
+ bad(text, "unsupported.unknown_param", `"${paramPart}" is not a parameter of ${toolName} (it carries ${[...vocabulary].sort().join(", ")}) — the rule would compile and never match. ` +
175
+ `For a path glob use the tool-specific lane (\`${toolName}(src/**)\` is not the generic param DSL); a Windows path such as \`${toolName}(C:/foo)\` parses as the param "C".`);
176
+ continue;
177
+ }
157
178
  }
158
179
  if (colon <= 0) {
159
180
  bad(text, "invalid.param_split", "param-level ruleContent must be `param:pattern`");
@@ -1,5 +1,6 @@
1
1
  import { canonicalizeTarget, fileArgPath } from "../../tools/fs/safety.js";
2
2
  import { canonicalToolName } from "../tool-name-aliases.js";
3
+ import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
3
4
  export class ActiveSkillScope {
4
5
  frames = [];
5
6
  push(frame) {
@@ -47,12 +48,19 @@ export function createActiveSkillScopePolicy(opts) {
47
48
  };
48
49
  }
49
50
  const manifests = frames.flatMap((f) => (f.kind === "manifest" ? [f.manifest] : []));
50
- let allowed;
51
- for (const m of manifests) {
52
- const here = new Set(m.allowTools.map(canonicalToolName));
53
- allowed = allowed === undefined ? here : new Set([...allowed].filter((t) => here.has(t)));
54
- }
55
- if (!allowed || !allowed.has(toolName)) {
51
+ const perFrame = manifests.map((m) => {
52
+ const byName = new Map();
53
+ for (const raw of m.allowTools) {
54
+ const entry = parseSkillToolEntry(raw);
55
+ const bucket = byName.get(entry.name);
56
+ if (bucket)
57
+ bucket.push(entry);
58
+ else
59
+ byName.set(entry.name, [entry]);
60
+ }
61
+ return { manifest: m, byName };
62
+ });
63
+ if (perFrame.length === 0 || perFrame.some((f) => !f.byName.has(toolName))) {
56
64
  const ids = manifests.map((m) => m.lineageId).join(", ");
57
65
  return {
58
66
  action: "deny",
@@ -60,6 +68,26 @@ export function createActiveSkillScopePolicy(opts) {
60
68
  decisionReason: "safety",
61
69
  };
62
70
  }
71
+ for (const frame of perFrame) {
72
+ const alternatives = frame.byName.get(toolName) ?? [];
73
+ const rejections = [];
74
+ let admitted = false;
75
+ for (const entry of alternatives) {
76
+ const rejection = skillSpecifierRejection(entry, req.args);
77
+ if (rejection === undefined) {
78
+ admitted = true;
79
+ break;
80
+ }
81
+ rejections.push(rejection);
82
+ }
83
+ if (!admitted) {
84
+ return {
85
+ action: "deny",
86
+ reason: `tool "${req.toolName}" is narrowed by skill manifest "${frame.manifest.lineageId}": ${rejections.join("; ")}`,
87
+ decisionReason: "safety",
88
+ };
89
+ }
90
+ }
63
91
  const pathConstrainingActive = manifests.some((m) => m.allowPaths && m.allowPaths.length > 0);
64
92
  if (pathConstrainingActive && !PATH_WRITE_TOOLS.has(toolName)) {
65
93
  const eff = toolEffects?.get(toolName) ?? "write";
@@ -10,7 +10,7 @@ export interface Stats {
10
10
  cacheWriteTokensLong: number;
11
11
  outputTokens: number;
12
12
  cacheHitRate?: number;
13
- totalInputTokens?: number;
13
+ totalInputTokens: number;
14
14
  costMicroUsd: number;
15
15
  nested?: {
16
16
  tokens: number;
@@ -35,7 +35,6 @@ function assistantText(msg) {
35
35
  export function assembleResult(spec, sessionId, final, stats, flags) {
36
36
  const taskId = spec.taskId ?? sessionId;
37
37
  const text = final ? assistantText(final) : "";
38
- stats.totalInputTokens = stats.promptTokens;
39
38
  if (!flags.unpricedSpend) {
40
39
  const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
41
40
  const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
@@ -432,6 +432,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
432
432
  }
433
433
  }
434
434
  const taskScope = internals?.registryScope ?? spec.principal ?? "default";
435
+ const offloadScope = spec.principal ?? "default";
436
+ if (internals?.registryScope !== undefined && internals.registryScope !== offloadScope) {
437
+ deps.onError?.(new Error(`offload/tool-result namespace note: this run mounts in registry domain "${internals.registryScope}" but its offloaded tool results live under the TRUST identity "${offloadScope}" (principal${spec.principal === undefined ? " absent → default" : ""}). Before 2.13.0 the registry domain named this namespace too; if a deployment used registryScope as its ref-tenancy boundary, that boundary is now the principal — pass one, or read refs under "${offloadScope}".`), { phase: "config", sessionId });
438
+ }
435
439
  defaultTaskRegistry.maybeGc();
436
440
  const forgetOnThrow = () => forgetQuietly(sessions, sessionId);
437
441
  const extraBodyCollisions = reservedCollisions(model.extraBody, reservedFor(model.api));
@@ -456,7 +460,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
456
460
  const offloadEnabled = Number.isFinite(offloadThreshold) && offloadThreshold > 0;
457
461
  const rawOffloadStore = offloadEnabled ? (deps.toolResultStore ?? new InMemoryToolResultStore()) : undefined;
458
462
  const offloadStore = rawOffloadStore instanceof RunnerSharedToolResultStore
459
- ? new ScopedToolResultStore(rawOffloadStore, taskScope)
463
+ ? new ScopedToolResultStore(rawOffloadStore, offloadScope)
460
464
  : rawOffloadStore;
461
465
  const offloadReachableToolsRef = {};
462
466
  const maybeOffload = (tool, perTool) => {
@@ -1384,6 +1388,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1384
1388
  owner: hostTaskId,
1385
1389
  scope: taskScope,
1386
1390
  ...(sessionId !== undefined ? { sessionId } : {}),
1391
+ enrichCtx: enrichSpecToolCtx,
1387
1392
  })));
1388
1393
  }
1389
1394
  }
@@ -1915,6 +1920,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1915
1920
  throw e;
1916
1921
  }
1917
1922
  const registry = buildDeferredRegistry(deferred, tools);
1923
+ let activationChain = Promise.resolve();
1924
+ const serializeActivation = (section) => {
1925
+ const p = activationChain.then(section);
1926
+ activationChain = p.then(() => undefined, () => undefined);
1927
+ return p;
1928
+ };
1918
1929
  const directCallFor = (name) => {
1919
1930
  if (spec.deferSelfResolve === false)
1920
1931
  return undefined;
@@ -1930,7 +1941,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1930
1941
  };
1931
1942
  },
1932
1943
  ...(executionMode !== undefined ? { executionMode } : {}),
1933
- activate: async () => {
1944
+ activate: async () => serializeActivation(async () => {
1934
1945
  if (activeTools.has(name))
1935
1946
  return undefined;
1936
1947
  activeTools.add(name);
@@ -1942,7 +1953,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1942
1953
  throw e;
1943
1954
  }
1944
1955
  return listingRideRef.current?.([name]);
1945
- },
1956
+ }),
1946
1957
  };
1947
1958
  };
1948
1959
  const placeholders = new Map([...registry.values()].map((i) => [i.name, createPlaceholderTool(i, directCallFor(i.name))]));
@@ -2016,6 +2027,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2016
2027
  listingRide: (newly) => listingRideRef.current?.(newly),
2017
2028
  mountedNames: callableToolNames,
2018
2029
  directCallEnabled: spec.deferSelfResolve !== false,
2030
+ serializeActivation,
2019
2031
  });
2020
2032
  harnessTools = buildToolList(activeTools);
2021
2033
  }
@@ -2734,6 +2746,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2734
2746
  }
2735
2747
  }
2736
2748
  : undefined;
2749
+ const notifyHookError = (err) => {
2750
+ try {
2751
+ deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
2752
+ }
2753
+ catch {
2754
+ }
2755
+ };
2737
2756
  const checkpointStore = spec.checkpointStore ?? deps.checkpointStore;
2738
2757
  const durableApproval = spec.durableApproval ??
2739
2758
  (runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
@@ -3174,6 +3193,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3174
3193
  reversibilityProbe: reversibilityProbes.get(e.toolName),
3175
3194
  abortSignal: abortController.signal,
3176
3195
  ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
3196
+ onHookError: notifyHookError,
3177
3197
  shellGated: (e.toolName === "Bash" && shellGatedBash) || (e.toolName === "Monitor" && shellGatedMonitor),
3178
3198
  ...(autoModeDecider ? { autoMode: { decider: autoModeDecider } } : {}),
3179
3199
  isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),