@softspark/ai-toolkit 4.15.0 → 4.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.
Files changed (76) hide show
  1. package/AGENTS.md +117 -0
  2. package/CHANGELOG.md +43 -0
  3. package/README.md +19 -13
  4. package/app/.claude-plugin/plugin.json +1 -1
  5. package/app/ARCHITECTURE.md +4 -3
  6. package/app/hooks/_hook-io.sh +18 -3
  7. package/app/hooks/ai-toolkit-statusline.sh +30 -5
  8. package/app/hooks/filter-tool-output.sh +76 -0
  9. package/app/hooks/governance-capture.sh +1 -1
  10. package/app/hooks/guard-path.sh +2 -2
  11. package/app/hooks/post-tool-use.sh +5 -3
  12. package/app/hooks/pre-compact-save.sh +4 -3
  13. package/app/hooks/quality-gate.sh +12 -1
  14. package/app/hooks/revert-guard.sh +5 -2
  15. package/app/hooks/save-session.sh +4 -2
  16. package/app/hooks/session-end.sh +36 -4
  17. package/app/hooks/session-start.sh +11 -5
  18. package/app/hooks.json +10 -0
  19. package/app/output-filter-policy.json +15 -0
  20. package/app/skills/brand-voice/scripts/measure.py +7 -5
  21. package/benchmarks/ecosystem-doctor-snapshot.json +22 -22
  22. package/benchmarks/output-filter/README.md +11 -0
  23. package/benchmarks/output-filter/scenarios.json +25 -0
  24. package/bin/ai-toolkit.js +2 -0
  25. package/kb/history/completed/native-tool-output-filter-plan.md +517 -0
  26. package/kb/procedures/release-preparation-sop.md +6 -5
  27. package/kb/reference/architecture-overview.md +6 -5
  28. package/kb/reference/cli-reference.md +19 -2
  29. package/kb/reference/codex-cli-compatibility.md +1 -0
  30. package/kb/reference/copilot-compatibility.md +173 -0
  31. package/kb/reference/enterprise-config-guide.md +28 -2
  32. package/kb/reference/global-install-model.md +6 -2
  33. package/kb/reference/hooks-catalog.md +105 -16
  34. package/kb/reference/opencode-compatibility.md +1 -0
  35. package/kb/reference/supported-tools-registry.md +10 -5
  36. package/kb/reference/tool-output-filter.md +288 -0
  37. package/kb/reference/windows-support.md +4 -3
  38. package/llms-full.txt +1182 -40
  39. package/llms.txt +3 -0
  40. package/manifest.json +9 -6
  41. package/package.json +3 -2
  42. package/scripts/benchmark_output_filter.py +343 -0
  43. package/scripts/check_deps.py +16 -0
  44. package/scripts/claude_app.py +30 -2
  45. package/scripts/config_cli.py +4 -4
  46. package/scripts/config_lock.py +120 -14
  47. package/scripts/config_merger.py +103 -20
  48. package/scripts/config_resolver.py +22 -2
  49. package/scripts/config_validator.py +268 -16
  50. package/scripts/copilot_legacy_hashes.json +338 -0
  51. package/scripts/doctor.py +1 -0
  52. package/scripts/generate_codex_hooks.py +2 -0
  53. package/scripts/generate_copilot.py +464 -71
  54. package/scripts/generate_copilot_hooks.py +124 -7
  55. package/scripts/generate_gemini_hooks.py +33 -10
  56. package/scripts/generate_opencode_plugin.py +28 -12
  57. package/scripts/install_steps/ai_tools.py +115 -3
  58. package/scripts/install_steps/hooks.py +25 -1
  59. package/scripts/output_filter_cli.py +347 -0
  60. package/scripts/output_filter_hook.py +23 -0
  61. package/scripts/plugin_schema.py +27 -1
  62. package/scripts/schemas/ai-toolkit-config.schema.json +83 -5
  63. package/scripts/session_state.py +156 -42
  64. package/scripts/tool_output_filter/__init__.py +33 -0
  65. package/scripts/tool_output_filter/contracts.py +173 -0
  66. package/scripts/tool_output_filter/engine.py +260 -0
  67. package/scripts/tool_output_filter/hook_runtime.py +369 -0
  68. package/scripts/tool_output_filter/input.py +56 -0
  69. package/scripts/tool_output_filter/invariants.py +40 -0
  70. package/scripts/tool_output_filter/policy.py +153 -0
  71. package/scripts/tool_output_filter/profiles/__init__.py +68 -0
  72. package/scripts/tool_output_filter/profiles/repeat_lines.py +71 -0
  73. package/scripts/tool_output_filter/profiles/tap_success.py +154 -0
  74. package/scripts/tool_output_filter/recovery.py +846 -0
  75. package/scripts/tool_output_filter/telemetry.py +13 -0
  76. package/scripts/uninstall.py +96 -3
@@ -0,0 +1,13 @@
1
+ """Metadata-only telemetry boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .contracts import FilterTelemetry
6
+
7
+
8
+ class TelemetrySink:
9
+ """Receives content-free filter decision events."""
10
+
11
+ def record(self, event: FilterTelemetry) -> None:
12
+ """Record one event without raising into the filter path."""
13
+ raise NotImplementedError
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env python3
2
- """Safely remove ai-toolkit-managed runtime customizations.
2
+ """Safely remove ai-toolkit-managed runtime data and customizations.
3
3
 
4
4
  The default scope is the current user's global install. ``--local`` targets a
5
5
  project, while an explicit legacy positional target scans both project and
6
6
  home-style locations for backward compatibility. Only files, symlinks, JSON
7
7
  handlers, and marker blocks with verifiable ai-toolkit ownership are removed.
8
+ Global scope also removes validated output-filter recovery files while keeping
9
+ foreign content in the same session trees.
8
10
 
9
11
  Usage:
10
12
  python3 scripts/uninstall.py [--yes] [--local|--global] [--target DIR]
@@ -31,6 +33,8 @@ from typing import Any
31
33
  sys.path.insert(0, str(Path(__file__).resolve().parent))
32
34
  from _common import app_dir, toolkit_dir
33
35
  from injection import strip_all_sections, strip_section, trim_trailing_blanks
36
+ from tool_output_filter.recovery import clean_owned_recovery_tree
37
+ from tool_output_filter.recovery import count_owned_recovery_artifacts
34
38
 
35
39
 
36
40
  CODEX_AGENT_MARKER = "# ai-toolkit-managed: codex-agent"
@@ -555,6 +559,45 @@ def _discover_claude_hooks(claude_dir: Path) -> list[tuple[str, str]]:
555
559
  return []
556
560
 
557
561
 
562
+ _OUTPUT_FILTER_POLICY_NAME = "ai-toolkit-output-filter.json"
563
+ _OUTPUT_FILTER_OWNER_NAME = ".ai-toolkit-output-filter.owner"
564
+ _OUTPUT_FILTER_OWNER_MARKER = b"ai-toolkit-output-filter-policy-v1\n"
565
+
566
+
567
+ def _managed_output_filter_policy(claude_dir: Path) -> list[Path]:
568
+ """Return the managed per-project policy pair, or [] when unmanaged."""
569
+ policy = claude_dir / _OUTPUT_FILTER_POLICY_NAME
570
+ owner = claude_dir / _OUTPUT_FILTER_OWNER_NAME
571
+ if owner.is_symlink() or not owner.is_file():
572
+ return []
573
+ try:
574
+ if owner.read_bytes() != _OUTPUT_FILTER_OWNER_MARKER:
575
+ return []
576
+ except OSError:
577
+ return []
578
+ managed = [owner]
579
+ if not policy.is_symlink() and policy.is_file():
580
+ managed.insert(0, policy)
581
+ return managed
582
+
583
+
584
+ def _discover_output_filter_policy(claude_dir: Path) -> list[tuple[str, str]]:
585
+ if not _managed_output_filter_policy(claude_dir):
586
+ return []
587
+ return [(
588
+ f"Managed: {_OUTPUT_FILTER_POLICY_NAME} (project output-filter policy)",
589
+ "output-filter-policy",
590
+ )]
591
+
592
+
593
+ def _remove_output_filter_policy(claude_dir: Path, trusted_root: Path) -> None:
594
+ managed = _managed_output_filter_policy(claude_dir)
595
+ for path in managed:
596
+ _safe_unlink(path, trusted_root)
597
+ if managed:
598
+ print(f" Removed: .claude/{_OUTPUT_FILTER_POLICY_NAME} (managed)")
599
+
600
+
558
601
  def _discover_claude_markers(claude_dir: Path) -> list[tuple[str, str]]:
559
602
  found: list[tuple[str, str]] = []
560
603
  for item in ("constitution.md", "ARCHITECTURE.md"):
@@ -579,6 +622,7 @@ def discover_components(claude_dir: Path) -> list[tuple[str, str]]:
579
622
  found.append((f"Symlinks: skills/ ({len(skill_links)} toolkit directories)", "skill-link"))
580
623
  found.extend(_discover_claude_hooks(claude_dir))
581
624
  found.extend(_discover_claude_markers(claude_dir))
625
+ found.extend(_discover_output_filter_policy(claude_dir))
582
626
  return found
583
627
 
584
628
 
@@ -663,6 +707,7 @@ def remove_components(claude_dir: Path, trusted_root: Path) -> None:
663
707
  )
664
708
  _remove_claude_hooks(claude_dir, trusted_root)
665
709
  _remove_claude_markers(claude_dir, trusted_root)
710
+ _remove_output_filter_policy(claude_dir, trusted_root)
666
711
 
667
712
 
668
713
  # ---------------------------------------------------------------------------
@@ -1087,6 +1132,36 @@ def _remove_copilot(surface: CopilotSurface) -> None:
1087
1132
  # Scope resolution, safety preflight, CLI
1088
1133
  # ---------------------------------------------------------------------------
1089
1134
 
1135
+ def _discover_recovery(sessions_root: Path) -> list[tuple[str, str]]:
1136
+ try:
1137
+ os.lstat(sessions_root)
1138
+ except FileNotFoundError:
1139
+ return []
1140
+ artifact_count = count_owned_recovery_artifacts(sessions_root)
1141
+ if artifact_count == 0:
1142
+ return []
1143
+ noun = "artifact" if artifact_count == 1 else "artifacts"
1144
+ return [
1145
+ (
1146
+ "Recovery: sessions/*/output-filter "
1147
+ f"({artifact_count} owned {noun}) under {sessions_root}",
1148
+ "output-filter-recovery",
1149
+ )
1150
+ ]
1151
+
1152
+
1153
+ def _remove_recovery(sessions_root: Path) -> None:
1154
+ """Remove owned recovery last so its failure rolls back other surfaces."""
1155
+ try:
1156
+ removed = clean_owned_recovery_tree(sessions_root)
1157
+ except OSError as error:
1158
+ raise RuntimeError(
1159
+ "output-filter recovery cleanup failed; an I/O fault after "
1160
+ "preflight may have left recovery cleanup partial"
1161
+ ) from error
1162
+ print(f" Removed: {removed} output-filter recovery file(s)")
1163
+
1164
+
1090
1165
  def _configured_home(env_name: str, fallback: Path, *, strict_absolute: bool) -> Path:
1091
1166
  value = os.environ.get(env_name)
1092
1167
  if not value:
@@ -1203,6 +1278,8 @@ def _transaction_specs(
1203
1278
  (claude / "hooks.json", False),
1204
1279
  (claude / "constitution.md", False),
1205
1280
  (claude / "ARCHITECTURE.md", False),
1281
+ (claude / _OUTPUT_FILTER_POLICY_NAME, False),
1282
+ (claude / _OUTPUT_FILTER_OWNER_NAME, False),
1206
1283
  ):
1207
1284
  add(path, recursive, claude_boundary)
1208
1285
  for surface in codex:
@@ -1240,8 +1317,8 @@ def _transaction_specs(
1240
1317
  def _parse_args(argv: list[str]) -> argparse.Namespace:
1241
1318
  parser = argparse.ArgumentParser(
1242
1319
  description=(
1243
- "Remove only ai-toolkit-managed Claude, Codex, and Copilot "
1244
- "customizations while preserving user-owned content."
1320
+ "Remove only ai-toolkit-managed Claude, Codex, Copilot, and "
1321
+ "output-filter recovery data while preserving user-owned content."
1245
1322
  ),
1246
1323
  epilog=(
1247
1324
  "Global Codex and Copilot locations honor CODEX_HOME and "
@@ -1290,6 +1367,17 @@ def main(argv: list[str] | None = None) -> None:
1290
1367
  components.extend(_discover_codex(surface))
1291
1368
  for surface in copilot:
1292
1369
  components.extend(_discover_copilot(surface))
1370
+ recovery_root = (
1371
+ target / ".softspark" / "ai-toolkit" / "sessions"
1372
+ if scope in {"global", "both"}
1373
+ else None
1374
+ )
1375
+ recovery_components = (
1376
+ _discover_recovery(recovery_root)
1377
+ if recovery_root is not None
1378
+ else []
1379
+ )
1380
+ components.extend(recovery_components)
1293
1381
  except (OSError, RuntimeError, UnicodeError) as error:
1294
1382
  print(f"Error: {error}", file=sys.stderr)
1295
1383
  raise SystemExit(1) from error
@@ -1338,6 +1426,11 @@ def main(argv: list[str] | None = None) -> None:
1338
1426
  for surface in copilot:
1339
1427
  _preflight(target, claude, [], [surface])
1340
1428
  _remove_copilot(surface)
1429
+ if recovery_root is not None and recovery_components:
1430
+ # The recovery API preflights its complete tree before the first
1431
+ # unlink. A later I/O fault can still leave recovery partially
1432
+ # cleaned; the transaction restores every other runtime surface.
1433
+ _remove_recovery(recovery_root)
1341
1434
  except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
1342
1435
  rollback_error: RuntimeError | None = None
1343
1436
  if transaction is not None: