skillscript-runtime 0.2.4
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.
- package/ARCHITECTURE.md +70 -0
- package/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/audit.d.ts +33 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +76 -0
- package/dist/audit.js.map +1 -0
- package/dist/bootstrap.d.ts +69 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +117 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +805 -0
- package/dist/cli.js.map +1 -0
- package/dist/compile.d.ts +88 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +544 -0
- package/dist/compile.js.map +1 -0
- package/dist/connectors/agent-noop.d.ts +23 -0
- package/dist/connectors/agent-noop.d.ts.map +1 -0
- package/dist/connectors/agent-noop.js +43 -0
- package/dist/connectors/agent-noop.js.map +1 -0
- package/dist/connectors/agent.d.ts +54 -0
- package/dist/connectors/agent.d.ts.map +1 -0
- package/dist/connectors/agent.js +21 -0
- package/dist/connectors/agent.js.map +1 -0
- package/dist/connectors/index.d.ts +13 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +17 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/local-model.d.ts +41 -0
- package/dist/connectors/local-model.d.ts.map +1 -0
- package/dist/connectors/local-model.js +106 -0
- package/dist/connectors/local-model.js.map +1 -0
- package/dist/connectors/mcp.d.ts +22 -0
- package/dist/connectors/mcp.d.ts.map +1 -0
- package/dist/connectors/mcp.js +31 -0
- package/dist/connectors/mcp.js.map +1 -0
- package/dist/connectors/memory-store.d.ts +53 -0
- package/dist/connectors/memory-store.d.ts.map +1 -0
- package/dist/connectors/memory-store.js +169 -0
- package/dist/connectors/memory-store.js.map +1 -0
- package/dist/connectors/registry.d.ts +74 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +127 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/connectors/skill-store.d.ts +38 -0
- package/dist/connectors/skill-store.d.ts.map +1 -0
- package/dist/connectors/skill-store.js +314 -0
- package/dist/connectors/skill-store.js.map +1 -0
- package/dist/connectors/types.d.ts +188 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +35 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/dashboard/server.d.ts +40 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +122 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/spa/app.js +375 -0
- package/dist/dashboard/spa/index.html +26 -0
- package/dist/dashboard/spa/styles.css +99 -0
- package/dist/errors.d.ts +111 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +187 -0
- package/dist/errors.js.map +1 -0
- package/dist/filters.d.ts +17 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +40 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +97 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +990 -0
- package/dist/lint.js.map +1 -0
- package/dist/mcp-server.d.ts +93 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +505 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/metrics.d.ts +51 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +107 -0
- package/dist/metrics.js.map +1 -0
- package/dist/parser.d.ts +160 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +991 -0
- package/dist/parser.js.map +1 -0
- package/dist/provenance.d.ts +43 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +58 -0
- package/dist/provenance.js.map +1 -0
- package/dist/runtime.d.ts +145 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1071 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scheduler.d.ts +121 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +271 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/skill-manager.d.ts +121 -0
- package/dist/skill-manager.d.ts.map +1 -0
- package/dist/skill-manager.js +251 -0
- package/dist/skill-manager.js.map +1 -0
- package/dist/testing/conformance.d.ts +57 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +365 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/trace.d.ts +141 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +226 -0
- package/dist/trace.js.map +1 -0
- package/examples/README.md +56 -0
- package/examples/classify-support-ticket.skill.md +30 -0
- package/examples/cut-release-tag.skill.md +40 -0
- package/examples/doc-qa-with-citations.skill.md +12 -0
- package/examples/feedback-sentiment-scan.skill.md +29 -0
- package/examples/hello.skill.md +9 -0
- package/examples/hello.skill.provenance.json +10 -0
- package/examples/morning-brief.skill.md +24 -0
- package/examples/programmatic-trace-demo.mjs +89 -0
- package/examples/service-health-watch.skill.md +18 -0
- package/package.json +100 -0
- package/scaffold/config.toml +35 -0
- package/scaffold/connectors.json +19 -0
- package/scaffold/examples/hello.skill.md +9 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
import { tokenizeKeywordArgs, processSetValue } from "./parser.js";
|
|
2
|
+
import { applyFilter } from "./filters.js";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { OpError, ConnectorNotFoundError, OpTimeoutError, InteractiveOpInAutonomousModeError, UnsafeShellDisabledError, UnresolvedVariableError, } from "./errors.js";
|
|
5
|
+
import { TraceBuilder, shouldTraceFire } from "./trace.js";
|
|
6
|
+
/**
|
|
7
|
+
* Execute a parsed skill against the live variable state. Walks targets in
|
|
8
|
+
* the provided order. Each target's ops run sequentially; on failure the
|
|
9
|
+
* chain falls back to `else:` → `# OnError:` → bubble.
|
|
10
|
+
*/
|
|
11
|
+
export async function execute(parsed, initialVars, order, ctx) {
|
|
12
|
+
const vars = new Map();
|
|
13
|
+
// Tier-1 ambient refs per language reference §3. Runtime injects these
|
|
14
|
+
// by default; caller-provided initialVars override (e.g., scheduler's
|
|
15
|
+
// dispatchSkill pre-populates EVENT.* and TRIGGER_TYPE for cron/session
|
|
16
|
+
// fires; bare execute() callers still get clock-time defaults so
|
|
17
|
+
// `$(EVENT.fired_at_unix)` resolves uniformly across dispatch paths).
|
|
18
|
+
const nowMs = Date.now();
|
|
19
|
+
const nowSec = Math.floor(nowMs / 1000);
|
|
20
|
+
vars.set("NOW", nowMs);
|
|
21
|
+
vars.set("USER", ctx.agentId ?? "unknown");
|
|
22
|
+
vars.set("SESSION_CONTEXT", "");
|
|
23
|
+
vars.set("TRIGGER_TYPE", "manual");
|
|
24
|
+
vars.set("TRIGGER_PAYLOAD", "");
|
|
25
|
+
vars.set("EVENT.fired_at", nowMs);
|
|
26
|
+
vars.set("EVENT.fired_at_unix", nowSec);
|
|
27
|
+
vars.set("EVENT.fired_at_plus_1h_unix", nowSec + 3600);
|
|
28
|
+
vars.set("EVENT.fired_at_plus_1d_unix", nowSec + 86_400);
|
|
29
|
+
vars.set("EVENT.fired_at_plus_7d_unix", nowSec + 604_800);
|
|
30
|
+
for (const v of parsed.vars) {
|
|
31
|
+
if (v.default !== undefined)
|
|
32
|
+
vars.set(v.name, coerceLiteralValue(v.default));
|
|
33
|
+
}
|
|
34
|
+
for (const [k, val] of Object.entries(initialVars)) {
|
|
35
|
+
vars.set(k, typeof val === "string" ? coerceLiteralValue(val) : val);
|
|
36
|
+
}
|
|
37
|
+
const emissions = [];
|
|
38
|
+
const errors = [];
|
|
39
|
+
let lastBoundVar = null;
|
|
40
|
+
const absoluteTimeoutMs = ctx.absoluteTimeoutMs ?? DEFAULT_RUNTIME_ABSOLUTE_TIMEOUT_MS;
|
|
41
|
+
// Trace recording (per ERD §8). Build when shouldTraceFire returns true;
|
|
42
|
+
// skip entirely when off (the NFR-11 floor — errors still surface via
|
|
43
|
+
// `result.errors[]`).
|
|
44
|
+
const triggerCtx = ctx.triggerCtx ?? { source: "manual", name: "", fired_at_ms: nowMs };
|
|
45
|
+
const triggerId = triggerCtx.trigger_id ?? `${triggerCtx.source}:${triggerCtx.name}`;
|
|
46
|
+
const skillName = parsed.name ?? "(anonymous)";
|
|
47
|
+
const traceBuilder = shouldTraceFire(ctx.trace, triggerId, skillName)
|
|
48
|
+
? new TraceBuilder(skillName, ctx.skillVersion ?? "unknown", triggerCtx, { agent_id: ctx.agentId })
|
|
49
|
+
: null;
|
|
50
|
+
for (const targetName of order) {
|
|
51
|
+
const target = parsed.targets.get(targetName);
|
|
52
|
+
if (!target)
|
|
53
|
+
continue;
|
|
54
|
+
let targetLastBound = null;
|
|
55
|
+
let targetLastValue = undefined;
|
|
56
|
+
try {
|
|
57
|
+
const r = await execOps(target.ops, vars, emissions, ctx, targetName, parsed.timeout, absoluteTimeoutMs, traceBuilder);
|
|
58
|
+
targetLastBound = r.lastBoundVar;
|
|
59
|
+
targetLastValue = r.lastBoundVar !== null ? vars.get(r.lastBoundVar) : r.lastValue;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
errors.push(buildExecutionError(err, targetName));
|
|
63
|
+
if (target.elseBlock !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
const r = await execOps(target.elseBlock, vars, emissions, ctx, targetName, parsed.timeout, absoluteTimeoutMs, traceBuilder);
|
|
66
|
+
targetLastBound = r.lastBoundVar;
|
|
67
|
+
targetLastValue = r.lastBoundVar !== null ? vars.get(r.lastBoundVar) : r.lastValue;
|
|
68
|
+
}
|
|
69
|
+
catch (innerErr) {
|
|
70
|
+
errors.push(buildExecutionError(innerErr, targetName, "else"));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (parsed.onError !== null && ctx.fallbackSkillExecutor) {
|
|
74
|
+
try {
|
|
75
|
+
const fbResult = await ctx.fallbackSkillExecutor(parsed.onError, Object.fromEntries(vars));
|
|
76
|
+
for (const em of fbResult.emissions)
|
|
77
|
+
emissions.push(em);
|
|
78
|
+
for (const fe of fbResult.errors)
|
|
79
|
+
errors.push(fe);
|
|
80
|
+
}
|
|
81
|
+
catch (fbErr) {
|
|
82
|
+
errors.push(buildExecutionError(fbErr, parsed.onError, "skill-fallback"));
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
vars.set(`${targetName}.output`, targetLastValue);
|
|
91
|
+
if (targetLastBound !== null)
|
|
92
|
+
lastBoundVar = targetLastBound;
|
|
93
|
+
}
|
|
94
|
+
// Outputs map per `# Output:` declarations. Per-kind value semantics:
|
|
95
|
+
// - Human-readable surfaces (`prompt-context:`, `slack:`, `card:`):
|
|
96
|
+
// default to joined emissions. These deliver content for an agent
|
|
97
|
+
// or human to *read*; trailing `>`/`~` JSON values are the wrong shape.
|
|
98
|
+
// - Programmatic surfaces (`text`, `file:`): default to lastBoundVar
|
|
99
|
+
// (structured), fall back to emissions array. Callers consuming
|
|
100
|
+
// `outputs.text` typically want the structured return value.
|
|
101
|
+
// - `none`: no-op marker; value irrelevant.
|
|
102
|
+
// Output payload-shape coercion: when the output kind is text-shaped
|
|
103
|
+
// (joined emissions are the natural delivery payload) we publish the
|
|
104
|
+
// string in `outputs[key]`; otherwise we pass the last bound variable
|
|
105
|
+
// through structurally. Membership here is about payload shape, not
|
|
106
|
+
// semantic destination — `slack` and `card` are listed because their
|
|
107
|
+
// delivery payloads are text, NOT because the runtime knows anything
|
|
108
|
+
// about Slack or card UIs. (v1.x: move this to connector-registered
|
|
109
|
+
// metadata via the EmissionConnector design so adopters can register
|
|
110
|
+
// new text-shaped destinations without a runtime code change.)
|
|
111
|
+
const TEXT_COERCED_OUTPUT_KINDS = new Set(["prompt-context", "template", "slack", "card"]);
|
|
112
|
+
// Agent-bound dispatch uses literal kind checks below so TS can narrow
|
|
113
|
+
// `decl.kind` to the discriminated `DeliveryPayload.kind` automatically;
|
|
114
|
+
// a runtime Set forces a type predicate. Keep the literals colocated
|
|
115
|
+
// with the dispatch loop so the agent-bound semantic set is one-line
|
|
116
|
+
// grep-able.
|
|
117
|
+
const outputDecls = parsed.outputs.length > 0
|
|
118
|
+
? parsed.outputs
|
|
119
|
+
: [{ kind: "text" }];
|
|
120
|
+
const outputs = {};
|
|
121
|
+
for (const decl of outputDecls) {
|
|
122
|
+
const key = decl.target !== undefined ? `${decl.kind}:${decl.target}` : decl.kind;
|
|
123
|
+
if (TEXT_COERCED_OUTPUT_KINDS.has(decl.kind)) {
|
|
124
|
+
outputs[key] = emissions.join("\n");
|
|
125
|
+
}
|
|
126
|
+
else if (lastBoundVar !== null && vars.has(lastBoundVar)) {
|
|
127
|
+
outputs[key] = vars.get(lastBoundVar);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
outputs[key] = emissions.slice();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Dispatch agent-targeted output decls through AgentConnector.deliver
|
|
134
|
+
// (T7.1). `prompt-context: <agent>` routes as `kind: "augment"`,
|
|
135
|
+
// `template: <agent>` as `kind: "template"`. Skipped in mechanical mode
|
|
136
|
+
// so previews don't deliver placeholder content to real substrates.
|
|
137
|
+
// Connector fallback: Registry.getAgentConnector() returns a transparent
|
|
138
|
+
// NoOpAgentConnector when no adapter is wired, so the dispatch loop
|
|
139
|
+
// never throws on missing-substrate; the no-op logs to stderr.
|
|
140
|
+
const agentDeliveryReceipts = [];
|
|
141
|
+
if (ctx.mechanical !== true) {
|
|
142
|
+
for (const decl of outputDecls) {
|
|
143
|
+
if (decl.target === undefined)
|
|
144
|
+
continue;
|
|
145
|
+
// Agent-bound output kinds: literal `===` so TS narrows decl.kind
|
|
146
|
+
// for the deliver() payload discriminator below.
|
|
147
|
+
if (decl.kind !== "prompt-context" && decl.kind !== "template")
|
|
148
|
+
continue;
|
|
149
|
+
const key = `${decl.kind}:${decl.target}`;
|
|
150
|
+
const body = String(outputs[key] ?? emissions.join("\n"));
|
|
151
|
+
const agent = ctx.registry.getAgentConnector();
|
|
152
|
+
try {
|
|
153
|
+
const receipt = decl.kind === "prompt-context"
|
|
154
|
+
? await agent.deliver(decl.target, { kind: "augment", content: body })
|
|
155
|
+
: await agent.deliver(decl.target, {
|
|
156
|
+
kind: "template",
|
|
157
|
+
prompt: body,
|
|
158
|
+
...(parsed.name !== null ? { source_skill: parsed.name } : {}),
|
|
159
|
+
});
|
|
160
|
+
agentDeliveryReceipts.push({ agent_id: decl.target, output_kind: decl.kind, receipt });
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
// Delivery failure is non-fatal — record alongside other errors so
|
|
164
|
+
// the dashboard surfaces it, but don't propagate. Skill execution
|
|
165
|
+
// already succeeded by this point.
|
|
166
|
+
process.stderr.write(`[agent-deliver] ${decl.kind}:${decl.target} failed: ${err.message}\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Persist trace record if recording was active. Write is non-blocking —
|
|
171
|
+
// a failed write logs to stderr but doesn't change the execute() result
|
|
172
|
+
// (per ERD §8 NFR-11 floor: errors in trace persistence shouldn't bubble
|
|
173
|
+
// up as op errors; the trace store is an observability surface, not a
|
|
174
|
+
// dispatch dependency).
|
|
175
|
+
if (traceBuilder !== null && ctx.traceStore !== undefined) {
|
|
176
|
+
const record = traceBuilder.finalize(emissions, outputs, errors);
|
|
177
|
+
try {
|
|
178
|
+
await ctx.traceStore.write(record);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
process.stderr.write(`[trace] failed to write record ${record.trace_id}: ${err.message}\n`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
finalVars: Object.fromEntries(vars),
|
|
186
|
+
emissions,
|
|
187
|
+
outputs,
|
|
188
|
+
errors,
|
|
189
|
+
targetOrder: order,
|
|
190
|
+
agentDeliveryReceipts,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async function execOps(ops, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder) {
|
|
194
|
+
let lastBoundVar = null;
|
|
195
|
+
let lastValue = undefined;
|
|
196
|
+
for (const op of ops) {
|
|
197
|
+
const r = await execOp(op, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder);
|
|
198
|
+
if (r.lastBoundVar !== null) {
|
|
199
|
+
lastBoundVar = r.lastBoundVar;
|
|
200
|
+
lastValue = r.lastValue;
|
|
201
|
+
}
|
|
202
|
+
else if (r.lastValue !== undefined) {
|
|
203
|
+
lastValue = r.lastValue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { lastBoundVar, lastValue };
|
|
207
|
+
}
|
|
208
|
+
async function execOp(op, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder) {
|
|
209
|
+
const startMs = traceBuilder !== null ? Date.now() : 0;
|
|
210
|
+
let errored = false;
|
|
211
|
+
try {
|
|
212
|
+
return await execOpInner(op, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
errored = true;
|
|
216
|
+
// Default-tag any escaping error with `op.kind`. Explicit makeOpError()
|
|
217
|
+
// tags take precedence. Fixes the case where `~` failures classified as `?`.
|
|
218
|
+
const e = err;
|
|
219
|
+
if (e.opKind === undefined)
|
|
220
|
+
e.opKind = op.kind;
|
|
221
|
+
throw e;
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
if (traceBuilder !== null) {
|
|
225
|
+
const connector = extractOpConnector(op);
|
|
226
|
+
traceBuilder.recordOp({
|
|
227
|
+
op_kind: op.kind,
|
|
228
|
+
target: targetName,
|
|
229
|
+
body: op.body,
|
|
230
|
+
started_at_ms: startMs,
|
|
231
|
+
duration_ms: Date.now() - startMs,
|
|
232
|
+
errored,
|
|
233
|
+
...(connector !== undefined ? { connector } : {}),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Extract the connector instance name for $/~/> ops; undefined for others. */
|
|
239
|
+
function extractOpConnector(op) {
|
|
240
|
+
switch (op.kind) {
|
|
241
|
+
case "$": return op.mcpConnector ?? "primary";
|
|
242
|
+
case "~": return op.localModelParams?.model ?? "default";
|
|
243
|
+
case ">": return op.retrievalParams?.connector ?? "primary";
|
|
244
|
+
default: return undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function execOpInner(op, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder) {
|
|
248
|
+
switch (op.kind) {
|
|
249
|
+
case "$set": {
|
|
250
|
+
const coerced = coerceLiteralValue(op.setValue);
|
|
251
|
+
vars.set(op.setName, coerced);
|
|
252
|
+
return { lastBoundVar: op.setName, lastValue: coerced };
|
|
253
|
+
}
|
|
254
|
+
case "?": {
|
|
255
|
+
const body = substituteRuntime(op.body, vars);
|
|
256
|
+
emissions.push(`Reason: ${body}`);
|
|
257
|
+
return { lastBoundVar: null, lastValue: undefined };
|
|
258
|
+
}
|
|
259
|
+
case "!": {
|
|
260
|
+
const body = substituteRuntime(op.body, vars);
|
|
261
|
+
emissions.push(body);
|
|
262
|
+
return { lastBoundVar: null, lastValue: undefined };
|
|
263
|
+
}
|
|
264
|
+
case "@": {
|
|
265
|
+
const body = op.policy === "unsafe"
|
|
266
|
+
? substituteRuntimeUnsafe(op.body, vars)
|
|
267
|
+
: substituteRuntime(op.body, vars);
|
|
268
|
+
const shellTimeoutMs = resolveOpTimeoutMs(undefined, skillTimeoutSec, absoluteTimeoutMs, vars);
|
|
269
|
+
if (ctx.mechanical === true) {
|
|
270
|
+
const label = op.policy === "unsafe" ? "Would run unsafe shell" : "Would run shell";
|
|
271
|
+
emissions.push(`${label}: ${body} (mechanical: true preview).`);
|
|
272
|
+
// Bind a placeholder so downstream `$(VAR)` substitutions resolve.
|
|
273
|
+
// Matches the convention used by `$`/`~`/`>` mechanical-mode binding.
|
|
274
|
+
const flatKey = `${targetName}.output`;
|
|
275
|
+
const placeholder = `[mechanical: would run ${body.slice(0, 40)}${body.length > 40 ? "..." : ""}]`;
|
|
276
|
+
vars.set(flatKey, placeholder);
|
|
277
|
+
if (op.outputVar !== undefined)
|
|
278
|
+
vars.set(op.outputVar, placeholder);
|
|
279
|
+
return {
|
|
280
|
+
lastBoundVar: op.outputVar ?? flatKey,
|
|
281
|
+
lastValue: placeholder,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
let stdout;
|
|
285
|
+
if (op.policy === "unsafe") {
|
|
286
|
+
if (ctx.enableUnsafeShell !== true) {
|
|
287
|
+
throw new UnsafeShellDisabledError(body, targetName);
|
|
288
|
+
}
|
|
289
|
+
stdout = await execShellCommand("bash", ["-c", body], shellTimeoutMs);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const tokens = tokenizeShellArgs(body);
|
|
293
|
+
if (tokens.length === 0) {
|
|
294
|
+
throw makeOpError("@", `Empty \`@\` op body in target '${targetName}'.`);
|
|
295
|
+
}
|
|
296
|
+
const [bin, ...args] = tokens;
|
|
297
|
+
stdout = await execShellCommand(bin, args, shellTimeoutMs);
|
|
298
|
+
}
|
|
299
|
+
const flatKey = `${targetName}.output`;
|
|
300
|
+
vars.set(flatKey, stdout);
|
|
301
|
+
if (op.outputVar !== undefined)
|
|
302
|
+
vars.set(op.outputVar, stdout);
|
|
303
|
+
return {
|
|
304
|
+
lastBoundVar: op.outputVar ?? flatKey,
|
|
305
|
+
lastValue: stdout,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
case "??": {
|
|
309
|
+
const promptStr = substituteRuntime(op.body, vars);
|
|
310
|
+
if (ctx.askUser === undefined) {
|
|
311
|
+
// Autonomous mode — no interactive surface wired. Per decision 6 +
|
|
312
|
+
// §6 dispatcher routing, `??` fails fast so dependent targets don't
|
|
313
|
+
// silently fall through.
|
|
314
|
+
throw new InteractiveOpInAutonomousModeError(promptStr, targetName);
|
|
315
|
+
}
|
|
316
|
+
const response = await ctx.askUser(promptStr);
|
|
317
|
+
const outName = op.outputVar;
|
|
318
|
+
if (outName !== undefined)
|
|
319
|
+
vars.set(outName, response);
|
|
320
|
+
// Decline semantics (per Section 2 Ops + §13 Open Q #2 resolution):
|
|
321
|
+
// bind the response AND short-circuit downstream via soft op-error
|
|
322
|
+
// routed through else: / # OnError:. Closes the silent-fall-through
|
|
323
|
+
// security bug pattern (subsequent `apply:` running on a "no").
|
|
324
|
+
if (isDeclineResponse(response)) {
|
|
325
|
+
throw makeOpError("??", `User declined at \`??\` prompt: '${promptStr}' (response: '${response}'). Dependent targets short-circuited.`);
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
lastBoundVar: outName ?? null,
|
|
329
|
+
lastValue: response,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case "&": {
|
|
333
|
+
// `&` ops are resolved at compile time — data-skill content is
|
|
334
|
+
// inlined; procedural-skill refs compile to a runtime invocation
|
|
335
|
+
// shape (not this op). If we hit `&` at runtime, the executor was
|
|
336
|
+
// handed a raw AST that bypassed compile().
|
|
337
|
+
const skillName = op.ampParams?.skillName ?? "(unknown)";
|
|
338
|
+
throw makeOpError("&", `\`& ${skillName}\` reached the runtime unresolved. The compile() ` +
|
|
339
|
+
`step inlines data-skills and lowers procedural refs to invocation ` +
|
|
340
|
+
`ops; running raw parsed skills bypasses that. Call compile() ` +
|
|
341
|
+
`before execute().`);
|
|
342
|
+
}
|
|
343
|
+
case "$": {
|
|
344
|
+
const body = substituteRuntime(op.body, vars);
|
|
345
|
+
const m = /^([A-Za-z_][\w:-]*)\s*([\s\S]*)$/.exec(body);
|
|
346
|
+
if (m === null) {
|
|
347
|
+
throw makeOpError("$", `Malformed \`$\` op body: '${body}' — expected 'TOOL_NAME key=value ...'`);
|
|
348
|
+
}
|
|
349
|
+
const toolName = m[1];
|
|
350
|
+
const argsStr = m[2] ?? "";
|
|
351
|
+
const args = parseToolArgs(argsStr);
|
|
352
|
+
const connectorLabel = op.mcpConnector !== undefined ? `${op.mcpConnector}.` : "";
|
|
353
|
+
const flatKey = `${targetName}.output`;
|
|
354
|
+
// Mechanical preview, registry-routed, test escape hatch, no-dispatcher.
|
|
355
|
+
if (ctx.mechanical === true) {
|
|
356
|
+
emissions.push(`Would call tool ${connectorLabel}${toolName} with ${JSON.stringify(args)} (mechanical: true preview).`);
|
|
357
|
+
// Bind a placeholder that responds to dotted access (`$(X.field)`)
|
|
358
|
+
// so cold-agent skills using `$ tool -> X` then `$(X.title)` etc.
|
|
359
|
+
// can execute end-to-end without real dispatch.
|
|
360
|
+
const placeholder = makeMechanicalPlaceholder(op.outputVar ?? flatKey);
|
|
361
|
+
vars.set(flatKey, placeholder);
|
|
362
|
+
if (op.outputVar !== undefined)
|
|
363
|
+
vars.set(op.outputVar, placeholder);
|
|
364
|
+
return {
|
|
365
|
+
lastBoundVar: op.outputVar ?? flatKey,
|
|
366
|
+
lastValue: placeholder,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const connectorName = op.mcpConnector ?? "primary";
|
|
370
|
+
let rawResult;
|
|
371
|
+
let dispatched = false;
|
|
372
|
+
const timeoutMs = resolveOpTimeoutMs(undefined, skillTimeoutSec, absoluteTimeoutMs, vars);
|
|
373
|
+
// Op-level fallback (per language reference §9, extended to `$` for
|
|
374
|
+
// cold-agent corpus consistency). On dispatch throw, bind the
|
|
375
|
+
// fallback value to the output var; on missing connector with
|
|
376
|
+
// fallback present, ditto.
|
|
377
|
+
const dollarFallback = op.fallback !== undefined ? coerceLiteralValue(op.fallback) : undefined;
|
|
378
|
+
try {
|
|
379
|
+
if (ctx.registry.hasMcpConnector(connectorName)) {
|
|
380
|
+
const connector = ctx.registry.getMcpConnector(connectorName);
|
|
381
|
+
rawResult = await dispatchWithTimeout(() => connector.call(toolName, args, ctx.agentId !== undefined ? { agentId: ctx.agentId } : undefined), timeoutMs, "$");
|
|
382
|
+
dispatched = true;
|
|
383
|
+
}
|
|
384
|
+
else if (op.mcpConnector === undefined && ctx.toolDispatch) {
|
|
385
|
+
rawResult = await dispatchWithTimeout(() => ctx.toolDispatch(toolName, args), timeoutMs, "$");
|
|
386
|
+
dispatched = true;
|
|
387
|
+
}
|
|
388
|
+
else if (op.mcpConnector !== undefined) {
|
|
389
|
+
throw new ConnectorNotFoundError(connectorName, "mcp_connector", "$", targetName);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
if (dollarFallback !== undefined) {
|
|
394
|
+
vars.set(flatKey, dollarFallback);
|
|
395
|
+
if (op.outputVar !== undefined)
|
|
396
|
+
vars.set(op.outputVar, dollarFallback);
|
|
397
|
+
return { lastBoundVar: op.outputVar ?? flatKey, lastValue: dollarFallback };
|
|
398
|
+
}
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
if (!dispatched) {
|
|
402
|
+
emissions.push(`Would call tool ${connectorLabel}${toolName} with ${JSON.stringify(args)} (no dispatcher wired).`);
|
|
403
|
+
vars.set(flatKey, null);
|
|
404
|
+
if (op.outputVar !== undefined)
|
|
405
|
+
vars.set(op.outputVar, null);
|
|
406
|
+
return {
|
|
407
|
+
lastBoundVar: op.outputVar ?? flatKey,
|
|
408
|
+
lastValue: null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// c580de5: surface inner-tool `isError: true` as an op error. Otherwise
|
|
412
|
+
// the error text gets bound silently to the output var and the skill
|
|
413
|
+
// continues. Throw so the outer execOps catch records this in
|
|
414
|
+
// `result.errors[]` and the else/OnError fallback machinery can fire.
|
|
415
|
+
if (rawResult !== null &&
|
|
416
|
+
typeof rawResult === "object" &&
|
|
417
|
+
rawResult.isError === true) {
|
|
418
|
+
const innerText = extractToolErrorText(rawResult);
|
|
419
|
+
throw makeOpError("$", `tool ${connectorLabel}${toolName} returned isError: ${innerText}`);
|
|
420
|
+
}
|
|
421
|
+
const bindValue = unwrapToolResult(rawResult);
|
|
422
|
+
vars.set(flatKey, bindValue);
|
|
423
|
+
if (op.outputVar !== undefined)
|
|
424
|
+
vars.set(op.outputVar, bindValue);
|
|
425
|
+
return {
|
|
426
|
+
lastBoundVar: op.outputVar ?? flatKey,
|
|
427
|
+
lastValue: bindValue,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
case ">": {
|
|
431
|
+
const p = op.retrievalParams;
|
|
432
|
+
const querySub = substituteRuntime(p.query, vars);
|
|
433
|
+
const extraSub = {};
|
|
434
|
+
for (const [k, v] of Object.entries(p.extra)) {
|
|
435
|
+
extraSub[k] = substituteRuntime(v, vars);
|
|
436
|
+
}
|
|
437
|
+
if (ctx.mechanical === true) {
|
|
438
|
+
// Bind a 1-element array of placeholders so foreach M in $(RESULTS)
|
|
439
|
+
// iterates once with a dotted-accessible M (matches common author
|
|
440
|
+
// patterns like `$(M.id)`, `$(M.summary)`). Authors expecting empty
|
|
441
|
+
// result sets test the empty case in unit tests, not mechanical mode.
|
|
442
|
+
const mechanicalValue = p.fallback !== undefined
|
|
443
|
+
? p.fallback
|
|
444
|
+
: [makeMechanicalPlaceholder(`${op.outputVar}[0]`)];
|
|
445
|
+
emissions.push(`Would query MemoryStore \`${p.connector}\` with mode=${p.mode}, ` +
|
|
446
|
+
`query="${querySub}", limit=${p.limit} (mechanical: true preview). ` +
|
|
447
|
+
`Binding $(${op.outputVar}) = placeholder result set.`);
|
|
448
|
+
vars.set(op.outputVar, mechanicalValue);
|
|
449
|
+
return { lastBoundVar: op.outputVar, lastValue: mechanicalValue };
|
|
450
|
+
}
|
|
451
|
+
const store = ctx.registry.getMemoryStore(p.connector);
|
|
452
|
+
const limitResolved = resolveIntParam(p.limit, vars, "limit");
|
|
453
|
+
const filters = {
|
|
454
|
+
query: querySub,
|
|
455
|
+
mode: p.mode,
|
|
456
|
+
limit: limitResolved,
|
|
457
|
+
...extraSub,
|
|
458
|
+
};
|
|
459
|
+
const retrievalTimeoutMs = resolveOpTimeoutMs(undefined, skillTimeoutSec, absoluteTimeoutMs, vars);
|
|
460
|
+
// Op-level fallback (per language reference §9): on throw OR empty
|
|
461
|
+
// result, bind the fallback value and continue. Without a fallback,
|
|
462
|
+
// throws propagate to `else:` / `# OnError:` / target error.
|
|
463
|
+
// coerceLiteralValue parses array-shaped literals (`[]`, `[a, b]`)
|
|
464
|
+
// into actual arrays so downstream `foreach M in $(VAR)` iterates
|
|
465
|
+
// correctly on the empty/sentinel case.
|
|
466
|
+
const coercedFallback = p.fallback !== undefined ? coerceLiteralValue(p.fallback) : undefined;
|
|
467
|
+
let results;
|
|
468
|
+
try {
|
|
469
|
+
results = await dispatchWithTimeout(() => store.query(filters), retrievalTimeoutMs, ">");
|
|
470
|
+
if (coercedFallback !== undefined && Array.isArray(results) && results.length === 0) {
|
|
471
|
+
results = coercedFallback;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
if (coercedFallback !== undefined) {
|
|
476
|
+
results = coercedFallback;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
throw err;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
vars.set(op.outputVar, results);
|
|
483
|
+
return { lastBoundVar: op.outputVar, lastValue: results };
|
|
484
|
+
}
|
|
485
|
+
case "~": {
|
|
486
|
+
const p = op.localModelParams;
|
|
487
|
+
const promptSub = substituteRuntime(p.prompt, vars);
|
|
488
|
+
if (ctx.mechanical === true) {
|
|
489
|
+
const modelName = p.model ?? "default";
|
|
490
|
+
const placeholder = `[mechanical: would call LocalModel ${modelName} with prompt='${promptSub}']`;
|
|
491
|
+
emissions.push(`Would invoke LocalModel \`${modelName}\` (mechanical: true preview). Binding $(${op.outputVar}) = ${placeholder}`);
|
|
492
|
+
vars.set(op.outputVar, placeholder);
|
|
493
|
+
return { lastBoundVar: op.outputVar, lastValue: placeholder };
|
|
494
|
+
}
|
|
495
|
+
let model;
|
|
496
|
+
try {
|
|
497
|
+
model = ctx.registry.getLocalModel(p.model);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
if (p.fallback !== undefined) {
|
|
501
|
+
vars.set(op.outputVar, p.fallback);
|
|
502
|
+
return { lastBoundVar: op.outputVar, lastValue: p.fallback };
|
|
503
|
+
}
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
506
|
+
const runOpts = {};
|
|
507
|
+
if (p.maxTokens !== undefined) {
|
|
508
|
+
runOpts.maxTokens = resolveIntParam(p.maxTokens, vars, "maxTokens");
|
|
509
|
+
}
|
|
510
|
+
const tildeTimeoutMs = resolveOpTimeoutMs(p.timeoutSeconds, skillTimeoutSec, absoluteTimeoutMs, vars);
|
|
511
|
+
// Op-level fallback (per language reference §9): on throw OR empty
|
|
512
|
+
// (trimmed) response, bind the fallback value.
|
|
513
|
+
let response;
|
|
514
|
+
try {
|
|
515
|
+
response = await dispatchWithTimeout(() => model.run(promptSub, runOpts), tildeTimeoutMs, "~");
|
|
516
|
+
if (p.fallback !== undefined && response.trim() === "") {
|
|
517
|
+
response = p.fallback;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
if (p.fallback !== undefined) {
|
|
522
|
+
response = p.fallback;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
throw err;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
vars.set(op.outputVar, response);
|
|
529
|
+
return { lastBoundVar: op.outputVar, lastValue: response };
|
|
530
|
+
}
|
|
531
|
+
case "foreach": {
|
|
532
|
+
const listVal = resolveListExpr(op.foreachList, vars);
|
|
533
|
+
const iterName = op.foreachIter;
|
|
534
|
+
const before = new Set(vars.keys());
|
|
535
|
+
let last = { lastBoundVar: null, lastValue: undefined };
|
|
536
|
+
for (const item of listVal) {
|
|
537
|
+
vars.set(iterName, item);
|
|
538
|
+
last = await execOps(op.foreachBody, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder);
|
|
539
|
+
}
|
|
540
|
+
for (const k of Array.from(vars.keys())) {
|
|
541
|
+
if (!before.has(k))
|
|
542
|
+
vars.delete(k);
|
|
543
|
+
}
|
|
544
|
+
return last;
|
|
545
|
+
}
|
|
546
|
+
case "if": {
|
|
547
|
+
for (const branch of op.ifBranches) {
|
|
548
|
+
if (evalCondition(branch.cond, vars)) {
|
|
549
|
+
return execOps(branch.body, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (op.ifElseBody !== undefined) {
|
|
553
|
+
return execOps(op.ifElseBody, vars, emissions, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder);
|
|
554
|
+
}
|
|
555
|
+
return { lastBoundVar: null, lastValue: undefined };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { lastBoundVar: null, lastValue: undefined };
|
|
559
|
+
}
|
|
560
|
+
function makeOpError(opKind, message) {
|
|
561
|
+
const err = new Error(message);
|
|
562
|
+
err.opKind = opKind;
|
|
563
|
+
return err;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Build a structured ExecutionError from a thrown value. Recognizes OpError
|
|
567
|
+
* subclasses (preserves class name + canned remediation); falls back to
|
|
568
|
+
* generic Error inspection (message + opKind tag) per existing convention.
|
|
569
|
+
*/
|
|
570
|
+
function buildExecutionError(err, target, opKindOverride) {
|
|
571
|
+
if (err instanceof OpError) {
|
|
572
|
+
const entry = {
|
|
573
|
+
target: err.target ?? target,
|
|
574
|
+
opKind: opKindOverride ?? err.opKind,
|
|
575
|
+
message: err.message,
|
|
576
|
+
class: err.name,
|
|
577
|
+
remediation: err.remediation,
|
|
578
|
+
};
|
|
579
|
+
if (err.innerCause !== undefined)
|
|
580
|
+
entry.innerCause = err.innerCause;
|
|
581
|
+
return entry;
|
|
582
|
+
}
|
|
583
|
+
const e = err;
|
|
584
|
+
return {
|
|
585
|
+
target,
|
|
586
|
+
opKind: opKindOverride ?? e.opKind ?? "?",
|
|
587
|
+
message: e.message,
|
|
588
|
+
class: e.name ?? "Error",
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const DEFAULT_RUNTIME_ABSOLUTE_TIMEOUT_MS = 300_000;
|
|
592
|
+
/**
|
|
593
|
+
* Per-op timeout resolution chain (ERD §6 decision 7) — top wins:
|
|
594
|
+
* 1. Per-op override (`~ ... timeoutSeconds=30 ...`)
|
|
595
|
+
* 2. Skill-level `# Timeout: N` header
|
|
596
|
+
* 3. Connector instance default (v1: not yet declared by impls — collapses
|
|
597
|
+
* to built-in fallback when no per-op or skill-level value is present)
|
|
598
|
+
* 4. Built-in language fallback (`absoluteTimeoutMs`, default 300000ms)
|
|
599
|
+
*
|
|
600
|
+
* Both per-op and skill-level values are in seconds (per author convention)
|
|
601
|
+
* and converted to milliseconds here.
|
|
602
|
+
*/
|
|
603
|
+
function resolveOpTimeoutMs(perOpTimeoutSec, skillTimeoutSec, absoluteTimeoutMs, vars) {
|
|
604
|
+
if (perOpTimeoutSec !== undefined) {
|
|
605
|
+
return resolveIntParam(perOpTimeoutSec, vars, "timeoutSeconds") * 1000;
|
|
606
|
+
}
|
|
607
|
+
if (skillTimeoutSec !== null) {
|
|
608
|
+
return resolveIntParam(skillTimeoutSec, vars, "# Timeout:") * 1000;
|
|
609
|
+
}
|
|
610
|
+
return absoluteTimeoutMs;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Race the op against a timer. On timeout, throws `OpTimeoutError`-shaped
|
|
614
|
+
* op-error so the existing else: / # OnError: machinery catches it.
|
|
615
|
+
*
|
|
616
|
+
* v1 caveat: timeout returns control to the executor promptly, but the
|
|
617
|
+
* underlying request may still complete in the background — its result is
|
|
618
|
+
* discarded. v2 should thread AbortSignal through connector contracts so
|
|
619
|
+
* implementations can cancel cleanly.
|
|
620
|
+
*/
|
|
621
|
+
async function dispatchWithTimeout(fn, timeoutMs, opKind) {
|
|
622
|
+
let timer;
|
|
623
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
624
|
+
timer = setTimeout(() => {
|
|
625
|
+
reject(new OpTimeoutError(timeoutMs, opKind));
|
|
626
|
+
}, timeoutMs);
|
|
627
|
+
});
|
|
628
|
+
try {
|
|
629
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
if (timer !== undefined)
|
|
633
|
+
clearTimeout(timer);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Decline detection for `??` interactive responses. A response is declining
|
|
638
|
+
* when trimmed-lowercase matches `no`/`n`/`false`/`0` or is empty. Anything
|
|
639
|
+
* else (including "yes", "y", or any non-empty positive content) is treated
|
|
640
|
+
* as approval.
|
|
641
|
+
*/
|
|
642
|
+
/**
|
|
643
|
+
* Tokenize a shell-style command body into binary + args. Respects matching
|
|
644
|
+
* single/double quotes; strips outer quotes. No metachar interpretation —
|
|
645
|
+
* the structural-spawn sandbox forbids shell processing per decision 2.
|
|
646
|
+
*/
|
|
647
|
+
function tokenizeShellArgs(body) {
|
|
648
|
+
const tokens = [];
|
|
649
|
+
let current = "";
|
|
650
|
+
let inQuote = null;
|
|
651
|
+
for (let i = 0; i < body.length; i++) {
|
|
652
|
+
const ch = body[i];
|
|
653
|
+
if (inQuote) {
|
|
654
|
+
if (ch === inQuote) {
|
|
655
|
+
inQuote = null;
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
current += ch;
|
|
659
|
+
}
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (ch === '"' || ch === "'") {
|
|
663
|
+
inQuote = ch;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (/\s/.test(ch)) {
|
|
667
|
+
if (current !== "") {
|
|
668
|
+
tokens.push(current);
|
|
669
|
+
current = "";
|
|
670
|
+
}
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
current += ch;
|
|
674
|
+
}
|
|
675
|
+
if (current !== "")
|
|
676
|
+
tokens.push(current);
|
|
677
|
+
return tokens;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Spawn a child process and capture stdout. SIGKILL on timeout via the
|
|
681
|
+
* process group (kills child + descendants). Non-zero exit → op-error with
|
|
682
|
+
* stderr preserved per ERD §6 dispatcher routing.
|
|
683
|
+
*/
|
|
684
|
+
async function execShellCommand(bin, args, timeoutMs) {
|
|
685
|
+
return new Promise((resolve, reject) => {
|
|
686
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
687
|
+
let stdout = "";
|
|
688
|
+
let stderr = "";
|
|
689
|
+
let killed = false;
|
|
690
|
+
const timer = setTimeout(() => {
|
|
691
|
+
killed = true;
|
|
692
|
+
// Send SIGKILL to the process group on POSIX. Windows lacks process
|
|
693
|
+
// groups; fall back to direct child kill (descendants leak — out of
|
|
694
|
+
// v1 scope to fix).
|
|
695
|
+
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
696
|
+
try {
|
|
697
|
+
process.kill(-child.pid, "SIGKILL");
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
child.kill("SIGKILL");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
child.kill("SIGKILL");
|
|
705
|
+
}
|
|
706
|
+
}, timeoutMs);
|
|
707
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
708
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
709
|
+
child.on("error", (err) => {
|
|
710
|
+
clearTimeout(timer);
|
|
711
|
+
reject(makeOpError("@", `Failed to spawn '${bin}': ${err.message}`));
|
|
712
|
+
});
|
|
713
|
+
child.on("close", (code) => {
|
|
714
|
+
clearTimeout(timer);
|
|
715
|
+
if (killed) {
|
|
716
|
+
reject(new OpTimeoutError(timeoutMs, "@"));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (code !== 0) {
|
|
720
|
+
const trimmed = stderr.trim();
|
|
721
|
+
reject(makeOpError("@", `Shell command '${bin}' exited with code ${code}${trimmed ? `: ${trimmed.slice(0, 200)}` : ""}.`));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
// Strip trailing newline — convention for shell command output.
|
|
725
|
+
resolve(stdout.replace(/\n$/, ""));
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function isDeclineResponse(raw) {
|
|
730
|
+
const t = raw.trim().toLowerCase();
|
|
731
|
+
return t === "" || t === "no" || t === "n" || t === "false" || t === "0";
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Resolve an integer parameter that may be a literal number or a string
|
|
735
|
+
* containing a `$(VAR)` ref. Substitutes any refs then parseInts. Throws
|
|
736
|
+
* a clear runtime error if the resolved value isn't a positive integer —
|
|
737
|
+
* the parser deferred validation to here because at parse time the ref
|
|
738
|
+
* couldn't be resolved.
|
|
739
|
+
*/
|
|
740
|
+
function resolveIntParam(raw, vars, paramName) {
|
|
741
|
+
if (typeof raw === "number")
|
|
742
|
+
return raw;
|
|
743
|
+
const substituted = substituteRuntime(raw, vars);
|
|
744
|
+
const n = parseInt(substituted, 10);
|
|
745
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
746
|
+
throw new Error(`'${paramName}' resolved to '${substituted}', which isn't a positive integer.`);
|
|
747
|
+
}
|
|
748
|
+
return n;
|
|
749
|
+
}
|
|
750
|
+
function extractToolErrorText(rawResult) {
|
|
751
|
+
if (rawResult === null || typeof rawResult !== "object")
|
|
752
|
+
return String(rawResult);
|
|
753
|
+
const obj = rawResult;
|
|
754
|
+
if (Array.isArray(obj.content) && obj.content.length > 0) {
|
|
755
|
+
const first = obj.content[0];
|
|
756
|
+
if (first && first.type === "text" && typeof first.text === "string") {
|
|
757
|
+
return first.text;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
return JSON.stringify(rawResult);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return "(unparseable error envelope)";
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function parseToolArgs(argsStr) {
|
|
768
|
+
const tokens = tokenizeKeywordArgs(argsStr);
|
|
769
|
+
const args = {};
|
|
770
|
+
for (const tok of tokens) {
|
|
771
|
+
const eq = tok.indexOf("=");
|
|
772
|
+
if (eq === -1)
|
|
773
|
+
continue;
|
|
774
|
+
const key = tok.slice(0, eq).trim();
|
|
775
|
+
const rawValue = tok.slice(eq + 1);
|
|
776
|
+
args[key] = processSetValue(rawValue);
|
|
777
|
+
}
|
|
778
|
+
return args;
|
|
779
|
+
}
|
|
780
|
+
function resolveListExpr(expr, vars) {
|
|
781
|
+
const trimmed = expr.trim();
|
|
782
|
+
const ref = /^\$\(([^)]+)\)$/.exec(trimmed);
|
|
783
|
+
if (ref) {
|
|
784
|
+
const val = resolveRef(ref[1], vars);
|
|
785
|
+
if (Array.isArray(val))
|
|
786
|
+
return val;
|
|
787
|
+
if (val === undefined || val === null)
|
|
788
|
+
return [];
|
|
789
|
+
return [val];
|
|
790
|
+
}
|
|
791
|
+
const list = /^\[(.*)\]$/.exec(trimmed);
|
|
792
|
+
if (list) {
|
|
793
|
+
const inner = list[1].trim();
|
|
794
|
+
if (inner === "")
|
|
795
|
+
return [];
|
|
796
|
+
return inner.split(",").map((s) => {
|
|
797
|
+
const t = s.trim();
|
|
798
|
+
if (t.length >= 2) {
|
|
799
|
+
const first = t[0];
|
|
800
|
+
const last = t[t.length - 1];
|
|
801
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
802
|
+
return t.slice(1, -1);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return t;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
const sub = substituteRuntime(trimmed, vars);
|
|
809
|
+
try {
|
|
810
|
+
const v = JSON.parse(sub);
|
|
811
|
+
if (Array.isArray(v))
|
|
812
|
+
return v;
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
/* not JSON — wrap */
|
|
816
|
+
}
|
|
817
|
+
return [sub];
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Unwrap `CallToolResult`-shaped values into the meaningful payload.
|
|
821
|
+
* Symmetry with `>` (binds `PortableMemory[]`) and `~` (binds the response
|
|
822
|
+
* string) — `$` should bind the *content*, not the wire envelope.
|
|
823
|
+
*
|
|
824
|
+
* Rules:
|
|
825
|
+
* 1. Non-CallToolResult-shaped — bind as-is.
|
|
826
|
+
* 2. `content[0].type === "text"` + JSON-parseable — bind parsed.
|
|
827
|
+
* 3. `content[0].type === "text"` + non-parseable — bind the raw string.
|
|
828
|
+
* 4. Non-text content — bind the content array.
|
|
829
|
+
*/
|
|
830
|
+
function unwrapToolResult(result) {
|
|
831
|
+
if (result === null || typeof result !== "object")
|
|
832
|
+
return result;
|
|
833
|
+
const obj = result;
|
|
834
|
+
if (!Array.isArray(obj.content))
|
|
835
|
+
return result;
|
|
836
|
+
const first = obj.content[0];
|
|
837
|
+
if (!first)
|
|
838
|
+
return result;
|
|
839
|
+
if (first.type !== "text" || typeof first.text !== "string") {
|
|
840
|
+
return obj.content;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
return JSON.parse(first.text);
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
return first.text;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Coerce a string literal into its natural JS type when the shape is
|
|
851
|
+
* unambiguous. v1: bracket-list `[a, b, c]` → array. Other shapes pass through.
|
|
852
|
+
*/
|
|
853
|
+
function coerceLiteralValue(raw) {
|
|
854
|
+
const trimmed = raw.trim();
|
|
855
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
|
|
856
|
+
return raw;
|
|
857
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
858
|
+
if (inner === "")
|
|
859
|
+
return [];
|
|
860
|
+
return inner.split(",").map((s) => {
|
|
861
|
+
const t = s.trim();
|
|
862
|
+
if (t.length >= 2) {
|
|
863
|
+
const first = t[0];
|
|
864
|
+
const last = t[t.length - 1];
|
|
865
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
866
|
+
return t.slice(1, -1);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return t;
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
// ─── Substitution and condition evaluation (runtime-side) ─────────────────
|
|
873
|
+
/**
|
|
874
|
+
* Variant for `@ unsafe` op bodies. The `$$(...)` escape lets authors send
|
|
875
|
+
* `$(...)` literally to bash (for bash command-substitution); skillscript
|
|
876
|
+
* substitution sees `$$` and collapses to `$`. `$(NAME)` (single `$`)
|
|
877
|
+
* remains a skillscript variable substitution.
|
|
878
|
+
*/
|
|
879
|
+
export function substituteRuntimeUnsafe(text, vars) {
|
|
880
|
+
// Step 1: pull `$$(` escapes out so step 2's regex doesn't see the inner $.
|
|
881
|
+
const ESCAPE = "DOLLAR_DOLLAR_PAREN";
|
|
882
|
+
const escaped = text.replace(/\$\$\(/g, ESCAPE);
|
|
883
|
+
// Step 2: normal skillscript substitution against the de-escaped text.
|
|
884
|
+
const substituted = substituteRuntime(escaped, vars);
|
|
885
|
+
// Step 3: restore the escape as literal `$(` for bash.
|
|
886
|
+
return substituted.replace(new RegExp(ESCAPE, "g"), "$(");
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Runtime `$(NAME[|filter])` substitution. At runtime the full variable
|
|
890
|
+
* state is in scope; unresolved refs are a hard error (compile-time leaves
|
|
891
|
+
* them to pass through; runtime can't).
|
|
892
|
+
*/
|
|
893
|
+
export function substituteRuntime(text, vars) {
|
|
894
|
+
return text.replace(/\$\(([^|)\s]+)\s*(?:\|\s*([A-Za-z_]\w*))?\s*\)/g, (_match, ref, filter) => {
|
|
895
|
+
const value = resolveRef(ref, vars);
|
|
896
|
+
if (value === undefined) {
|
|
897
|
+
throw new UnresolvedVariableError(ref, "?");
|
|
898
|
+
}
|
|
899
|
+
const s = stringifyValue(value);
|
|
900
|
+
if (!filter)
|
|
901
|
+
return s;
|
|
902
|
+
return applyFilter(s, filter);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Marker symbol for mechanical-mode placeholder objects. Tagged proxies
|
|
907
|
+
* stringify to their label when consumed by `stringifyValue` (used by
|
|
908
|
+
* substituteRuntime), so dotted access like `$(ISSUE.title)` works in
|
|
909
|
+
* mechanical mode even though no real dispatch happened — every property
|
|
910
|
+
* access produces a child placeholder.
|
|
911
|
+
*/
|
|
912
|
+
const MECHANICAL_PLACEHOLDER = Symbol.for("skillscript.mechanical_placeholder");
|
|
913
|
+
/**
|
|
914
|
+
* Build a mechanical-mode placeholder. Acts like an object whose properties
|
|
915
|
+
* are also placeholders (recursive), but `stringifyValue` unwraps it to
|
|
916
|
+
* the literal label string. Lets cold-agent skills that use `$(VAR.field)`
|
|
917
|
+
* patterns execute end-to-end in mechanical mode without infrastructure.
|
|
918
|
+
*/
|
|
919
|
+
function makeMechanicalPlaceholder(label) {
|
|
920
|
+
const target = { [MECHANICAL_PLACEHOLDER]: label };
|
|
921
|
+
return new Proxy(target, {
|
|
922
|
+
get(target, key) {
|
|
923
|
+
if (key === MECHANICAL_PLACEHOLDER)
|
|
924
|
+
return label;
|
|
925
|
+
// Symbol-keyed access (Symbol.iterator, Symbol.toPrimitive, etc.):
|
|
926
|
+
// return the target's own value so JS internals see a plain object.
|
|
927
|
+
if (typeof key === "symbol")
|
|
928
|
+
return Reflect.get(target, key);
|
|
929
|
+
// String-keyed access: synthesize a deeper placeholder.
|
|
930
|
+
return makeMechanicalPlaceholder(`${label}.${String(key)}`);
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function isMechanicalPlaceholder(v) {
|
|
935
|
+
return v !== null && typeof v === "object" && v[MECHANICAL_PLACEHOLDER] !== undefined;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Resolve `$(NAME)` or `$(NAME.path)` against the variable map. Two strategies:
|
|
939
|
+
* 1. Flat-key match (full ref including dots). Handles `targetname.output`.
|
|
940
|
+
* 2. Dot-path traversal — split, descend.
|
|
941
|
+
* Returns `undefined` when unresolved.
|
|
942
|
+
*/
|
|
943
|
+
export function resolveRef(ref, vars) {
|
|
944
|
+
if (vars.has(ref))
|
|
945
|
+
return vars.get(ref);
|
|
946
|
+
const path = ref.split(".");
|
|
947
|
+
const root = path[0];
|
|
948
|
+
if (!vars.has(root))
|
|
949
|
+
return undefined;
|
|
950
|
+
let cur = vars.get(root);
|
|
951
|
+
for (let i = 1; i < path.length; i++) {
|
|
952
|
+
if (cur === null || cur === undefined)
|
|
953
|
+
return undefined;
|
|
954
|
+
if (typeof cur !== "object")
|
|
955
|
+
return undefined;
|
|
956
|
+
cur = cur[path[i]];
|
|
957
|
+
}
|
|
958
|
+
return cur;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Render a value for inline substitution. Scalars stringify naturally;
|
|
962
|
+
* objects/arrays JSON-serialize. `null` renders as the literal `"null"` so
|
|
963
|
+
* authors can distinguish bound-to-null from unresolved.
|
|
964
|
+
*/
|
|
965
|
+
export function stringifyValue(v) {
|
|
966
|
+
if (typeof v === "string")
|
|
967
|
+
return v;
|
|
968
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
969
|
+
return String(v);
|
|
970
|
+
if (v === null)
|
|
971
|
+
return "null";
|
|
972
|
+
if (isMechanicalPlaceholder(v))
|
|
973
|
+
return v[MECHANICAL_PLACEHOLDER];
|
|
974
|
+
return JSON.stringify(v);
|
|
975
|
+
}
|
|
976
|
+
const TRUTHY = /^\s*\$\(([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)(?:\s*\|\s*([A-Za-z_]\w*))?\)\s*$/;
|
|
977
|
+
const EQ = /^\s*\$\(([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)(?:\s*\|\s*([A-Za-z_]\w*))?\)\s*(==|!=)\s*"([^"]*)"\s*$/;
|
|
978
|
+
/** Ref-vs-ref equality (per language reference §5 + 2026-05-21 grammar extension). Filter + dotted-field-access permitted on either side. */
|
|
979
|
+
const EQ_REF = /^\s*\$\(([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)(?:\s*\|\s*([A-Za-z_]\w*))?\)\s*(==|!=)\s*\$\(([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)(?:\s*\|\s*([A-Za-z_]\w*))?\)\s*$/;
|
|
980
|
+
const IN = /^\s*\$\(([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)(?:\s*\|\s*([A-Za-z_]\w*))?\)\s+(not\s+)?in\s+\$\(([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\)\s*$/;
|
|
981
|
+
export function evalCondition(cond, vars) {
|
|
982
|
+
const t = TRUTHY.exec(cond);
|
|
983
|
+
if (t) {
|
|
984
|
+
const val = resolveRef(t[1], vars);
|
|
985
|
+
const filter = t[2];
|
|
986
|
+
const filtered = filter && val !== undefined ? applyFilter(stringifyValue(val), filter) : val;
|
|
987
|
+
return isTruthy(filtered);
|
|
988
|
+
}
|
|
989
|
+
const e = EQ.exec(cond);
|
|
990
|
+
if (e) {
|
|
991
|
+
const [, ref, filter, op, lit] = e;
|
|
992
|
+
const val = resolveRef(ref, vars);
|
|
993
|
+
const valStr = val === undefined ? "" : stringifyValue(val);
|
|
994
|
+
// Filter applies BEFORE comparison so `if $(COLOR|trim) == "yellow"` matches
|
|
995
|
+
// values that local models return with trailing whitespace.
|
|
996
|
+
const final = filter !== undefined ? applyFilter(valStr, filter) : valStr;
|
|
997
|
+
return op === "==" ? final === lit : final !== lit;
|
|
998
|
+
}
|
|
999
|
+
const eRef = EQ_REF.exec(cond);
|
|
1000
|
+
if (eRef) {
|
|
1001
|
+
const [, lhsRef, lhsFilter, op, rhsRef, rhsFilter] = eRef;
|
|
1002
|
+
// Both sides resolve via the same path as `EQ` LHS — undefined → ""
|
|
1003
|
+
// (matches the existing tolerance for unresolved refs in conditions).
|
|
1004
|
+
const lhsVal = resolveRef(lhsRef, vars);
|
|
1005
|
+
const rhsVal = resolveRef(rhsRef, vars);
|
|
1006
|
+
const lhsStr = lhsVal === undefined ? "" : stringifyValue(lhsVal);
|
|
1007
|
+
const rhsStr = rhsVal === undefined ? "" : stringifyValue(rhsVal);
|
|
1008
|
+
const lhsFinal = lhsFilter !== undefined ? applyFilter(lhsStr, lhsFilter) : lhsStr;
|
|
1009
|
+
const rhsFinal = rhsFilter !== undefined ? applyFilter(rhsStr, rhsFilter) : rhsStr;
|
|
1010
|
+
return op === "==" ? lhsFinal === rhsFinal : lhsFinal !== rhsFinal;
|
|
1011
|
+
}
|
|
1012
|
+
const i = IN.exec(cond);
|
|
1013
|
+
if (i) {
|
|
1014
|
+
const [, lhsRef, lhsFilter, notKey, rhsRef] = i;
|
|
1015
|
+
let rhsVal = resolveRef(rhsRef, vars);
|
|
1016
|
+
if (rhsVal === undefined) {
|
|
1017
|
+
throw new Error(`Runtime error in \`in\` condition: RHS \`$(${rhsRef})\` is unresolved`);
|
|
1018
|
+
}
|
|
1019
|
+
// Cold-agent corpus tolerance: model responses (`~` op) are strings;
|
|
1020
|
+
// when the author prompts for a JSON array and uses it as `in` RHS,
|
|
1021
|
+
// auto-parse the string to its array form. Matches how foreach's
|
|
1022
|
+
// resolveListExpr tolerates JSON-string list expressions. Strings
|
|
1023
|
+
// that don't JSON-parse to an array still error below as before.
|
|
1024
|
+
//
|
|
1025
|
+
// Mechanical-mode special-case: placeholder strings ("[mechanical:...]")
|
|
1026
|
+
// are treated as single-element arrays so `in` checks execute
|
|
1027
|
+
// structurally without false errors during dry-run validation.
|
|
1028
|
+
if (typeof rhsVal === "string") {
|
|
1029
|
+
if (rhsVal.startsWith("[mechanical:")) {
|
|
1030
|
+
rhsVal = [rhsVal];
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
try {
|
|
1034
|
+
const parsed = JSON.parse(rhsVal);
|
|
1035
|
+
if (Array.isArray(parsed))
|
|
1036
|
+
rhsVal = parsed;
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
/* not JSON — fall through to the array-check error */
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (!Array.isArray(rhsVal)) {
|
|
1044
|
+
const got = rhsVal === null ? "null" : typeof rhsVal;
|
|
1045
|
+
throw new Error(`Runtime error in \`in\` condition: RHS \`$(${rhsRef})\` must be an array (got ${got})`);
|
|
1046
|
+
}
|
|
1047
|
+
const lhsVal = resolveRef(lhsRef, vars);
|
|
1048
|
+
if (lhsVal === undefined)
|
|
1049
|
+
return false;
|
|
1050
|
+
const lhsStr = lhsFilter !== undefined
|
|
1051
|
+
? applyFilter(stringifyValue(lhsVal), lhsFilter)
|
|
1052
|
+
: stringifyValue(lhsVal);
|
|
1053
|
+
const found = rhsVal.some((item) => stringifyValue(item) === lhsStr);
|
|
1054
|
+
return notKey !== undefined ? !found : found;
|
|
1055
|
+
}
|
|
1056
|
+
throw new Error(`Invalid runtime condition (parser should have rejected): ${cond}`);
|
|
1057
|
+
}
|
|
1058
|
+
function isTruthy(v) {
|
|
1059
|
+
if (v === undefined || v === null)
|
|
1060
|
+
return false;
|
|
1061
|
+
if (typeof v === "string")
|
|
1062
|
+
return v.length > 0;
|
|
1063
|
+
if (typeof v === "number")
|
|
1064
|
+
return v !== 0;
|
|
1065
|
+
if (typeof v === "boolean")
|
|
1066
|
+
return v;
|
|
1067
|
+
if (Array.isArray(v))
|
|
1068
|
+
return v.length > 0;
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
//# sourceMappingURL=runtime.js.map
|