@softspark/ai-toolkit 4.29.2 → 4.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +40 -15
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/ARCHITECTURE.md +2 -2
  5. package/app/mcp-templates/README.md +7 -2
  6. package/app/mcp-templates/rag-mcp-legal.json +11 -0
  7. package/app/mcp-templates/rag-mcp.json +11 -0
  8. package/app/surface.json +1 -0
  9. package/benchmarks/ecosystem-doctor-snapshot.json +29 -17
  10. package/bin/ai-toolkit.js +8 -0
  11. package/kb/history/completed/dsh-integration-plan-superseded.md +322 -0
  12. package/kb/history/completed/dsh-native-install-target-plan.md +331 -0
  13. package/kb/procedures/ecosystem-sync-sop.md +7 -5
  14. package/kb/procedures/maintenance-sop.md +1 -1
  15. package/kb/procedures/release-preparation-sop.md +81 -20
  16. package/kb/procedures/release-verification-sop.md +35 -5
  17. package/kb/reference/architecture-overview.md +24 -5
  18. package/kb/reference/cli-reference.md +1 -1
  19. package/kb/reference/dsh-compatibility.md +183 -0
  20. package/kb/reference/manifest-install.md +112 -5
  21. package/kb/reference/mcp-templates.md +11 -4
  22. package/kb/reference/plugin-pack-conventions.md +35 -18
  23. package/kb/reference/supported-tools-registry.md +30 -6
  24. package/llms-full.txt +1190 -69
  25. package/llms.txt +3 -0
  26. package/manifest.json +2 -2
  27. package/package.json +2 -2
  28. package/scripts/codex_skill_adapter.py +673 -34
  29. package/scripts/config_resolver.py +80 -14
  30. package/scripts/doctor.py +98 -20
  31. package/scripts/ecosystem_tools.json +51 -1
  32. package/scripts/generate_codex_skills.py +22 -20
  33. package/scripts/install.py +30 -13
  34. package/scripts/install_steps/ai_tools.py +97 -33
  35. package/scripts/install_steps/dsh.py +5063 -0
  36. package/scripts/install_steps/install_state.py +1645 -57
  37. package/scripts/mcp_editors.py +5 -2
  38. package/scripts/plugin.py +2495 -163
  39. package/scripts/plugin_mcp.py +279 -0
  40. package/scripts/plugin_rules.py +389 -0
  41. package/scripts/plugin_schema.py +139 -23
  42. package/scripts/uninstall.py +47 -4
  43. package/scripts/validate.py +421 -0
@@ -22,6 +22,7 @@ import json
22
22
  import re
23
23
  import sys
24
24
  import tempfile
25
+ from itertools import islice
25
26
  from pathlib import Path
26
27
 
27
28
  sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -130,6 +131,14 @@ VALID_KB_CATEGORIES = frozenset({
130
131
  "decisions", "runbooks", "planning", "business", "templates",
131
132
  })
132
133
 
134
+ SKILL_DIRECTORY_NAME = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
135
+ INVOCATION_BOOLEAN_FIELDS = ("user-invocable", "disable-model-invocation")
136
+ CAMEL_CASE_INVOCATION_FIELDS = ("userInvocable", "disableModelInvocation")
137
+ INVOCATION_BOOLEAN_VALUES = frozenset({
138
+ "true", "false", "yes", "no", "on", "off", "1", "0",
139
+ })
140
+ MAX_EMITTED_SKILL_NODES = 10_000
141
+
133
142
  # Skill body budget, in bytes after the frontmatter block.
134
143
  #
135
144
  # The body loads in full every time a trigger matches, including when it matches
@@ -247,6 +256,268 @@ def _fm_has(lines: list[str], field: str) -> bool:
247
256
  return any(line.startswith(f"{field}:") for line in lines)
248
257
 
249
258
 
259
+ def _validate_invocation_metadata(label: str, fm_lines: list[str],
260
+ vr: ValidationResult) -> None:
261
+ """Reject metadata spellings that DSH interprets differently or ignores."""
262
+ entries: list[tuple[str, str]] = []
263
+ for line_number, line in enumerate(fm_lines, start=2):
264
+ if not line.strip() or line.lstrip().startswith("#") or line[0].isspace():
265
+ continue
266
+ parsed = _parse_supported_top_level_entry(
267
+ line,
268
+ label=label,
269
+ line_number=line_number,
270
+ vr=vr,
271
+ )
272
+ if parsed is not None:
273
+ entries.append(parsed)
274
+
275
+ for field in CAMEL_CASE_INVOCATION_FIELDS:
276
+ if any(key == field for key, _ in entries):
277
+ vr.error(
278
+ f"{label}: camel-case field '{field}' is forbidden; "
279
+ "use kebab-case invocation metadata"
280
+ )
281
+
282
+ for field in INVOCATION_BOOLEAN_FIELDS:
283
+ occurrences = sum(1 for key, _ in entries if key == field)
284
+ if occurrences > 1:
285
+ vr.error(
286
+ f"{label}: duplicate canonical invocation key '{field}'"
287
+ )
288
+ for key, raw_value in entries:
289
+ if key != field:
290
+ continue
291
+ value = _frontmatter_scalar(raw_value).lower()
292
+ if value not in INVOCATION_BOOLEAN_VALUES:
293
+ vr.error(
294
+ f"{label}: field '{field}' has invalid boolean value"
295
+ )
296
+
297
+
298
+ def _frontmatter_scalar(raw_value: str) -> str:
299
+ """Decode the simple scalar forms used by emitted skill metadata."""
300
+ value = raw_value.strip()
301
+ if not value:
302
+ return ""
303
+ quoted = re.fullmatch(r'''(["'])(.*?)\1(?:\s+#.*)?''', value)
304
+ if quoted:
305
+ return quoted.group(2).strip()
306
+ return re.split(r"\s+#", value, maxsplit=1)[0].strip()
307
+
308
+
309
+ def _parse_supported_top_level_entry(
310
+ line: str,
311
+ *,
312
+ label: str,
313
+ line_number: int,
314
+ vr: ValidationResult,
315
+ ) -> tuple[str, str] | None:
316
+ """Parse one scalar-mapping entry without YAML indirection features."""
317
+ if line.startswith(("'", '"')):
318
+ vr.error(
319
+ f"{label}:{line_number} - quoted frontmatter keys are unsupported"
320
+ )
321
+ return None
322
+ if re.match(r"^<<\s*:", line):
323
+ vr.error(f"{label}:{line_number} - YAML merge key '<<' is unsupported")
324
+ return None
325
+ if re.match(r"^[A-Za-z][A-Za-z0-9-]*\s+:", line):
326
+ vr.error(f"{label}:{line_number} - whitespace before ':' is unsupported")
327
+ return None
328
+ match = re.fullmatch(r"(?P<key>[A-Za-z][A-Za-z0-9-]*):(?P<value>.*)", line)
329
+ if match is None:
330
+ feature = "YAML anchors" if line.startswith("&") else "YAML aliases"
331
+ if line.startswith(("&", "*")):
332
+ vr.error(f"{label}:{line_number} - {feature} are unsupported")
333
+ else:
334
+ vr.error(
335
+ f"{label}:{line_number} - unsupported top-level key syntax; "
336
+ "use an unquoted key immediately followed by ':'"
337
+ )
338
+ return None
339
+
340
+ raw_value = match.group("value")
341
+ value = raw_value.strip()
342
+ if value.startswith("&"):
343
+ vr.error(f"{label}:{line_number} - YAML anchors are unsupported")
344
+ return None
345
+ if value.startswith("*"):
346
+ vr.error(f"{label}:{line_number} - YAML aliases are unsupported")
347
+ return None
348
+ return match.group("key"), raw_value
349
+
350
+
351
+ def _emitted_quoted_scalar(
352
+ value: str,
353
+ *,
354
+ quote: str,
355
+ label: str,
356
+ line_number: int,
357
+ vr: ValidationResult,
358
+ ) -> str | None:
359
+ """Decode one supported quoted scalar and reject trailing YAML nodes."""
360
+ characters: list[str] = []
361
+ closing_index: int | None = None
362
+ index = 1
363
+ while index < len(value):
364
+ character = value[index]
365
+ if quote == "'" and character == "'" and index + 1 < len(value):
366
+ if value[index + 1] == "'":
367
+ characters.append("'")
368
+ index += 2
369
+ continue
370
+ if quote == '"' and character == "\\" and index + 1 < len(value):
371
+ characters.extend((character, value[index + 1]))
372
+ index += 2
373
+ continue
374
+ if character == quote:
375
+ closing_index = index
376
+ break
377
+ characters.append(character)
378
+ index += 1
379
+
380
+ quote_name = "single" if quote == "'" else "double"
381
+ if closing_index is None:
382
+ vr.error(
383
+ f"{label}:{line_number} - unterminated {quote_name}-quoted scalar"
384
+ )
385
+ return None
386
+
387
+ trailing = value[closing_index + 1:].strip()
388
+ if trailing and not trailing.startswith("#"):
389
+ vr.error(
390
+ f"{label}:{line_number} - unsupported content after quoted scalar"
391
+ )
392
+ return None
393
+
394
+ if quote == "'":
395
+ return "".join(characters).strip()
396
+ token = value[:closing_index + 1]
397
+ try:
398
+ decoded = json.loads(token)
399
+ except json.JSONDecodeError:
400
+ vr.error(f"{label}:{line_number} - invalid double-quoted scalar")
401
+ return None
402
+ return decoded.strip()
403
+
404
+
405
+ def _parse_emitted_scalar(
406
+ raw_value: str,
407
+ *,
408
+ label: str,
409
+ line_number: int,
410
+ vr: ValidationResult,
411
+ ) -> str | None:
412
+ """Parse the supported single-line scalar subset of emitted YAML."""
413
+ value = raw_value.strip()
414
+ if not value:
415
+ return ""
416
+ if value.startswith(("|", ">")):
417
+ vr.error(f"{label}:{line_number} - unsupported block scalar")
418
+ return None
419
+ if value.startswith(("[", "{")) or value == "-" or value.startswith("- "):
420
+ vr.error(f"{label}:{line_number} - unsupported collection value")
421
+ return None
422
+ if value[0] in {"'", '"'}:
423
+ return _emitted_quoted_scalar(
424
+ value,
425
+ quote=value[0],
426
+ label=label,
427
+ line_number=line_number,
428
+ vr=vr,
429
+ )
430
+ return re.split(r"\s+#", value, maxsplit=1)[0].strip()
431
+
432
+
433
+ def _parse_emitted_frontmatter(
434
+ skill_file: Path,
435
+ label: str,
436
+ vr: ValidationResult,
437
+ ) -> dict[str, str] | None:
438
+ """Parse a complete top-level frontmatter mapping and reject ambiguity."""
439
+ try:
440
+ lines = skill_file.read_text(encoding="utf-8").splitlines()
441
+ except (OSError, UnicodeError) as error:
442
+ vr.error(f"{label} - cannot read SKILL.md: {error}")
443
+ return None
444
+ if not lines or lines[0] != "---":
445
+ vr.error(f"{label} - missing opening frontmatter delimiter")
446
+ return None
447
+
448
+ closing_index = next(
449
+ (index for index, line in enumerate(lines[1:], start=1)
450
+ if line == "---"),
451
+ None,
452
+ )
453
+ if closing_index is None:
454
+ vr.error(f"{label} - missing closing frontmatter delimiter")
455
+ return None
456
+
457
+ fields: dict[str, str] = {}
458
+ seen_keys: set[str] = set()
459
+ for line_number, line in enumerate(lines[1:closing_index], start=2):
460
+ if not line.strip() or line.lstrip().startswith("#"):
461
+ continue
462
+ if line[0].isspace():
463
+ vr.error(f"{label}:{line_number} - unexpected indentation")
464
+ continue
465
+ parsed = _parse_supported_top_level_entry(
466
+ line,
467
+ label=label,
468
+ line_number=line_number,
469
+ vr=vr,
470
+ )
471
+ if parsed is None:
472
+ continue
473
+ key, raw_value = parsed
474
+ if key in seen_keys:
475
+ vr.error(f"{label}:{line_number} - duplicate top-level key '{key}'")
476
+ continue
477
+ seen_keys.add(key)
478
+ value = _parse_emitted_scalar(
479
+ raw_value,
480
+ label=label,
481
+ line_number=line_number,
482
+ vr=vr,
483
+ )
484
+ if value is not None:
485
+ fields[key] = value
486
+ return fields
487
+
488
+
489
+ def _validate_emitted_frontmatter(
490
+ entry: Path,
491
+ fields: dict[str, str],
492
+ label: str,
493
+ vr: ValidationResult,
494
+ ) -> None:
495
+ """Validate the DSH Agent Skills frontmatter contract."""
496
+ for field in CAMEL_CASE_INVOCATION_FIELDS:
497
+ if field in fields:
498
+ vr.error(
499
+ f"{label}: camel-case field '{field}' is forbidden; "
500
+ "use kebab-case invocation metadata"
501
+ )
502
+ for field in INVOCATION_BOOLEAN_FIELDS:
503
+ if field in fields and fields[field].lower() not in INVOCATION_BOOLEAN_VALUES:
504
+ vr.error(f"{label}: field '{field}' has invalid boolean value")
505
+
506
+ name = fields.get("name", "").strip()
507
+ description = fields.get("description", "").strip()
508
+ if not name:
509
+ vr.error(f"{label} - required field 'name' must be non-empty")
510
+ elif not SKILL_DIRECTORY_NAME.fullmatch(name):
511
+ vr.error(f"{label} - name must be kebab-case")
512
+ elif name != entry.name:
513
+ vr.error(
514
+ f"{label} - declared name '{name}' does not match directory "
515
+ f"'{entry.name}'"
516
+ )
517
+ if not description:
518
+ vr.error(f"{label} - required field 'description' must be non-empty")
519
+
520
+
250
521
  def _body_line_count(filepath: Path) -> int:
251
522
  """Count lines after the second --- delimiter."""
252
523
  count = 0
@@ -368,6 +639,11 @@ def _validate_skill_frontmatter(tk_dir: Path, skill_path: Path,
368
639
  name = skill_path.name
369
640
  skill_file = skill_path / "SKILL.md"
370
641
 
642
+ if not SKILL_DIRECTORY_NAME.fullmatch(name):
643
+ vr.error(f"skills/{name} - invalid skill directory name")
644
+
645
+ _validate_invocation_metadata(f"skills/{name}/SKILL.md", fm_lines, vr)
646
+
371
647
  if not _fm_has(fm_lines, "name"):
372
648
  vr.error(f"{name} - Missing name field")
373
649
  if not _fm_has(fm_lines, "description"):
@@ -553,6 +829,150 @@ def validate_skills(tk_dir: Path, vr: ValidationResult) -> int:
553
829
  return skill_count
554
830
 
555
831
 
832
+ def _validate_emitted_skill_layout(
833
+ entry: Path,
834
+ label: str,
835
+ vr: ValidationResult,
836
+ ) -> None:
837
+ """Reject hidden skills and unsafe links within one emitted bundle."""
838
+ try:
839
+ bundle_root = entry.resolve(strict=True)
840
+ except RuntimeError:
841
+ vr.error(f"{label} - symlink cycle detected at bundle root")
842
+ return
843
+ except OSError as error:
844
+ vr.error(f"{label} - cannot resolve emitted skill bundle: {error}")
845
+ return
846
+
847
+ visited = {bundle_root}
848
+ pending = [(entry, frozenset({bundle_root}))]
849
+ observed_nodes = 0
850
+
851
+ while pending:
852
+ logical_dir, ancestors = pending.pop()
853
+ try:
854
+ remaining = MAX_EMITTED_SKILL_NODES - observed_nodes
855
+ children, is_overflow = _bounded_sorted_directory(
856
+ logical_dir,
857
+ remaining,
858
+ )
859
+ except OSError as error:
860
+ relative = logical_dir.relative_to(entry)
861
+ location = label if relative == Path(".") else f"{label}/{relative}"
862
+ vr.error(f"{location} - cannot inspect skill bundle: {error}")
863
+ continue
864
+ if is_overflow:
865
+ vr.error(
866
+ f"{label} - traversal exceeded {MAX_EMITTED_SKILL_NODES} "
867
+ "entries; reduce the emitted skill bundle or remove cycles"
868
+ )
869
+ return
870
+
871
+ for child in children:
872
+ observed_nodes += 1
873
+
874
+ relative = child.relative_to(entry)
875
+ child_label = f"{label}/{relative.as_posix()}"
876
+ is_symlink = child.is_symlink()
877
+ if child.name == "SKILL.md" and relative != Path("SKILL.md"):
878
+ vr.error(
879
+ f"{child_label} - nested SKILL.md is not discoverable; "
880
+ "place it at .agents/skills/<name>/SKILL.md"
881
+ )
882
+ try:
883
+ resolved_child = child.resolve(strict=True)
884
+ except RuntimeError:
885
+ vr.error(
886
+ f"{child_label} - symlink cycle detected; remove the "
887
+ "cycle from the emitted skill bundle"
888
+ )
889
+ continue
890
+ except OSError as error:
891
+ kind = "symlink" if is_symlink else "path"
892
+ vr.error(f"{child_label} - cannot resolve {kind}: {error}")
893
+ continue
894
+
895
+ if is_symlink:
896
+ try:
897
+ resolved_child.relative_to(bundle_root)
898
+ except ValueError:
899
+ continue
900
+
901
+ if not resolved_child.is_dir():
902
+ continue
903
+ if resolved_child in ancestors:
904
+ vr.error(
905
+ f"{child_label} - symlink cycle detected; remove the "
906
+ "cycle from the emitted skill bundle"
907
+ )
908
+ continue
909
+ if resolved_child in visited:
910
+ continue
911
+ visited.add(resolved_child)
912
+ pending.append((child, ancestors | {resolved_child}))
913
+
914
+
915
+ def _bounded_sorted_directory(
916
+ directory: Path,
917
+ limit: int,
918
+ ) -> tuple[list[Path], bool]:
919
+ """Materialize at most ``limit + 1`` children before sorting."""
920
+ children = list(islice(directory.iterdir(), limit + 1))
921
+ is_overflow = len(children) > limit
922
+ if is_overflow:
923
+ children.pop()
924
+ children.sort(key=lambda path: path.name)
925
+ return children, is_overflow
926
+
927
+
928
+ def validate_emitted_agent_skills(tk_dir: Path, vr: ValidationResult) -> None:
929
+ """Validate the one-level ``.agents/skills`` discovery contract."""
930
+ print("## Emitted Agent Skills")
931
+ skills_root = tk_dir / ".agents" / "skills"
932
+ if not skills_root.is_dir():
933
+ print(" SKIP: .agents/skills not present")
934
+ print()
935
+ return
936
+
937
+ try:
938
+ root_entries, is_overflow = _bounded_sorted_directory(
939
+ skills_root,
940
+ MAX_EMITTED_SKILL_NODES,
941
+ )
942
+ except OSError as error:
943
+ vr.error(f".agents/skills - cannot inspect skill root: {error}")
944
+ print()
945
+ return
946
+ if is_overflow:
947
+ vr.error(
948
+ f".agents/skills - directory exceeds {MAX_EMITTED_SKILL_NODES} "
949
+ "entries; reduce the emitted skill catalogue"
950
+ )
951
+ print()
952
+ return
953
+ entries = [
954
+ entry for entry in root_entries
955
+ if not entry.name.startswith(".") and (entry.is_dir() or entry.is_symlink())
956
+ ]
957
+ for entry in entries:
958
+ label = f".agents/skills/{entry.name}"
959
+ if not SKILL_DIRECTORY_NAME.fullmatch(entry.name):
960
+ vr.error(f"{label} - invalid skill directory name")
961
+
962
+ _validate_emitted_skill_layout(entry, label, vr)
963
+ skill_file = entry / "SKILL.md"
964
+ if not skill_file.is_file():
965
+ vr.error(f"{label} - missing top-level SKILL.md")
966
+ continue
967
+ skill_label = f"{label}/SKILL.md"
968
+ fields = _parse_emitted_frontmatter(skill_file, skill_label, vr)
969
+ if fields is not None:
970
+ _validate_emitted_frontmatter(entry, fields, skill_label, vr)
971
+
972
+ print(f" Found: {len(entries)} emitted skill entries")
973
+ print()
974
+
975
+
556
976
  def validate_legacy_commands(tk_dir: Path, vr: ValidationResult) -> None:
557
977
  """Check for legacy command files."""
558
978
  commands_dir = tk_dir / "app" / "commands"
@@ -1251,6 +1671,7 @@ def _run_all_checks(tk_dir: Path, vr: ValidationResult) -> tuple[int, int, str]:
1251
1671
  """Run all validation checks. Returns (agent_count, skill_count, actual_tests)."""
1252
1672
  agent_count = validate_agents(tk_dir, vr)
1253
1673
  skill_count = validate_skills(tk_dir, vr)
1674
+ validate_emitted_agent_skills(tk_dir, vr)
1254
1675
  validate_legacy_commands(tk_dir, vr)
1255
1676
  validate_hook_events(tk_dir, vr)
1256
1677
  validate_language_rules(tk_dir, vr)