skillscript-runtime 0.15.7 → 0.16.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.
- package/README.md +23 -24
- package/dist/compile.js +25 -71
- package/dist/compile.js.map +1 -1
- package/dist/errors.d.ts +2 -7
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -14
- package/dist/errors.js.map +1 -1
- package/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +74 -103
- package/dist/help-content.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +43 -214
- package/dist/lint.js.map +1 -1
- package/dist/mutation-gate.d.ts +7 -13
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +6 -11
- package/dist/mutation-gate.js.map +1 -1
- package/dist/parser.d.ts +25 -74
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +30 -264
- package/dist/parser.js.map +1 -1
- package/dist/runtime.d.ts +1 -10
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +76 -188
- package/dist/runtime.js.map +1 -1
- package/dist/skill-manager.js +1 -1
- package/dist/skill-manager.js.map +1 -1
- package/docs/language-reference.md +3 -24
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/package.json +1 -1
- package/examples/skillscripts/cut-release-tag.skill.md +0 -40
package/dist/lint.js
CHANGED
|
@@ -257,11 +257,11 @@ const UNDECLARED_VAR = {
|
|
|
257
257
|
for (const [targetName, target] of ctx.parsed.targets) {
|
|
258
258
|
const reported = new Set(); // dedupe per target
|
|
259
259
|
for (const op of target.ops) {
|
|
260
|
-
//
|
|
260
|
+
// Unsafe `shell` ops use bash `$(...)` syntax — handled by
|
|
261
261
|
// unsafe-shell-ambiguous-subst, which offers the dual rewrite
|
|
262
262
|
// (`$$(NAME)` for bash, `$(KNOWN_VAR)` for skillscript). Skip here
|
|
263
263
|
// to avoid double-reporting.
|
|
264
|
-
if (op.kind === "
|
|
264
|
+
if (op.kind === "shell" && op.policy === "unsafe")
|
|
265
265
|
continue;
|
|
266
266
|
for (const ref of extractVarRefs(op)) {
|
|
267
267
|
// Heuristic: dotted refs (targetname.output, MEMORY.field) pass
|
|
@@ -321,9 +321,9 @@ const MALFORMED_OP_GRAMMAR = {
|
|
|
321
321
|
id: "malformed-op-grammar",
|
|
322
322
|
severity: "error",
|
|
323
323
|
description: "An op line failed parser grammar validation. Surfaces parse errors that originate from op-specific shape.",
|
|
324
|
-
remediation: "Check the op's syntax against the language reference.
|
|
324
|
+
remediation: "Check the op's syntax against the language reference. `$ tool key=value ... -> VAR` for MCP dispatch; `verb(kwarg=value, ...) [-> VAR]` for runtime intrinsics.",
|
|
325
325
|
check: (ctx) => ctx.parsed.parseErrors
|
|
326
|
-
.filter((msg) => /Malformed `[
|
|
326
|
+
.filter((msg) => /Malformed `\$|Malformed function-call|Legacy `[~>&@!?]/.test(msg))
|
|
327
327
|
.map((msg) => ({
|
|
328
328
|
rule: "malformed-op-grammar",
|
|
329
329
|
severity: "error",
|
|
@@ -430,40 +430,6 @@ const UNKNOWN_SKILL_REFERENCE = {
|
|
|
430
430
|
return findings;
|
|
431
431
|
},
|
|
432
432
|
};
|
|
433
|
-
// v0.2.12 Bug 26. Tier-2 warning when a `>` retrieval op carries kwargs
|
|
434
|
-
// outside the documented set. Cold author wrote `since=1h` (hallucinated
|
|
435
|
-
// time-window predicate) and the kwarg passed silently. The documented
|
|
436
|
-
// kwarg set is mode/query/limit/connector/fallback plus filter shapes
|
|
437
|
-
// the connector advertises via staticCapabilities (out of band here).
|
|
438
|
-
const KNOWN_RETRIEVAL_KWARGS = new Set(["mode", "query", "limit", "connector", "fallback"]);
|
|
439
|
-
const UNKNOWN_RETRIEVAL_ARG = {
|
|
440
|
-
id: "unknown-retrieval-arg",
|
|
441
|
-
severity: "warning",
|
|
442
|
-
description: "A `>` retrieval op carries a kwarg outside the documented set (mode/query/limit/connector/fallback).",
|
|
443
|
-
remediation: "Remove the kwarg, or check it against your DataStore connector's documentation — extras pass through to the connector but unrecognized ones often indicate hallucinated syntax.",
|
|
444
|
-
check: (ctx) => {
|
|
445
|
-
const findings = [];
|
|
446
|
-
for (const [targetName, target] of ctx.parsed.targets) {
|
|
447
|
-
walkOps(target.ops, (op) => {
|
|
448
|
-
if (op.kind !== ">")
|
|
449
|
-
return;
|
|
450
|
-
const extras = op.retrievalParams?.extra ?? {};
|
|
451
|
-
for (const k of Object.keys(extras)) {
|
|
452
|
-
if (KNOWN_RETRIEVAL_KWARGS.has(k))
|
|
453
|
-
continue;
|
|
454
|
-
findings.push({
|
|
455
|
-
rule: "unknown-retrieval-arg",
|
|
456
|
-
severity: "warning",
|
|
457
|
-
message: `\`>\` op in target '${targetName}' carries unknown kwarg '${k}'. Documented kwargs: ${Array.from(KNOWN_RETRIEVAL_KWARGS).join(", ")}. If '${k}' is a connector-specific filter, confirm it against the connector's docs.`,
|
|
458
|
-
block: targetName,
|
|
459
|
-
extras: { unknown_kwarg: k },
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
return findings;
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
433
|
// v0.4.0 — `$ name.tool` references a connector name not registered.
|
|
468
434
|
// `connectorNames` is the authoritative list from the Registry (passed
|
|
469
435
|
// through LintOptions); when undefined (caller doesn't know what's
|
|
@@ -685,11 +651,19 @@ const UNVERIFIED_QUALIFIED_TOOL = {
|
|
|
685
651
|
// the lint message didn't explain the layering. Now: if all sites have
|
|
686
652
|
// fallback → info with layering explanation; if any site lacks it → error
|
|
687
653
|
// (unchanged).
|
|
654
|
+
// v0.16.0 — bare-form `$ <tool>` is reserved for runtime-intrinsic ops + tools
|
|
655
|
+
// whose name name-matches a registered MCP connector (the typed-contract pattern,
|
|
656
|
+
// e.g. `$ llm prompt=...` dispatches to the `llm` connector). Substrate-specific
|
|
657
|
+
// MCP tool dispatch (e.g. `amp_olsen_task`) MUST use named form `$ <connector>.<tool>`
|
|
658
|
+
// — no more "primary fallback" leniency. Closes the discipline-only-contract gap
|
|
659
|
+
// where adopters wrote bare-form expecting `primary` and silent-failed on cron-fired
|
|
660
|
+
// paths when primary wasn't registered.
|
|
661
|
+
const BARE_FORM_INTRINSICS = new Set(["execute_skill", "json_parse"]);
|
|
688
662
|
const UNWIRED_PRIMARY_CONNECTOR = {
|
|
689
663
|
id: "unwired-primary-connector",
|
|
690
664
|
severity: "error",
|
|
691
|
-
description: "A bare `$ TOOL` op (no connector prefix) routes to
|
|
692
|
-
remediation: "
|
|
665
|
+
description: "A bare `$ TOOL` op (no connector prefix) routes only to (a) a runtime-intrinsic op or (b) a wired connector whose name matches the op name. v0.16.0 removed the `primary` fallback — substrate-specific MCP tools require named form `$ <connector>.<tool>`.",
|
|
666
|
+
remediation: "Use named form `$ <connector>.<tool>` for substrate-specific MCP dispatch (e.g., `$ amp.amp_olsen_task` instead of `$ amp_olsen_task`). Bare form is reserved for runtime intrinsics (`execute_skill`, `json_parse`) and typed-contract ops where the tool name matches a wired connector (e.g., `$ llm prompt=...` if `llm` is wired).",
|
|
693
667
|
check: (ctx) => {
|
|
694
668
|
if (ctx.mcpConnectorNames === undefined)
|
|
695
669
|
return [];
|
|
@@ -700,22 +674,17 @@ const UNWIRED_PRIMARY_CONNECTOR = {
|
|
|
700
674
|
walkOps(target.ops, (op) => {
|
|
701
675
|
if (op.kind !== "$" || op.mcpConnector !== undefined)
|
|
702
676
|
return;
|
|
703
|
-
// Built-in intercepts that don't need a connector at all.
|
|
704
677
|
const m = /^([A-Za-z_][\w:-]*)/.exec(op.body);
|
|
705
678
|
if (m === null)
|
|
706
679
|
return;
|
|
707
680
|
const toolName = m[1];
|
|
708
|
-
if (toolName
|
|
681
|
+
if (BARE_FORM_INTRINSICS.has(toolName))
|
|
709
682
|
return;
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
// fix to prevent the v0.7.2-shape regression of lint-fails-then-
|
|
714
|
-
// user-never-reaches-runtime.)
|
|
683
|
+
// Name-match: bare op name matches a wired connector → typed-contract
|
|
684
|
+
// pattern (e.g., `$ llm` against wired `llm` connector). Runtime
|
|
685
|
+
// dispatch resolver routes directly.
|
|
715
686
|
if (ctx.mcpConnectorNames.includes(toolName))
|
|
716
687
|
return;
|
|
717
|
-
if (ctx.mcpConnectorNames.includes("primary"))
|
|
718
|
-
return;
|
|
719
688
|
const key = `${targetName}:${toolName}`;
|
|
720
689
|
const hasFallback = op.fallback !== undefined;
|
|
721
690
|
const existing = groups.get(key);
|
|
@@ -742,7 +711,7 @@ const UNWIRED_PRIMARY_CONNECTOR = {
|
|
|
742
711
|
findings.push({
|
|
743
712
|
rule: "unwired-primary-connector",
|
|
744
713
|
severity: "error",
|
|
745
|
-
message: `\`$ ${toolName}\` in target '${targetName}' is a
|
|
714
|
+
message: `\`$ ${toolName}\` in target '${targetName}' is bare-form but '${toolName}' is not a runtime intrinsic AND doesn't match any wired MCP connector. Bare form is reserved for typed-contract / intrinsic ops. Use named form \`$ <connector>.${toolName}\` for substrate-specific MCP dispatch. Wired connectors: ${wired}.`,
|
|
746
715
|
block: targetName,
|
|
747
716
|
extras: { tool: toolName },
|
|
748
717
|
});
|
|
@@ -809,14 +778,6 @@ const UNPARSED_JSON_FIELD_ACCESS = {
|
|
|
809
778
|
for (const b of op.ifBranches)
|
|
810
779
|
reportIfMatches(b.cond, targetName);
|
|
811
780
|
}
|
|
812
|
-
if (op.retrievalParams !== undefined) {
|
|
813
|
-
reportIfMatches(op.retrievalParams.query, targetName);
|
|
814
|
-
for (const v of Object.values(op.retrievalParams.extra))
|
|
815
|
-
reportIfMatches(String(v), targetName);
|
|
816
|
-
}
|
|
817
|
-
if (op.localModelParams !== undefined) {
|
|
818
|
-
reportIfMatches(op.localModelParams.prompt, targetName);
|
|
819
|
-
}
|
|
820
781
|
if (op.ampParams !== undefined) {
|
|
821
782
|
for (const v of Object.values(op.ampParams.args))
|
|
822
783
|
reportIfMatches(v, targetName);
|
|
@@ -1013,11 +974,11 @@ const MISSING_SKILLSTORE_FOR_DATA_REF = {
|
|
|
1013
974
|
const findings = [];
|
|
1014
975
|
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1015
976
|
for (const op of target.ops) {
|
|
1016
|
-
if (op.kind === "
|
|
977
|
+
if (op.kind === "inline") {
|
|
1017
978
|
findings.push({
|
|
1018
979
|
rule: "missing-skillstore-for-data-ref",
|
|
1019
980
|
severity: "error",
|
|
1020
|
-
message: `Skill references skill '${op.ampParams?.skillName ?? "(unknown)"}' via
|
|
981
|
+
message: `Skill references skill '${op.ampParams?.skillName ?? "(unknown)"}' via \`inline(...)\`, but lint was invoked without a SkillStore (call site: ${ctx.callSite}). Data-skill inlining will silently skip; the \`inline\` op will survive into the runtime and error.`,
|
|
1021
982
|
block: targetName,
|
|
1022
983
|
extras: { call_site: ctx.callSite },
|
|
1023
984
|
});
|
|
@@ -1088,7 +1049,7 @@ const UNSAFE_SHELL_AMBIGUOUS_SUBST = {
|
|
|
1088
1049
|
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1089
1050
|
const reported = new Set();
|
|
1090
1051
|
walkOps(target.ops, (op) => {
|
|
1091
|
-
if (op.kind !== "
|
|
1052
|
+
if (op.kind !== "shell" || op.policy !== "unsafe")
|
|
1092
1053
|
return;
|
|
1093
1054
|
const re = new RegExp(REF_RE.source, "g");
|
|
1094
1055
|
let m;
|
|
@@ -1137,7 +1098,7 @@ const UNSAFE_SHELL_OP = {
|
|
|
1137
1098
|
const findings = [];
|
|
1138
1099
|
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1139
1100
|
walkOps(target.ops, (op) => {
|
|
1140
|
-
if (op.kind === "
|
|
1101
|
+
if (op.kind === "shell" && op.policy === "unsafe") {
|
|
1141
1102
|
findings.push({
|
|
1142
1103
|
rule: "unsafe-shell-op",
|
|
1143
1104
|
severity: "warning",
|
|
@@ -1173,7 +1134,7 @@ const UNSAFE_SHELL_DISABLED = {
|
|
|
1173
1134
|
const findings = [];
|
|
1174
1135
|
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1175
1136
|
walkOps(target.ops, (op) => {
|
|
1176
|
-
if (op.kind === "
|
|
1137
|
+
if (op.kind === "shell" && op.policy === "unsafe") {
|
|
1177
1138
|
findings.push({
|
|
1178
1139
|
rule: "unsafe-shell-disabled",
|
|
1179
1140
|
severity: "error",
|
|
@@ -1202,8 +1163,8 @@ const UNSAFE_SHELL_DISABLED = {
|
|
|
1202
1163
|
const UNCONFIRMED_MUTATION = {
|
|
1203
1164
|
id: "unconfirmed-mutation",
|
|
1204
1165
|
severity: "warning",
|
|
1205
|
-
description: "A mutation-class op runs without author authorization. Mutation classes: `$ tool` with mutating-name shape (write/update/delete/...); `$ data_write` MCP dispatch; `file_write(...)` function-call op. Silent when the skill declares `# Autonomous: true`
|
|
1206
|
-
remediation: "
|
|
1166
|
+
description: "A mutation-class op runs without author authorization. Mutation classes: `$ tool` with mutating-name shape (write/update/delete/...); `$ data_write` MCP dispatch; `file_write(...)` function-call op. Silent when the skill declares `# Autonomous: true` or when the op carries `approved=\"reason\"` per-op authorization.",
|
|
1167
|
+
remediation: "Two ways to authorize: (1) add `# Autonomous: true` at the skill header for cron/agent-fired skills; (2) pass `approved=\"reason\"` kwarg on the mutation op itself (any non-empty string; presence is what matters, value not parsed semantically).",
|
|
1207
1168
|
check: (ctx) => {
|
|
1208
1169
|
const skillAutonomous = ctx.parsed.autonomous === true;
|
|
1209
1170
|
// v0.4.2 — `# Autonomous: true` skills are unattended by design;
|
|
@@ -1214,12 +1175,8 @@ const UNCONFIRMED_MUTATION = {
|
|
|
1214
1175
|
return [];
|
|
1215
1176
|
const findings = [];
|
|
1216
1177
|
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1217
|
-
const authState = { skillAutonomous
|
|
1178
|
+
const authState = { skillAutonomous };
|
|
1218
1179
|
for (const op of target.ops) {
|
|
1219
|
-
// v0.7.0+: ask(prompt=...) parses to kind "??" in the AST, so the
|
|
1220
|
-
// same check captures both legacy `??` and canonical `ask()`.
|
|
1221
|
-
if (op.kind === "??")
|
|
1222
|
-
authState.sawConfirm = true;
|
|
1223
1180
|
if (authorizationGranted(op, authState))
|
|
1224
1181
|
continue;
|
|
1225
1182
|
const classification = classifyMutation(op);
|
|
@@ -1229,7 +1186,7 @@ const UNCONFIRMED_MUTATION = {
|
|
|
1229
1186
|
findings.push({
|
|
1230
1187
|
rule: "unconfirmed-mutation",
|
|
1231
1188
|
severity: "warning",
|
|
1232
|
-
message: `\`file_write(path="${classification.detail}")\` in target '${targetName}' is a mutation op without authorization. Add \`approved="..."\` kwarg
|
|
1189
|
+
message: `\`file_write(path="${classification.detail}")\` in target '${targetName}' is a mutation op without authorization. Add \`approved="..."\` kwarg or declare \`# Autonomous: true\`.`,
|
|
1233
1190
|
block: targetName,
|
|
1234
1191
|
extras: { op_kind: "file_write", path: classification.detail },
|
|
1235
1192
|
});
|
|
@@ -1238,7 +1195,7 @@ const UNCONFIRMED_MUTATION = {
|
|
|
1238
1195
|
findings.push({
|
|
1239
1196
|
rule: "unconfirmed-mutation",
|
|
1240
1197
|
severity: "warning",
|
|
1241
|
-
message: `\`$\` op in target '${targetName}' invokes '${classification.detail}' (mutating shape) without authorization. Add \`approved="..."\` kwarg
|
|
1198
|
+
message: `\`$\` op in target '${targetName}' invokes '${classification.detail}' (mutating shape) without authorization. Add \`approved="..."\` kwarg or declare \`# Autonomous: true\`.`,
|
|
1242
1199
|
block: targetName,
|
|
1243
1200
|
extras: { tool_name: classification.detail },
|
|
1244
1201
|
});
|
|
@@ -1248,41 +1205,6 @@ const UNCONFIRMED_MUTATION = {
|
|
|
1248
1205
|
return findings;
|
|
1249
1206
|
},
|
|
1250
1207
|
};
|
|
1251
|
-
const MODEL_CONTENTION = {
|
|
1252
|
-
id: "model-contention",
|
|
1253
|
-
severity: "warning",
|
|
1254
|
-
description: "Skill body has a `$` op dispatching async batch work on a model + a downstream `~ model=X` synchronous call to the same model. The runtime serializes per-model; the sync call queues behind the batch.",
|
|
1255
|
-
remediation: "Use distinct models for async vs sync work: e.g., `gemma2` for batch + `qwen` for the interactive verdict. See ERD §3 model selection convention.",
|
|
1256
|
-
check: (ctx) => {
|
|
1257
|
-
const findings = [];
|
|
1258
|
-
// Heuristic: collect ~ op model names per target. Flag if any $ op
|
|
1259
|
-
// in the same target dispatches a batch-classification-shaped tool
|
|
1260
|
-
// (name contains "olsen", "scan", "batch", "classify"). Conservative.
|
|
1261
|
-
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1262
|
-
const syncModels = new Set();
|
|
1263
|
-
walkOps(target.ops, (op) => {
|
|
1264
|
-
if (op.kind === "~" && op.localModelParams?.model)
|
|
1265
|
-
syncModels.add(op.localModelParams.model);
|
|
1266
|
-
});
|
|
1267
|
-
if (syncModels.size === 0)
|
|
1268
|
-
return findings;
|
|
1269
|
-
walkOps(target.ops, (op) => {
|
|
1270
|
-
if (op.kind !== "$")
|
|
1271
|
-
return;
|
|
1272
|
-
const toolName = op.body.split(/\s+/)[0] ?? "";
|
|
1273
|
-
if (/scan|batch|classify|atomize/i.test(toolName)) {
|
|
1274
|
-
findings.push({
|
|
1275
|
-
rule: "model-contention",
|
|
1276
|
-
severity: "warning",
|
|
1277
|
-
message: `Target '${targetName}' dispatches batch work via '${toolName}' AND uses sync \`~ model=...\` — possible model contention on the same backend.`,
|
|
1278
|
-
block: targetName,
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
return findings;
|
|
1284
|
-
},
|
|
1285
|
-
};
|
|
1286
1208
|
const DRAFT_WITH_TRIGGER = {
|
|
1287
1209
|
id: "draft-with-trigger",
|
|
1288
1210
|
severity: "warning",
|
|
@@ -1523,7 +1445,7 @@ const OUTPUT_AGENT_TARGET_NO_EMIT = {
|
|
|
1523
1445
|
return findings;
|
|
1524
1446
|
let hasEmit = false;
|
|
1525
1447
|
for (const [, target] of ctx.parsed.targets) {
|
|
1526
|
-
walkOps(target.ops, (op) => { if (op.kind === "
|
|
1448
|
+
walkOps(target.ops, (op) => { if (op.kind === "emit")
|
|
1527
1449
|
hasEmit = true; });
|
|
1528
1450
|
if (hasEmit)
|
|
1529
1451
|
break;
|
|
@@ -2055,12 +1977,8 @@ function buildBindingOrigins(parsed) {
|
|
|
2055
1977
|
if (op.outputVar !== undefined) {
|
|
2056
1978
|
if (op.kind === "$")
|
|
2057
1979
|
origins.set(op.outputVar, { kind: "op-output", op: "$" });
|
|
2058
|
-
else if (op.kind === "
|
|
2059
|
-
origins.set(op.outputVar, { kind: "op-output", op: "
|
|
2060
|
-
else if (op.kind === ">")
|
|
2061
|
-
origins.set(op.outputVar, { kind: "op-output", op: ">" });
|
|
2062
|
-
else if (op.kind === "@")
|
|
2063
|
-
origins.set(op.outputVar, { kind: "op-output", op: "@" });
|
|
1980
|
+
else if (op.kind === "shell")
|
|
1981
|
+
origins.set(op.outputVar, { kind: "op-output", op: "shell" });
|
|
2064
1982
|
}
|
|
2065
1983
|
if (op.kind === "foreach" && op.foreachIter !== undefined) {
|
|
2066
1984
|
origins.set(op.foreachIter, { kind: "foreach-iter" });
|
|
@@ -2136,14 +2054,13 @@ const UNQUOTED_SUBSTITUTION_IN_KWARG_VALUE = {
|
|
|
2136
2054
|
});
|
|
2137
2055
|
}
|
|
2138
2056
|
}
|
|
2139
|
-
else if (op.kind === "
|
|
2140
|
-
//
|
|
2141
|
-
//
|
|
2142
|
-
//
|
|
2143
|
-
//
|
|
2057
|
+
else if (op.kind === "shell") {
|
|
2058
|
+
// Tokenize the shell body the same way the runtime would
|
|
2059
|
+
// (whitespace-separated, quotes respected), then flag any token
|
|
2060
|
+
// that is a bare unquoted substitution. Quoted tokens
|
|
2061
|
+
// (`"${VAR}"` / `'${VAR}'`) are safe.
|
|
2144
2062
|
const tokens = tokenizeKeywordArgs(op.body);
|
|
2145
2063
|
for (const tok of tokens) {
|
|
2146
|
-
// Skip quoted tokens — the quotes protect against whitespace split.
|
|
2147
2064
|
if ((tok.startsWith('"') && tok.endsWith('"')) || (tok.startsWith("'") && tok.endsWith("'")))
|
|
2148
2065
|
continue;
|
|
2149
2066
|
if (!(tok.startsWith("$(") || tok.startsWith("${")))
|
|
@@ -2156,16 +2073,16 @@ const UNQUOTED_SUBSTITUTION_IN_KWARG_VALUE = {
|
|
|
2156
2073
|
const origin = origins.get(rootVar);
|
|
2157
2074
|
if (!isOriginSuspect(origin))
|
|
2158
2075
|
continue;
|
|
2159
|
-
const dedupKey = `${targetName}
|
|
2076
|
+
const dedupKey = `${targetName}:shell:${varName}`;
|
|
2160
2077
|
if (reported.has(dedupKey))
|
|
2161
2078
|
continue;
|
|
2162
2079
|
reported.add(dedupKey);
|
|
2163
2080
|
findings.push({
|
|
2164
2081
|
rule: "unquoted-substitution-in-kwarg-value",
|
|
2165
2082
|
severity: "warning",
|
|
2166
|
-
message:
|
|
2083
|
+
message: `\`shell(command="... \${${varName}} ...")\` in target '${targetName}': unquoted substitution. ${describeOriginRisk(origin)} Wrap as \`"\${${varName}}"\` to prevent silent word-splitting if the value contains whitespace.`,
|
|
2167
2084
|
block: targetName,
|
|
2168
|
-
extras: { var_name: varName, origin: origin.kind, op: "
|
|
2085
|
+
extras: { var_name: varName, origin: origin.kind, op: "shell" },
|
|
2169
2086
|
});
|
|
2170
2087
|
}
|
|
2171
2088
|
}
|
|
@@ -2188,86 +2105,6 @@ function describeOriginRisk(origin) {
|
|
|
2188
2105
|
return `Variable is a \`foreach\` iterator — element type unknown statically.`;
|
|
2189
2106
|
}
|
|
2190
2107
|
}
|
|
2191
|
-
/**
|
|
2192
|
-
* v0.7.1 — tier-2 visibility nudge for legacy symbol-form ops (`~`, `>`,
|
|
2193
|
-
* `@`, `!`, `??`, `&`). The parser still accepts these during the v0.7.x
|
|
2194
|
-
* grace period; the runtime dispatches them as before. This rule surfaces
|
|
2195
|
-
* the canonical replacement so authors editing skills see the migration
|
|
2196
|
-
* path. Tier-1 promotion (refuse-to-compile) lands in v0.8 or v0.9 once
|
|
2197
|
-
* the adopter ecosystem confirms migration is settled.
|
|
2198
|
-
*
|
|
2199
|
-
* The `$ tool` op is NOT flagged — that shape is canonical (MCP dispatch
|
|
2200
|
-
* marker). Only the 6 symbol ops that became function-call ops in v0.7.0.
|
|
2201
|
-
*/
|
|
2202
|
-
const DEPRECATED_SYMBOL_OP_REPLACEMENT = {
|
|
2203
|
-
// v0.7.2: bridge classes ship default-wired so `$ llm` / `$ data_read` work
|
|
2204
|
-
// out of the box in default deployments. Suggestions are load-bearing
|
|
2205
|
-
// (no more "(or your wired connector name)" caveat).
|
|
2206
|
-
"~": "$ llm prompt=\"...\" [maxTokens=N] [model=\"...\"] -> R",
|
|
2207
|
-
">": "$ data_read mode=\"fts|semantic|rerank\" query=\"...\" limit=N -> R",
|
|
2208
|
-
"@": "shell(command=\"...\") [-> R]",
|
|
2209
|
-
"!": "emit(text=\"...\")",
|
|
2210
|
-
"??": "ask(prompt=\"...\") -> R",
|
|
2211
|
-
"&": "inline(skill=\"...\") (or execute_skill(skill_name=\"...\", ...) -> R for procedural composition)",
|
|
2212
|
-
};
|
|
2213
|
-
const DEPRECATED_SYMBOL_OP = {
|
|
2214
|
-
id: "deprecated-symbol-op",
|
|
2215
|
-
severity: "warning",
|
|
2216
|
-
description: "An op uses the legacy symbol form deprecated in v0.7.0.",
|
|
2217
|
-
remediation: "Rewrite to the canonical v0.7.0 form (see message). All legacy ops continue to compile during the grace period; tier-1 promotion (refuse-to-compile) lands in v0.8/v0.9. See CHANGELOG.md `## 0.7.0 — Migration` for the full rewrite rules.",
|
|
2218
|
-
check: (ctx) => {
|
|
2219
|
-
const findings = [];
|
|
2220
|
-
for (const [targetName, target] of ctx.parsed.targets) {
|
|
2221
|
-
const reported = new Set();
|
|
2222
|
-
walkOps(target.ops, (op) => {
|
|
2223
|
-
const replacement = DEPRECATED_SYMBOL_OP_REPLACEMENT[op.kind];
|
|
2224
|
-
if (replacement === undefined)
|
|
2225
|
-
return;
|
|
2226
|
-
// v0.7.1: skip ops authored in canonical function-call form. The
|
|
2227
|
-
// parser collapses both `! x` and `emit(text="x")` to kind "!",
|
|
2228
|
-
// so without the sourceForm marker the lint would fire on
|
|
2229
|
-
// canonical code. Source-form marker preserves which surface
|
|
2230
|
-
// the author wrote.
|
|
2231
|
-
if (op.sourceForm === "function-call")
|
|
2232
|
-
return;
|
|
2233
|
-
// Dedupe per-kind-per-target — one nudge per legacy op type per
|
|
2234
|
-
// target is plenty; further occurrences don't add signal.
|
|
2235
|
-
const key = `${targetName}:${op.kind}`;
|
|
2236
|
-
if (reported.has(key))
|
|
2237
|
-
return;
|
|
2238
|
-
reported.add(key);
|
|
2239
|
-
findings.push({
|
|
2240
|
-
rule: "deprecated-symbol-op",
|
|
2241
|
-
severity: "warning",
|
|
2242
|
-
message: `Op '${op.kind}' in target '${targetName}' is deprecated in v0.7.0. Rewrite as: \`${replacement}\``,
|
|
2243
|
-
block: targetName,
|
|
2244
|
-
extras: { legacy_op: op.kind, canonical_replacement: replacement },
|
|
2245
|
-
});
|
|
2246
|
-
});
|
|
2247
|
-
if (target.elseBlock !== undefined) {
|
|
2248
|
-
walkOps(target.elseBlock, (op) => {
|
|
2249
|
-
const replacement = DEPRECATED_SYMBOL_OP_REPLACEMENT[op.kind];
|
|
2250
|
-
if (replacement === undefined)
|
|
2251
|
-
return;
|
|
2252
|
-
if (op.sourceForm === "function-call")
|
|
2253
|
-
return;
|
|
2254
|
-
const key = `${targetName}:else:${op.kind}`;
|
|
2255
|
-
if (reported.has(key))
|
|
2256
|
-
return;
|
|
2257
|
-
reported.add(key);
|
|
2258
|
-
findings.push({
|
|
2259
|
-
rule: "deprecated-symbol-op",
|
|
2260
|
-
severity: "warning",
|
|
2261
|
-
message: `Op '${op.kind}' in target '${targetName}' (else block) is deprecated in v0.7.0. Rewrite as: \`${replacement}\``,
|
|
2262
|
-
block: targetName,
|
|
2263
|
-
extras: { legacy_op: op.kind, canonical_replacement: replacement },
|
|
2264
|
-
});
|
|
2265
|
-
});
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
return findings;
|
|
2269
|
-
},
|
|
2270
|
-
};
|
|
2271
2108
|
/**
|
|
2272
2109
|
* v0.7.1 — tier-2 visibility nudge for legacy `$(VAR)` substitution form.
|
|
2273
2110
|
* Canonical v0.7.0+ form is `${VAR}`. Parser/runtime accept both during
|
|
@@ -2381,7 +2218,6 @@ const RULES = [
|
|
|
2381
2218
|
RESERVED_KEYWORD,
|
|
2382
2219
|
UNKNOWN_SKILL_REFERENCE,
|
|
2383
2220
|
UNKNOWN_TEMPLATE_REFERENCE,
|
|
2384
|
-
UNKNOWN_RETRIEVAL_ARG,
|
|
2385
2221
|
UNKNOWN_CONNECTOR,
|
|
2386
2222
|
UNKNOWN_CONNECTOR_CLASS,
|
|
2387
2223
|
UNWIRED_PRIMARY_CONNECTOR,
|
|
@@ -2399,14 +2235,12 @@ const RULES = [
|
|
|
2399
2235
|
MISSING_SKILLSTORE_FOR_DATA_REF,
|
|
2400
2236
|
// Tier-2 (warning)
|
|
2401
2237
|
DEPRECATED_QUESTION,
|
|
2402
|
-
DEPRECATED_SYMBOL_OP,
|
|
2403
2238
|
DEPRECATED_SUBSTITUTION_SHAPE,
|
|
2404
2239
|
UNSAFE_SHELL_AMBIGUOUS_SUBST,
|
|
2405
2240
|
UNSAFE_SHELL_OP,
|
|
2406
2241
|
UNSAFE_SHELL_DISABLED,
|
|
2407
2242
|
UNCONFIRMED_MUTATION,
|
|
2408
2243
|
UNQUOTED_SUBSTITUTION_IN_KWARG_VALUE,
|
|
2409
|
-
MODEL_CONTENTION,
|
|
2410
2244
|
DRAFT_WITH_TRIGGER,
|
|
2411
2245
|
REFERENCE_TO_DISABLED_SKILL,
|
|
2412
2246
|
UNUSED_AUGMENTING_HEADER,
|
|
@@ -2457,9 +2291,9 @@ function collectAmpRefsFromOps(ops) {
|
|
|
2457
2291
|
out.push({ name, via });
|
|
2458
2292
|
};
|
|
2459
2293
|
walkOps(ops, (op) => {
|
|
2460
|
-
if (op.kind === "
|
|
2461
|
-
emit(op.ampParams.skillName, "
|
|
2462
|
-
//
|
|
2294
|
+
if (op.kind === "inline" && op.ampParams !== undefined)
|
|
2295
|
+
emit(op.ampParams.skillName, "inline");
|
|
2296
|
+
// `$ execute_skill` is also a composition primitive.
|
|
2463
2297
|
if (op.kind === "$" && /^execute_skill\b/.test(op.body)) {
|
|
2464
2298
|
// v0.15.2 — accept either `name` or `skill_name` (back-compat alias).
|
|
2465
2299
|
const m = /\b(?:skill_name|name)\s*=\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z_][\w-]*))/.exec(op.body);
|
|
@@ -2520,11 +2354,6 @@ function extractVarRefsWithFilter(op) {
|
|
|
2520
2354
|
}
|
|
2521
2355
|
function collectOpText(op) {
|
|
2522
2356
|
let text = op.body;
|
|
2523
|
-
if (op.retrievalParams !== undefined) {
|
|
2524
|
-
text += " " + op.retrievalParams.query + " " + Object.values(op.retrievalParams.extra).join(" ");
|
|
2525
|
-
}
|
|
2526
|
-
if (op.localModelParams !== undefined)
|
|
2527
|
-
text += " " + op.localModelParams.prompt;
|
|
2528
2357
|
if (op.setValue !== undefined)
|
|
2529
2358
|
text += " " + op.setValue;
|
|
2530
2359
|
if (op.foreachList !== undefined)
|