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/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
- // `@ unsafe` ops use bash `$(...)` syntax — handled by
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 === "@" && op.policy === "unsafe")
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. Common cases: `>` and `~` need `key=value ... -> VAR`; `& skill arg=value -> VAR` for skill invocations.",
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 `[~>&$@!?]/.test(msg))
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 either (a) a wired connector matching the op name, or (b) the `primary` connector's tool dispatch. Neither resolves. Demoted to advisory when every call site declares `(fallback: ...)` (v0.9.4.1).",
692
- remediation: "For skill authors (in-skill fix): qualify the op as `$ <connector>.<tool>` against a wired connector, OR pick a `tool` name that matches a wired connector name (e.g., `$ llm prompt=...` if `llm` is wired), OR add `(fallback: \"...\")` to every call site to acknowledge dispatch-time guarding. For runtime operators (config fix): wire a connector whose name matches the bare op (the v0.7.2 canonical pattern — e.g., `llm` / `memory` auto-wire by default in bundled deployments), or add a `primary` entry to connectors.json that handles the tool.",
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 === "execute_skill" || toolName === "json_parse")
681
+ if (BARE_FORM_INTRINSICS.has(toolName))
709
682
  return;
710
- // v0.7.3 name-match: if the bare op name matches a wired
711
- // connector, the runtime dispatch resolver routes there directly.
712
- // (Mirrors `runtime.ts` `$` op dispatch kept in sync with that
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 bare tool op no connector named '${toolName}' wired and no \`primary\` fallback. Wired connectors: ${wired}.`,
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 \`&\`, but lint was invoked without a SkillStore (call site: ${ctx.callSite}). Data-skill inlining will silently skip; the \`&\` op will survive into the runtime and error.`,
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 !== "@" || op.policy !== "unsafe")
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 === "@" && op.policy === "unsafe") {
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 === "@" && op.policy === "unsafe") {
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` (v0.4.2), when a preceding `??` / `ask(...)` confirmation gates the op, or (v0.7.0+) when the op carries `approved=\"reason\"` per-op authorization.",
1206
- remediation: "Three ways to authorize: (1) add `# Autonomous: true` at the skill header for cron/agent-fired skills; (2) add a preceding `??` / `ask(prompt=\"...\")` confirmation op in the same target; (3) v0.7.0+: pass `approved=\"reason\"` kwarg on the mutation op itself (any non-empty string; presence is what matters, value not parsed semantically).",
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, sawConfirm: false };
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, precede with \`ask(...)\`, or declare \`# Autonomous: true\`.`,
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, precede with \`ask(...)\`, or declare \`# Autonomous: true\`.`,
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
- // v0.7.2 legacy @ shell op. Tokenize the body the same way the
2141
- // runtime would (whitespace-separated, quotes respected), then
2142
- // flag any token that is a bare unquoted substitution. Quoted
2143
- // tokens (`"${VAR}"` / `'${VAR}'`) are safe.
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}:@:${varName}`;
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: `\`@ ... \${${varName}}\` shell arg in target '${targetName}': unquoted substitution. ${describeOriginRisk(origin)} Wrap as \`"\${${varName}}"\` to prevent silent word-splitting if the value contains whitespace.`,
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 === "&" && op.ampParams !== undefined)
2461
- emit(op.ampParams.skillName, "&");
2462
- // v0.2.11 Bug 7: $ execute_skill is also a composition primitive.
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)