@sema-agent/core 2.8.0 → 2.9.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 (35) hide show
  1. package/dist/agents/send-message-tool.js +37 -29
  2. package/dist/agents/subagent.js +15 -3
  3. package/dist/brain/circuit-breaker.js +18 -8
  4. package/dist/brain/retry.d.ts +1 -0
  5. package/dist/brain/retry.js +29 -7
  6. package/dist/brain/stream-engine.d.ts +1 -0
  7. package/dist/brain/stream-engine.js +74 -12
  8. package/dist/core/auto-compaction.js +9 -1
  9. package/dist/core/background-agent-store.d.ts +2 -0
  10. package/dist/core/background-agent-store.js +20 -0
  11. package/dist/core/mcp.js +8 -5
  12. package/dist/core/runner/prepare-task.js +24 -8
  13. package/dist/core/runner/runtask.js +20 -7
  14. package/dist/core/runner/tool-disclosure.d.ts +8 -3
  15. package/dist/core/runner/tool-disclosure.js +22 -8
  16. package/dist/core/skills-directory.d.ts +1 -1
  17. package/dist/core/skills-directory.js +257 -28
  18. package/dist/core/task-registry-agent.d.ts +2 -1
  19. package/dist/core/task-registry-agent.js +47 -54
  20. package/dist/core/task-registry.d.ts +1 -0
  21. package/dist/core/task-registry.js +1 -1
  22. package/dist/core/types.d.ts +5 -1
  23. package/dist/engine/compaction/compaction.js +71 -20
  24. package/dist/internal/harness-types.d.ts +1 -1
  25. package/dist/internal/harness.d.ts +1 -1
  26. package/dist/internal/harness.js +1 -1
  27. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  28. package/dist/orchestration/run-workflow-tool.js +4 -1
  29. package/dist/orchestration/workflow.d.ts +1 -1
  30. package/dist/orchestration/workflow.js +2 -2
  31. package/dist/tools/fs/bash-readonly-classifier.js +83 -17
  32. package/dist/tools/fs/fs-bash.js +17 -11
  33. package/dist/tools/fs/fs-shared.d.ts +1 -0
  34. package/dist/tools/fs/fs-shared.js +44 -2
  35. package/package.json +1 -1
@@ -688,13 +688,6 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
688
688
  queue.push({ type: "compaction_outcome", outcome, trigger: passTrigger, ...ident() });
689
689
  }
690
690
  }
691
- if (comp.compacted && !forceManual) {
692
- if (recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
693
- compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
694
- runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
695
- queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
696
- }
697
- }
698
691
  runnerHooks.recordCompactionReuse(prepared, comp);
699
692
  if (comp.compacted) {
700
693
  queue.push({
@@ -713,6 +706,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
713
706
  ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
714
707
  ...ident(),
715
708
  });
709
+ if (!forceManual && recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
710
+ compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
711
+ queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
712
+ runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
713
+ }
716
714
  if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
717
715
  const pd = comp.phaseDurations;
718
716
  const pdDur = comp.durationMs;
@@ -2074,6 +2072,9 @@ export class Runner {
2074
2072
  phase: s.phase,
2075
2073
  ...(s.detail !== undefined ? { detail: s.detail } : {}),
2076
2074
  ...(s.retryInSec !== undefined ? { retryInSec: s.retryInSec } : {}),
2075
+ ...(s.retryInMs !== undefined ? { retryInMs: s.retryInMs } : {}),
2076
+ ...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
2077
+ ...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
2077
2078
  ...ident(),
2078
2079
  });
2079
2080
  };
@@ -2675,6 +2676,7 @@ export class Runner {
2675
2676
  minTokens: rs.counters.compactionFloor,
2676
2677
  brain: compactionBrain,
2677
2678
  windowSafety: windowSafetyOptions(prepared.model),
2679
+ onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
2678
2680
  });
2679
2681
  try {
2680
2682
  if (comp?.compacted) {
@@ -3516,6 +3518,17 @@ export class Runner {
3516
3518
  comp = finishComp;
3517
3519
  }
3518
3520
  catch (err) {
3521
+ const failMsg = String(err instanceof Error ? err.message : err);
3522
+ const failReason = failMsg.length > 512 ? `${failMsg.slice(0, 512)}…` : failMsg;
3523
+ emitTrace(spec.tracer ?? this.deps.tracer, () => ({
3524
+ kind: "compaction.failed",
3525
+ version: 1,
3526
+ taskId: spec.taskId ?? prepared.sessionId,
3527
+ trigger: "auto",
3528
+ reason: failReason,
3529
+ ts: Date.now(),
3530
+ }));
3531
+ opts?.onCompactionFailed?.(failReason);
3519
3532
  this.deps.onError?.(err, { phase: "compaction", sessionId: prepared.sessionId });
3520
3533
  }
3521
3534
  }
@@ -1,5 +1,5 @@
1
1
  import { type TSchema } from "typebox";
2
- import type { AgentMessage, AgentTool, AgentToolResult } from "../../internal/harness-types.js";
2
+ import type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../../internal/harness-types.js";
3
3
  import type { Model } from "../../internal/llm.js";
4
4
  import type { ToolSpec } from "../types.js";
5
5
  export declare const TOOL_SEARCH_NAME = "ToolSearch";
@@ -29,10 +29,14 @@ export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, too
29
29
  description: string;
30
30
  }>): Map<string, DeferredToolInfo>;
31
31
  export interface PlaceholderDirectCall {
32
- parameters: TSchema;
33
- invoke: (toolCallId: string, params: unknown, signal?: AbortSignal) => Promise<AgentToolResult<unknown>>;
32
+ resolveReal: () => PlaceholderDirectTarget | undefined;
33
+ executionMode?: ToolExecutionMode;
34
34
  activate: () => Promise<void>;
35
35
  }
36
+ export interface PlaceholderDirectTarget {
37
+ parameters: TSchema;
38
+ invoke: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<unknown>) => Promise<AgentToolResult<unknown>>;
39
+ }
36
40
  export declare function createPlaceholderTool(info: DeferredToolInfo, direct?: PlaceholderDirectCall): AgentTool;
37
41
  export declare function scoreToolMatch(query: string, info: DeferredToolInfo): number;
38
42
  export interface ToolSearchArgs {
@@ -52,4 +56,5 @@ export declare function createToolSearchTool(opts: {
52
56
  rematerialize: (active: ReadonlySet<string>) => Promise<void>;
53
57
  listingRide?: (newlyActivated: readonly string[]) => string | undefined;
54
58
  mountedNames?: () => ReadonlySet<string>;
59
+ directCallEnabled?: boolean;
55
60
  }): AgentTool;
@@ -43,7 +43,8 @@ export function classifyDeferred(opts) {
43
43
  deferred.add(name);
44
44
  if (opts.deferMode === "auto") {
45
45
  const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name));
46
- const total = candidates.reduce((n, t) => n + inlinedChars(t), 0);
46
+ const inlineFace = opts.fullTools.filter((t) => !deferred.has(t.name));
47
+ const total = inlineFace.reduce((n, t) => n + inlinedChars(t), 0);
47
48
  const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
48
49
  if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
49
50
  for (const t of candidates)
@@ -70,12 +71,16 @@ export function createPlaceholderTool(info, direct) {
70
71
  return {
71
72
  name: info.name,
72
73
  label: info.name,
73
- description: `${info.hint} — deferred: call ${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load its parameters before use.`,
74
+ description: `${info.hint} — deferred: its parameters are not listed here. Call ` +
75
+ `${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load them; a call that already matches this ` +
76
+ `tool's real parameters runs directly and activates it.`,
74
77
  parameters: EMPTY_PARAMS,
75
- execute: async (toolCallId, params, signal) => {
76
- if (Value.Check(direct.parameters, params)) {
78
+ ...(direct.executionMode !== undefined ? { executionMode: direct.executionMode } : {}),
79
+ execute: async (toolCallId, params, signal, onUpdate) => {
80
+ const real = direct.resolveReal();
81
+ if (real !== undefined && Value.Check(real.parameters, params)) {
77
82
  await direct.activate();
78
- return direct.invoke(toolCallId, params, signal);
83
+ return real.invoke(toolCallId, params, signal, onUpdate);
79
84
  }
80
85
  return teachingRejection();
81
86
  },
@@ -178,13 +183,22 @@ export function extractDiscoveredToolNames(messages, registry) {
178
183
  }
179
184
  export function createToolSearchTool(opts) {
180
185
  const { registry, active, rematerialize, listingRide, mountedNames } = opts;
186
+ const directCallEnabled = opts.directCallEnabled !== false;
187
+ const activationPosture = directCallEnabled
188
+ ? "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
189
+ "parameter schema. Until you have that schema you cannot reliably form a call, so activate a tool rather " +
190
+ "than guessing its arguments — a call that does match the real schema executes and activates the tool. " +
191
+ "When any instruction, reminder, or another tool's description names a deferred tool, activate it here " +
192
+ 'with query "select:<name>". '
193
+ : "Most tools start as name-only placeholders to keep requests small; to USE one you must activate it here " +
194
+ "first. When any instruction, reminder, or another tool's description names a deferred tool, activate it " +
195
+ 'with query "select:<name>" before calling it. ';
181
196
  let activationChain = Promise.resolve();
182
197
  return defineTool({
183
198
  name: TOOL_SEARCH_NAME,
184
199
  contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
185
- description: "Discover and activate deferred tools. Most tools start as name-only placeholders to keep requests " +
186
- "small; to USE one you must activate it here first. When any instruction, reminder, or another " +
187
- 'tool\'s description names a deferred tool, activate it with query "select:<name>" before calling it. ' +
200
+ description: "Discover and activate deferred tools. " +
201
+ activationPosture +
188
202
  "Query forms: " +
189
203
  '"select:ToolA,ToolB" — activate these exact tools by name; ' +
190
204
  '"notebook jupyter" — keyword search, up to max_results best matches; ' +
@@ -1,5 +1,5 @@
1
1
  import type { SkillSpec } from "./types.js";
2
- export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "attachment_skipped" | "read_failed";
2
+ export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
3
3
  export interface SkillsDirectoryWarning {
4
4
  code: SkillsDirectoryWarningCode;
5
5
  skill: string;
@@ -1,5 +1,6 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+ import { canonicalToolName } from "./tool-name-aliases.js";
3
4
  const SKILL_FILE = "SKILL.md";
4
5
  const RESOURCE_DIRS = ["assets", "references", "scripts"];
5
6
  const NAME_MAX_CHARS = 64;
@@ -22,26 +23,139 @@ function parseSkillFile(text) {
22
23
  }
23
24
  if (end === -1)
24
25
  return { fields, body: normalized, hadFrontmatter: false };
25
- for (let i = 1; i < end; i++) {
26
+ let i = 1;
27
+ while (i < end) {
26
28
  const line = lines[i];
27
29
  const trimmed = line.trim();
28
- if (trimmed === "" || trimmed.startsWith("#"))
30
+ if (trimmed === "" || trimmed.startsWith("#")) {
31
+ i++;
29
32
  continue;
30
- if (/^\s/.test(line))
33
+ }
34
+ if (/^\s/.test(line)) {
35
+ i++;
31
36
  continue;
37
+ }
32
38
  const kv = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(trimmed);
33
- if (!kv)
39
+ if (!kv) {
40
+ i++;
34
41
  continue;
42
+ }
35
43
  const [, key, raw] = kv;
44
+ const rawValue = raw.trim();
45
+ const block = parseBlockScalarHeader(rawValue);
46
+ if (block !== undefined) {
47
+ const read = readBlockScalar(lines, i + 1, end, block);
48
+ i = read.next;
49
+ if (!fields.has(key))
50
+ fields.set(key, read.value);
51
+ continue;
52
+ }
53
+ if (rawValue.startsWith("[") && !isFlowSequenceClosed(rawValue)) {
54
+ const joined = readFlowSequenceContinuation(lines, i + 1, end, rawValue);
55
+ i = joined.next;
56
+ if (!fields.has(key))
57
+ fields.set(key, joined.value);
58
+ continue;
59
+ }
60
+ i++;
36
61
  if (fields.has(key))
37
62
  continue;
38
- fields.set(key, unquote(raw.trim()));
63
+ fields.set(key, unquote(rawValue));
39
64
  }
40
65
  let body = lines.slice(end + 1).join("\n");
41
66
  if (body.startsWith("\n"))
42
67
  body = body.slice(1);
43
68
  return { fields, body, hadFrontmatter: true };
44
69
  }
70
+ function parseBlockScalarHeader(value) {
71
+ const m = /^([|>])([0-9+-]*)$/.exec(value);
72
+ if (!m)
73
+ return undefined;
74
+ const rest = m[2];
75
+ if (!/^(?:[1-9]?[+-]?|[+-][1-9]?)$/.test(rest))
76
+ return undefined;
77
+ const digit = /[1-9]/.exec(rest)?.[0];
78
+ return { style: m[1] === "|" ? "literal" : "folded", ...(digit !== undefined ? { indent: Number(digit) } : {}) };
79
+ }
80
+ function readBlockScalar(lines, start, end, header) {
81
+ const indentOf = (s) => /^[ \t]*/.exec(s)[0].length;
82
+ let indent = header.indent;
83
+ if (indent === undefined) {
84
+ for (let j = start; j < end; j++) {
85
+ if (lines[j].trim() === "")
86
+ continue;
87
+ indent = indentOf(lines[j]);
88
+ break;
89
+ }
90
+ }
91
+ if (indent === undefined || indent === 0)
92
+ return { value: "", next: start };
93
+ const taken = [];
94
+ let j = start;
95
+ for (; j < end; j++) {
96
+ const line = lines[j];
97
+ if (line.trim() === "") {
98
+ taken.push("");
99
+ continue;
100
+ }
101
+ if (indentOf(line) < indent)
102
+ break;
103
+ taken.push(line.slice(indent));
104
+ }
105
+ while (taken.length > 0 && taken[taken.length - 1] === "")
106
+ taken.pop();
107
+ if (header.style === "literal")
108
+ return { value: taken.join("\n"), next: j };
109
+ let folded = "";
110
+ let blanks = 0;
111
+ let started = false;
112
+ let prevMoreIndented = false;
113
+ for (const cur of taken) {
114
+ if (cur === "") {
115
+ blanks++;
116
+ continue;
117
+ }
118
+ const moreIndented = indentOf(cur) > 0;
119
+ if (!started) {
120
+ folded = cur;
121
+ started = true;
122
+ }
123
+ else if (blanks > 0) {
124
+ folded += "\n".repeat(blanks) + cur;
125
+ }
126
+ else if (moreIndented || prevMoreIndented) {
127
+ folded += `\n${cur}`;
128
+ }
129
+ else {
130
+ folded += ` ${cur}`;
131
+ }
132
+ blanks = 0;
133
+ prevMoreIndented = moreIndented;
134
+ }
135
+ return { value: folded, next: j };
136
+ }
137
+ function isFlowSequenceClosed(text) {
138
+ let depth = 0;
139
+ for (const ch of text) {
140
+ if (ch === "[")
141
+ depth++;
142
+ else if (ch === "]")
143
+ depth--;
144
+ }
145
+ return depth <= 0;
146
+ }
147
+ function readFlowSequenceContinuation(lines, start, end, head) {
148
+ let acc = head;
149
+ let j = start;
150
+ for (; j < end; j++) {
151
+ acc += ` ${lines[j].trim()}`;
152
+ if (isFlowSequenceClosed(acc)) {
153
+ j++;
154
+ break;
155
+ }
156
+ }
157
+ return { value: acc, next: j };
158
+ }
45
159
  function unquote(value) {
46
160
  if (value.length < 2)
47
161
  return value;
@@ -51,7 +165,13 @@ function unquote(value) {
51
165
  const inner = value.slice(1, -1);
52
166
  return q === '"' ? inner.replace(/\\t/g, "\t").replace(/\\n/g, "\n") : inner;
53
167
  }
54
- function listRelativeFiles(base, dir, prefix, out) {
168
+ function isInsideRoot(root, p) {
169
+ if (p === root)
170
+ return true;
171
+ const rel = relative(root, p);
172
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
173
+ }
174
+ function listRelativeFiles(base, dir, prefix, out, walk) {
55
175
  let entries;
56
176
  try {
57
177
  entries = readdirSync(join(base, dir), { withFileTypes: true });
@@ -61,24 +181,68 @@ function listRelativeFiles(base, dir, prefix, out) {
61
181
  }
62
182
  for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
63
183
  const rel = `${prefix}${e.name}`;
64
- if (e.isDirectory())
65
- listRelativeFiles(base, join(dir, e.name), `${rel}/`, out);
66
- else if (e.isFile())
184
+ const abs = join(base, dir, e.name);
185
+ const resolved = resolveInsideSkill(abs, rel, walk);
186
+ if (resolved === undefined)
187
+ continue;
188
+ if (resolved.stats.isDirectory()) {
189
+ if (walk.visited.has(resolved.real))
190
+ continue;
191
+ walk.visited.add(resolved.real);
192
+ listRelativeFiles(base, join(dir, e.name), `${rel}/`, out, walk);
193
+ }
194
+ else if (resolved.stats.isFile()) {
67
195
  out.push(rel);
196
+ }
197
+ }
198
+ }
199
+ function resolveInsideSkill(abs, rel, walk) {
200
+ let real;
201
+ let stats;
202
+ try {
203
+ real = realpathSync(abs);
204
+ stats = statSync(abs);
205
+ }
206
+ catch (err) {
207
+ walk.warn({ code: "attachment_skipped", skill: walk.skill, detail: `${rel}: could not be resolved (${errText(err)})` });
208
+ return undefined;
209
+ }
210
+ if (!isInsideRoot(walk.realRoot, real)) {
211
+ walk.warn({
212
+ code: "attachment_skipped",
213
+ skill: walk.skill,
214
+ detail: `${rel}: resolves outside the skill directory — skipped (attachments are collected from the declared skill directory only)`,
215
+ });
216
+ return undefined;
68
217
  }
218
+ return { real, stats };
69
219
  }
70
220
  function readAttachments(skillDir, skillName, budgetBytes, warn) {
221
+ let realRoot;
222
+ try {
223
+ realRoot = realpathSync(skillDir);
224
+ }
225
+ catch (err) {
226
+ warn({ code: "read_failed", skill: skillName, detail: `the skill directory could not be resolved (${errText(err)}) — attachments skipped` });
227
+ return [];
228
+ }
229
+ const walk = { realRoot, skill: skillName, warn, visited: new Set([realRoot]) };
71
230
  const relPaths = [];
72
231
  for (const d of RESOURCE_DIRS) {
73
- let st;
232
+ let exists = true;
74
233
  try {
75
- st = statSync(join(skillDir, d));
234
+ statSync(join(skillDir, d));
76
235
  }
77
236
  catch {
78
- continue;
237
+ exists = false;
79
238
  }
80
- if (st.isDirectory())
81
- listRelativeFiles(skillDir, d, `${d}/`, relPaths);
239
+ if (!exists)
240
+ continue;
241
+ const resolved = resolveInsideSkill(join(skillDir, d), d, walk);
242
+ if (resolved === undefined || !resolved.stats.isDirectory())
243
+ continue;
244
+ walk.visited.add(resolved.real);
245
+ listRelativeFiles(skillDir, d, `${d}/`, relPaths, walk);
82
246
  }
83
247
  relPaths.sort();
84
248
  const files = [];
@@ -114,20 +278,60 @@ function isDecodableText(bytes) {
114
278
  function errText(err) {
115
279
  return err instanceof Error ? err.message : String(err);
116
280
  }
117
- function manifestFromAllowedTools(declared, skillName, deployedTools, warn) {
281
+ const ALL_TOOLS_WILDCARD = "*";
282
+ function splitToolNames(declared) {
283
+ let text = declared.trim();
284
+ if (text.startsWith("[") && text.endsWith("]"))
285
+ text = text.slice(1, -1);
118
286
  const names = [];
119
- for (const n of declared.split(/\s+/)) {
120
- if (n !== "" && !names.includes(n))
121
- names.push(n);
287
+ let current = "";
288
+ let depth = 0;
289
+ const flush = () => {
290
+ const name = unquote(current.trim());
291
+ if (name !== "")
292
+ names.push(name);
293
+ current = "";
294
+ };
295
+ for (const ch of text) {
296
+ if (ch === "(") {
297
+ depth++;
298
+ current += ch;
299
+ }
300
+ else if (ch === ")") {
301
+ if (depth > 0)
302
+ depth--;
303
+ current += ch;
304
+ }
305
+ else if (depth === 0 && (ch === "," || /\s/.test(ch))) {
306
+ flush();
307
+ }
308
+ else {
309
+ current += ch;
310
+ }
311
+ }
312
+ flush();
313
+ return names;
314
+ }
315
+ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools, warn) {
316
+ const names = [];
317
+ const seen = new Set();
318
+ for (const n of splitToolNames(declared)) {
319
+ const key = canonicalToolName(n);
320
+ if (seen.has(key))
321
+ continue;
322
+ seen.add(key);
323
+ names.push(n);
122
324
  }
123
325
  if (names.length === 0)
124
326
  return undefined;
327
+ if (names.some((n) => n === ALL_TOOLS_WILDCARD))
328
+ return undefined;
125
329
  let allowTools = names;
126
330
  if (deployedTools !== undefined) {
127
- const mounted = new Set(deployedTools);
128
- allowTools = names.filter((n) => mounted.has(n));
331
+ const mounted = new Set(deployedTools.map(canonicalToolName));
332
+ allowTools = names.filter((n) => mounted.has(canonicalToolName(n)));
129
333
  for (const n of names) {
130
- if (!mounted.has(n)) {
334
+ if (!mounted.has(canonicalToolName(n))) {
131
335
  warn({
132
336
  code: "allowed_tool_not_mounted",
133
337
  skill: skillName,
@@ -136,6 +340,10 @@ function manifestFromAllowedTools(declared, skillName, deployedTools, warn) {
136
340
  }
137
341
  }
138
342
  }
343
+ if (disallowed.length > 0) {
344
+ const denied = new Set(disallowed.map(canonicalToolName));
345
+ allowTools = allowTools.filter((n) => !denied.has(canonicalToolName(n)));
346
+ }
139
347
  return { allowTools, lineageId: `skill:${skillName}` };
140
348
  }
141
349
  export function createSkillsFromDirectory(dir, options = {}) {
@@ -153,10 +361,23 @@ export function createSkillsFromDirectory(dir, options = {}) {
153
361
  catch (err) {
154
362
  throw new Error(`skills directory could not be read: ${dir} (${errText(err)})`);
155
363
  }
156
- const dirNames = entries
157
- .filter((e) => e.isDirectory())
158
- .map((e) => e.name)
159
- .sort();
364
+ const dirNames = [];
365
+ for (const e of entries) {
366
+ if (e.isDirectory()) {
367
+ dirNames.push(e.name);
368
+ continue;
369
+ }
370
+ if (!e.isSymbolicLink())
371
+ continue;
372
+ try {
373
+ if (statSync(join(dir, e.name)).isDirectory())
374
+ dirNames.push(e.name);
375
+ }
376
+ catch (err) {
377
+ warn({ code: "read_failed", skill: e.name, detail: `entry is a symbolic link that could not be resolved (${errText(err)}) — skipped` });
378
+ }
379
+ }
380
+ dirNames.sort();
160
381
  const skills = [];
161
382
  for (const name of dirNames) {
162
383
  const skillDir = join(dir, name);
@@ -200,7 +421,15 @@ export function createSkillsFromDirectory(dir, options = {}) {
200
421
  continue;
201
422
  }
202
423
  const allowed = parsed.fields.get("allowed-tools");
203
- const manifest = allowed === undefined ? undefined : manifestFromAllowedTools(allowed, name, options.deployedTools, warn);
424
+ const disallowed = splitToolNames(parsed.fields.get("disallowed-tools") ?? "");
425
+ const manifest = allowed === undefined ? undefined : manifestFromAllowedTools(allowed, disallowed, name, options.deployedTools, warn);
426
+ if (disallowed.length > 0 && manifest === undefined) {
427
+ warn({
428
+ code: "disallowed_tools_unenforced",
429
+ skill: name,
430
+ detail: `disallowed-tools names ${disallowed.map((n) => `"${n}"`).join(", ")}, but this skill produced no allowlist to subtract from (allowed-tools is absent or claims every tool) — the deny declaration is NOT enforced (express it in the task's own tool policy)`,
431
+ });
432
+ }
204
433
  const files = readAttachments(skillDir, name, budget, warn);
205
434
  skills.push({
206
435
  name: declaredName,
@@ -2,7 +2,7 @@ import { type BackgroundAgentRecord, type BackgroundAgentStore } from "./backgro
2
2
  import { type StopSource, type TaskAccess, type UnifiedTaskResult, type BackgroundAgentTaskHandle, type DurableAgentCore, type ParkedClaimTicket, type RegisterBackgroundAgentInput } from "./task-registry-shared.js";
3
3
  import { type ToolResultStore } from "./tool-result-store.js";
4
4
  export declare function ensureDurableHeartbeatLane(core: DurableAgentCore): void;
5
- export declare function durableAgentWriteLane(handle: BackgroundAgentTaskHandle, patch: Partial<BackgroundAgentRecord>, clear?: (keyof BackgroundAgentRecord)[]): void;
5
+ export declare function durableAgentWriteLane(handle: BackgroundAgentTaskHandle, patch: Partial<BackgroundAgentRecord>, clear?: readonly (keyof BackgroundAgentRecord)[]): void;
6
6
  export declare function durableAgentArmedLane(core: DurableAgentCore, id: string): boolean;
7
7
  export declare function durableAgentRowProbeLane(core: DurableAgentCore, id: string): (() => Promise<boolean>) | undefined;
8
8
  export declare function beginDurableClaimLane(core: DurableAgentCore, id: string): boolean;
@@ -71,6 +71,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
71
71
  errorKind?: string;
72
72
  stoppedBy?: StopSource;
73
73
  seq?: number;
74
+ cycle?: number;
74
75
  }): "completed" | "failed" | "killed" | undefined;
75
76
  export declare function abortBackgroundAgentsForOwnerLane(core: DurableAgentCore, access: TaskAccess, opts?: {
76
77
  skipSessionScoped?: boolean;