@softspark/ai-toolkit 4.15.1 → 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 (71) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +19 -11
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/ARCHITECTURE.md +4 -3
  5. package/app/hooks/_hook-io.sh +18 -3
  6. package/app/hooks/ai-toolkit-statusline.sh +30 -5
  7. package/app/hooks/filter-tool-output.sh +76 -0
  8. package/app/hooks/governance-capture.sh +1 -1
  9. package/app/hooks/guard-path.sh +2 -2
  10. package/app/hooks/post-tool-use.sh +5 -3
  11. package/app/hooks/pre-compact-save.sh +4 -3
  12. package/app/hooks/quality-gate.sh +12 -1
  13. package/app/hooks/revert-guard.sh +5 -2
  14. package/app/hooks/save-session.sh +4 -2
  15. package/app/hooks/session-end.sh +36 -4
  16. package/app/hooks/session-start.sh +11 -5
  17. package/app/hooks.json +10 -0
  18. package/app/output-filter-policy.json +15 -0
  19. package/app/skills/brand-voice/scripts/measure.py +7 -5
  20. package/benchmarks/ecosystem-doctor-snapshot.json +22 -22
  21. package/benchmarks/output-filter/README.md +11 -0
  22. package/benchmarks/output-filter/scenarios.json +25 -0
  23. package/bin/ai-toolkit.js +2 -0
  24. package/kb/history/completed/native-tool-output-filter-plan.md +517 -0
  25. package/kb/procedures/release-preparation-sop.md +6 -5
  26. package/kb/reference/architecture-overview.md +6 -5
  27. package/kb/reference/cli-reference.md +19 -2
  28. package/kb/reference/codex-cli-compatibility.md +1 -0
  29. package/kb/reference/copilot-compatibility.md +173 -0
  30. package/kb/reference/enterprise-config-guide.md +28 -2
  31. package/kb/reference/global-install-model.md +1 -0
  32. package/kb/reference/hooks-catalog.md +105 -16
  33. package/kb/reference/opencode-compatibility.md +1 -0
  34. package/kb/reference/supported-tools-registry.md +10 -5
  35. package/kb/reference/tool-output-filter.md +288 -0
  36. package/llms-full.txt +1173 -35
  37. package/llms.txt +3 -0
  38. package/manifest.json +9 -6
  39. package/package.json +3 -2
  40. package/scripts/benchmark_output_filter.py +343 -0
  41. package/scripts/check_deps.py +16 -0
  42. package/scripts/claude_app.py +30 -2
  43. package/scripts/config_cli.py +4 -4
  44. package/scripts/config_lock.py +120 -14
  45. package/scripts/config_merger.py +103 -20
  46. package/scripts/config_resolver.py +22 -2
  47. package/scripts/config_validator.py +268 -16
  48. package/scripts/doctor.py +1 -0
  49. package/scripts/generate_codex_hooks.py +2 -0
  50. package/scripts/generate_gemini_hooks.py +33 -10
  51. package/scripts/generate_opencode_plugin.py +28 -12
  52. package/scripts/install_steps/ai_tools.py +101 -2
  53. package/scripts/install_steps/hooks.py +25 -1
  54. package/scripts/output_filter_cli.py +347 -0
  55. package/scripts/output_filter_hook.py +23 -0
  56. package/scripts/plugin_schema.py +27 -1
  57. package/scripts/schemas/ai-toolkit-config.schema.json +83 -5
  58. package/scripts/session_state.py +156 -42
  59. package/scripts/tool_output_filter/__init__.py +33 -0
  60. package/scripts/tool_output_filter/contracts.py +173 -0
  61. package/scripts/tool_output_filter/engine.py +260 -0
  62. package/scripts/tool_output_filter/hook_runtime.py +369 -0
  63. package/scripts/tool_output_filter/input.py +56 -0
  64. package/scripts/tool_output_filter/invariants.py +40 -0
  65. package/scripts/tool_output_filter/policy.py +153 -0
  66. package/scripts/tool_output_filter/profiles/__init__.py +68 -0
  67. package/scripts/tool_output_filter/profiles/repeat_lines.py +71 -0
  68. package/scripts/tool_output_filter/profiles/tap_success.py +154 -0
  69. package/scripts/tool_output_filter/recovery.py +846 -0
  70. package/scripts/tool_output_filter/telemetry.py +13 -0
  71. package/scripts/uninstall.py +96 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,40 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.16.0 — Native tool-output filtering (2026-07-24)
11
+
12
+ Minor release. Adds a dependency-free, opt-in native tool-output filter for Claude Code and hardens the enterprise configuration, session-isolation, and editor-adapter surfaces around it. The filter ships disabled; `observe` and `safe` are per-project opt-ins. No source catalog count change (44 agents, 108 skills).
13
+
14
+ ### BREAKING
15
+
16
+ - **Unknown config keys rejected** — `.softspark-toolkit.json` and `ai-toolkit.config.json` are now validated against a closed set of top-level keys. Files carrying typos or unsupported keys that previously validated will fail `config validate` and `config check` until the key is removed.
17
+ - **Constitution Article VII reserved** — reserved articles expand from I-VI to I-VII, so custom amendments must start at article 8 instead of 7. A base config that defines article 7 was valid before and now aborts validation.
18
+ - **Plugin manifests require `requires`** — `plugin.json` must declare a non-empty `requires` map of non-empty string constraints. Existing manifests without the field fail schema validation.
19
+ - **Lock files go stale on every toolkit version bump** — `.softspark-toolkit.lock.json` records the toolkit version and is reported stale when it differs from the running version. This is intentional integrity design: re-run `install --local` or `update --local` after upgrading to refresh the lock.
20
+
21
+ ### Added
22
+
23
+ - **Native tool-output filter** — added a dependency-free, opt-in Claude Code `PostToolUse` filter with `off`, `observe`, and `safe` modes. The initial version handles only allowlisted successful Bash test and validation output through deterministic `repeat-lines` and `tap-success` profiles.
24
+ - **Exact ephemeral recovery** — every safe replacement is preceded by a private, bounded, session-scoped copy of the exact native response object. Opaque handles support manual recovery and cleanup without storing commands, paths, session IDs, or raw output in telemetry.
25
+ - **Output-filter operations** — added inspect, status, recover, and clean entry points under `ai-toolkit output-filter`, plus a deterministic benchmark script for the built-in profiles.
26
+ - **Project policy inheritance** — `toolOutputFilter` participates in schema validation, inheritance, merge, lock, local install, and managed project-policy cleanup.
27
+ - **GitHub Copilot compatibility reference** — added `kb/reference/copilot-compatibility.md` documenting the Copilot integration surface, with cross-links from the Codex CLI, opencode, and global-install references.
28
+
29
+ ### Fixed
30
+
31
+ - **Hook safety** — `guard-path.sh` now blocks when `jq` is unavailable instead of silently skipping path validation. Codex Bash hooks apply the same path guard during `PreToolUse` and `PermissionRequest`.
32
+ - **Filter eligibility** — allowlisted task names now use exact token boundaries, and recovery-backed filtering rejects unsafe or unbounded native session identifiers instead of creating mismatched cleanup paths.
33
+ - **Session isolation** — edit tracking uses per-session state files, hook adapters preserve native OpenCode and Augment session identifiers, unsafe identifiers receive collision-resistant normalized names, and pre-compact snapshots prefer the payload session identifier.
34
+ - **Editor adapter integrity** — OpenCode propagates guard exit code 2 as a blocked tool execution, Gemini preserves malformed settings, rejects symlinked destinations, and writes valid updates atomically, while Claude app exports omit the Claude Code-only output replacement hook.
35
+ - **Statusline token trend** — the documented baseline file now drives a visible input/output token trend, including payloads without optional cost or effort fields.
36
+ - **Brand-voice fact checks** — missing extracted facts now fail the quality gate, while inline-code extraction is restricted to identifier-shaped values to avoid false positives.
37
+ - **Enterprise configuration** — config and lock readers reject non-object JSON and invalid UTF-8 without traceback, invalid output-filter updates preserve the last valid managed policy, and lock staleness checks cover lock format, toolkit version, source, resolved version, and content integrity. Constitution Articles I through VII remain reserved, `requiredPlugins` materializes enforceable enable intent, and plugin manifests validate non-empty dependency constraints.
38
+ - **Recovery cleanup** — session end and global uninstall remove only validated ai-toolkit recovery artifacts, remain idempotent for foreign-only trees, and abort before other mutations when the recovery namespace is unsafe.
39
+ - **Flaky pack-audit tests** — npm's update-notifier stderr no longer corrupts JSON parsing in the package-content tests (`npm_config_update_notifier=false` in the test environment).
40
+ - **Linux-only test failures** — the jq-unavailable guard test hides `jq` via a stub `PATH` (plain `PATH=/bin` keeps `jq` visible on usrmerge systems where `/bin` is `/usr/bin`), and the foreign-recovery uninstall test now asserts the message `uninstall.py` actually prints. Both passed on macOS only because bash 3.2 does not enforce bare `[[ ]]` bats assertions.
41
+
42
+ Hook entries: 28 → 29. Test count: 1377 → 1477.
43
+
10
44
  ## v4.15.1 — Copilot profile cleanup (2026-07-15)
11
45
 
12
46
  ### Fixed
package/README.md CHANGED
@@ -6,15 +6,17 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-108-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-1377%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-1477%20passing-success)](tests/)
10
10
 
11
- ## What's New in v4.15.1
11
+ ## What's New in v4.16.0
12
12
 
13
- v4.15.1 fixes Copilot project upgrades and profile downgrades that could leave obsolete toolkit instructions, prompts, and hooks active.
13
+ v4.16.0 adds a dependency-free, opt-in native tool-output filter for Claude Code and hardens the enterprise configuration surface around it.
14
14
 
15
- - **Complete legacy cleanup**: byte-exact generated files from v3.0.0 through v4.14.1 are recognized and removed when switching a project to `minimal`.
16
- - **No user-file deletion**: unmarked prompts and instructions that do not match a historical toolkit artifact remain untouched.
17
- - **Race-safe removal**: managed cleanup uses pinned directory descriptors and transactional rollback instead of path-based deletion.
15
+ - **Native tool-output filter**: opt-in `PostToolUse` filter with `off`, `observe`, and `safe` modes; ships disabled, handles only allowlisted successful Bash test/validation output.
16
+ - **Exact ephemeral recovery**: every safe replacement keeps a private, bounded, session-scoped copy of the original response, recoverable via `ai-toolkit output-filter recover`.
17
+ - **Output-filter CLI**: `ai-toolkit output-filter status|inspect|recover|clean` plus a deterministic benchmark for the built-in profiles.
18
+ - **Stricter config validation**: unknown top-level config keys are rejected, plugin manifests must declare `requires`, and lock files go stale on toolkit version bumps.
19
+ - **GitHub Copilot compatibility reference**: new `kb/reference/copilot-compatibility.md` documenting the Copilot integration surface.
18
20
 
19
21
  See [CHANGELOG.md](CHANGELOG.md) for full history.
20
22
 
@@ -89,7 +91,10 @@ ai-toolkit claude-app export --verify
89
91
  ```
90
92
 
91
93
  Re-export and re-upload after toolkit or registered-rule updates. Skills work
92
- in Chat and Cowork; hooks and sub-agents are active only in Cowork.
94
+ in Chat and Cowork; hooks and sub-agents are active only in Cowork. The native
95
+ tool-output replacement hook remains Claude Code-only and is excluded from the
96
+ Claude app archive until that runtime has an independently verified replacement
97
+ contract.
93
98
 
94
99
  ### Install Profiles
95
100
 
@@ -140,7 +145,7 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
140
145
  | `skills/` (hybrid) | 30 | Slash commands with agent knowledge base |
141
146
  | `skills/` (knowledge) | 46 | Domain knowledge auto-loaded by agents (includes 13 `<lang>-rules` skills) |
142
147
  | `agents/` | 44 | Specialized agents across 10 categories |
143
- | `hooks/` | 28 entries / 14 events + statusLine | Quality gates, path safety, prompt governance, loop guard, session lifecycle |
148
+ | `hooks/` | 29 entries / 14 events + statusLine | Quality gates, path safety, prompt governance, loop guard, output filtering, session lifecycle |
144
149
  | `plugins/` | 11 packs | Opt-in domain bundles (security, research, frontend, enterprise, 6 language packs) |
145
150
  | `constitution.md` | 7 articles | Machine-enforced safety rules |
146
151
  | `rules/` | auto-synced | Global/project rule files for Claude and other editors |
@@ -156,7 +161,7 @@ ai-toolkit/
156
161
  │ ├── agents/ # 44 agent definitions
157
162
  │ ├── skills/ # 108 skills (task / hybrid / knowledge)
158
163
  │ ├── rules/ # Source rules synced into Claude/editor rule files
159
- │ ├── hooks/ # Hook scripts (28 entries, 14 lifecycle events)
164
+ │ ├── hooks/ # Hook scripts (29 entries, 14 lifecycle events)
160
165
  │ ├── claude-app/ # Generated Chat/Cowork plugin rules, hooks, instructions
161
166
  │ ├── plugins/ # 11 experimental plugin packs (opt-in)
162
167
  │ ├── output-styles/ # System prompt output style overrides
@@ -164,7 +169,7 @@ ai-toolkit/
164
169
  │ └── ARCHITECTURE.md # Full system design
165
170
  ├── kb/ # Reference docs, procedures, plans
166
171
  ├── scripts/ # Validation, install, evaluation scripts
167
- ├── tests/ # Bats test suite (1377 tests)
172
+ ├── tests/ # Bats and Python test suite (1477 tests)
168
173
  └── CHANGELOG.md
169
174
  ```
170
175
 
@@ -176,7 +181,9 @@ ai-toolkit/
176
181
 
177
182
  **Machine-enforced constitution** — 7-article safety constitution enforced via `PreToolUse` hooks that actually block `rm -rf`, `DROP TABLE`, and irreversible operations. Not just documentation.
178
183
 
179
- **29 lifecycle hooks** Executable scripts across 14 events (SessionStart → SessionEnd, plus InstructionsLoaded + ConfigChange). Guards, governance, quality gates, session persistence, MCP health checks, revert protection, test-cohesion enforcement, loop guard, search-first discipline. See [Hooks Catalog](kb/reference/hooks-catalog.md).
184
+ **29 lifecycle hook entries:** Executable handlers across 14 events (SessionStart → SessionEnd, plus InstructionsLoaded + ConfigChange). Guards, governance, quality gates, session persistence, MCP health checks, revert protection, test-cohesion enforcement, loop guard, search-first discipline, and opt-in output filtering. See [Hooks Catalog](kb/reference/hooks-catalog.md).
185
+
186
+ **Native tool-output filtering** — dependency-free, post-execution filtering for successful Claude Code Bash output, shipped **disabled by default** (`off`). Neither `off` nor `observe` ever replaces output; opt-in `safe` mode replaces only validated `repeat-lines` or `tap-success` results after exact ephemeral recovery is available. Failures, diagnostics, unsupported payloads, and unavailable recovery always pass through unchanged. See [Tool Output Filter](kb/reference/tool-output-filter.md).
180
187
 
181
188
  **Security scanning** — `/skill-audit` for code-level risks, `/cve-scan` for dependency CVEs. Both CI-ready with exit codes.
182
189
 
@@ -291,6 +298,7 @@ Need multi-agent coordination?
291
298
  | Ecosystem Comparison | [kb/reference/comparison.md](kb/reference/comparison.md) |
292
299
  | Codex CLI Compatibility | [kb/reference/codex-cli-compatibility.md](kb/reference/codex-cli-compatibility.md) |
293
300
  | opencode Compatibility | [kb/reference/opencode-compatibility.md](kb/reference/opencode-compatibility.md) |
301
+ | GitHub Copilot Compatibility | [kb/reference/copilot-compatibility.md](kb/reference/copilot-compatibility.md) |
294
302
  | Maintenance SOP | [kb/procedures/maintenance-sop.md](kb/procedures/maintenance-sop.md) |
295
303
 
296
304
  ---
@@ -3,7 +3,7 @@
3
3
  "name": "ai-toolkit",
4
4
  "displayName": "AI Toolkit",
5
5
  "description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
6
- "version": "4.15.1",
6
+ "version": "4.16.0",
7
7
  "author": {
8
8
  "name": "SoftSpark",
9
9
  "url": "https://github.com/softspark"
@@ -8,7 +8,7 @@ Universal multi-agent system for software development. Works across all reposito
8
8
  |-----------|-------|
9
9
  | Agents | See agents catalog |
10
10
  | Skills | See skills catalog |
11
- | Hooks | 14 events / 28 entries (SessionStart ×2, Notification ×1, PreToolUse ×5, UserPromptSubmit ×2, PostToolUse ×5, Stop ×4, TaskCompleted ×1, TeammateIdle ×1, SubagentStart ×1, SubagentStop ×1, PreCompact ×2, SessionEnd ×1, InstructionsLoaded ×1, ConfigChange ×1) plus statusLine |
11
+ | Hooks | 14 events / 29 entries (SessionStart ×2, Notification ×1, PreToolUse ×5, UserPromptSubmit ×2, PostToolUse ×6, Stop ×4, TaskCompleted ×1, TeammateIdle ×1, SubagentStart ×1, SubagentStop ×1, PreCompact ×2, SessionEnd ×1, InstructionsLoaded ×1, ConfigChange ×1) plus statusLine |
12
12
 
13
13
  ---
14
14
 
@@ -333,6 +333,7 @@ Lead Session (You)
333
333
  | `UserPromptSubmit` | Before prompt execution | Usage tracking (skill invocations) |
334
334
  | `PostToolUse` | After edit/write tools | Lightweight validation reminder |
335
335
  | `PostToolUse` | After search tools | Clears the per-session search-first flag |
336
+ | `PostToolUse` | After successful Bash tools | Observes or safely replaces eligible text output after exact recovery is stored; Claude Code only |
336
337
  | `Stop` | After Claude response | Multi-language quality check + saves session context |
337
338
  | `Stop` | After Claude response | Blocks skipped search-first only when a real MCP/Web search provider is configured, with Codex log fallback |
338
339
  | `TaskCompleted` | Teammate marks task done | Multi-language lint + type check (blocking) |
@@ -341,7 +342,7 @@ Lead Session (You)
341
342
  | `SubagentStop` | Subagent completes | Handoff checklist for spawned subagents |
342
343
  | `Notification` | Claude notification | OS notification |
343
344
  | `PreCompact` | Before compaction | Saves context before compaction boundary |
344
- | `SessionEnd` | Claude session ends | Writes handoff snapshot for the next session |
345
+ | `SessionEnd` | Claude session ends | Cleans owned output recovery, then writes the next-session handoff snapshot |
345
346
 
346
347
  ---
347
348
 
@@ -376,7 +377,7 @@ The `inject_section_cli.py` script provides a stable marker-based injection API.
376
377
  `scripts/compile_slm.py` compiles the full toolkit (20K+ tokens) into a minimal system prompt for Small Language Models (2K-16K tokens). Pipeline: Parse → Score → Compress → Pack → Emit. Supports 4 compression levels (ultra-light, light, standard, extended), 4 output formats (raw, ollama, json-string, aider), persona-aware scoring, and language-aware rule filtering. Profile `offline-slm` in `manifest.json`. Constitution is always included (non-negotiable).
377
378
 
378
379
  ### Config Inheritance (`extends`)
379
- `scripts/config_resolver.py`, `config_merger.py`, `config_validator.py`, `config_cli.py`, `config_scaffold.py`, `config_lock.py`. Enterprise configuration inheritance via `.softspark-toolkit.json` `extends` field. Resolves base configs from npm packages, Git URLs, or local paths. Layered deep merge with constitution immutability (Articles I-VII absolute), enforce constraints (`requiredAgents`, `forbidOverride`, `minHookProfile`), override validation (`override: true` + justification), and lock file (`.softspark-toolkit.lock.json`). CLI: `config validate`, `config diff`, `config init`, `config create-base`, `config check`. Integrated into `install --local` and `update --local` flows.
380
+ `scripts/config_resolver.py`, `config_merger.py`, `config_validator.py`, `config_cli.py`, `config_scaffold.py`, `config_lock.py`. Enterprise configuration inheritance via `.softspark-toolkit.json` `extends` field. Resolves base configs from npm packages, Git URLs, or local paths. Layered deep merge with constitution immutability (Articles I-VII absolute), enforce constraints (`requiredAgents`, `requiredPlugins`, `forbidOverride`, `minHookProfile`), override validation (`override: true` + justification), and a source/version/integrity lock file (`.softspark-toolkit.lock.json`). `requiredPlugins` materializes configuration intent and does not install plugin packs. CLI: `config validate`, `config diff`, `config init`, `config create-base`, `config check`. Integrated into `install --local` and `update --local` flows.
380
381
 
381
382
  ### Project Registry
382
383
  `scripts/install_steps/project_registry.py`, `scripts/update_projects.py`, `scripts/projects_cli.py`. Tracks all `--local` installed projects in `~/.softspark/ai-toolkit/projects.json`. `ai-toolkit update` propagates to all registered projects in parallel via `ThreadPoolExecutor`. CLI: `ai-toolkit projects`, `--prune`, `remove <path>`.
@@ -20,8 +20,11 @@ hook_prompt() {
20
20
  }
21
21
 
22
22
  hook_session_id() {
23
- local sid
24
- sid=$(hook_json '.session_id // empty')
23
+ local sid raw_sid digest
24
+ sid=$(hook_json '.session_id // .conversation_id // .conversationId // empty')
25
+ if [ -z "$sid" ] || [ "$sid" = "null" ]; then
26
+ sid="${CLAUDE_SESSION_ID:-}"
27
+ fi
25
28
  if [ -z "$sid" ] || [ "$sid" = "null" ]; then
26
29
  local tp
27
30
  tp=$(hook_json '.transcript_path // empty')
@@ -30,7 +33,19 @@ hook_session_id() {
30
33
  fi
31
34
  fi
32
35
  [ -z "$sid" ] && sid="default"
33
- printf '%s' "$sid" | LC_ALL=C tr -c 'a-zA-Z0-9_-' '_'
36
+ raw_sid="$sid"
37
+ sid=$(printf '%s' "$sid" | LC_ALL=C tr -c 'a-zA-Z0-9_-' '_')
38
+ if [ "$sid" != "$raw_sid" ] || [ "${#sid}" -gt 160 ]; then
39
+ if command -v shasum >/dev/null 2>&1; then
40
+ digest=$(printf '%s' "$raw_sid" | shasum -a 256 | cut -d ' ' -f 1)
41
+ elif command -v sha256sum >/dev/null 2>&1; then
42
+ digest=$(printf '%s' "$raw_sid" | sha256sum | cut -d ' ' -f 1)
43
+ else
44
+ digest=$(printf '%s' "$raw_sid" | cksum | cut -d ' ' -f 1)
45
+ fi
46
+ sid="${sid:0:135}-${digest:0:24}"
47
+ fi
48
+ printf '%s' "$sid"
34
49
  }
35
50
 
36
51
  hook_command() {
@@ -26,6 +26,7 @@
26
26
  # AI_TOOLKIT_STATUSLINE_NO_GIT — "1" hides git segment
27
27
  # AI_TOOLKIT_STATUSLINE_NO_EFFORT — "1" hides effort segment
28
28
  # AI_TOOLKIT_STATUSLINE_SHOW_COST — "1" appends Claude Code's reported cost
29
+ # AI_TOOLKIT_STATUSLINE_BASELINE — totals JSON file used for token trend
29
30
  # AI_TOOLKIT_STATUSLINE_DUMP — "1" writes stdin to /tmp/cc-statusline-input.json
30
31
  #
31
32
  # Performance: ~50ms (single python3 parse of stdin, one git invocation).
@@ -62,7 +63,7 @@ if [ "${AI_TOOLKIT_STATUSLINE_DUMP:-0}" = "1" ] && [ -n "$INPUT" ]; then
62
63
  fi
63
64
 
64
65
  PARSED="$(printf '%s' "$INPUT" | python3 -c '
65
- import json, sys
66
+ import json, os, sys
66
67
  def get(d, path):
67
68
  for k in path.split("."):
68
69
  d = d.get(k) if isinstance(d, dict) else None
@@ -77,6 +78,11 @@ def fmt_tokens(n):
77
78
  if n >= 1000:
78
79
  return f"{n/1000:.1f}k"
79
80
  return str(n)
81
+ def as_int(n):
82
+ try:
83
+ return int(n)
84
+ except (TypeError, ValueError):
85
+ return 0
80
86
  try:
81
87
  d = json.loads(sys.stdin.read() or "{}")
82
88
  except Exception:
@@ -84,18 +90,34 @@ except Exception:
84
90
  cwd = get(d, "cwd") or ""
85
91
  model = get(d, "model.display_name") or get(d, "model.id") or ""
86
92
  ctx_pct = get(d, "context_window.used_percentage")
87
- in_tok = fmt_tokens(get(d, "context_window.total_input_tokens"))
88
- out_tok = fmt_tokens(get(d, "context_window.total_output_tokens"))
93
+ in_raw = get(d, "context_window.total_input_tokens")
94
+ out_raw = get(d, "context_window.total_output_tokens")
95
+ in_tok = fmt_tokens(in_raw)
96
+ out_tok = fmt_tokens(out_raw)
89
97
  cost = get(d, "cost.total_cost_usd")
90
98
  try:
91
99
  cost_str = f"{float(cost):.2f}" if cost != "" else ""
92
100
  except (TypeError, ValueError):
93
101
  cost_str = ""
94
102
  effort = get(d, "effort.level") or ""
95
- print(f"{cwd}\t{model}\t{ctx_pct}\t{in_tok}\t{out_tok}\t{cost_str}\t{effort}")
103
+ trend = ""
104
+ baseline_path = os.environ.get("AI_TOOLKIT_STATUSLINE_BASELINE", "")
105
+ if baseline_path:
106
+ try:
107
+ with open(os.path.expanduser(baseline_path), encoding="utf-8") as baseline_file:
108
+ baseline = json.load(baseline_file)
109
+ baseline_total = baseline.get("total") if isinstance(baseline, dict) else None
110
+ if isinstance(baseline_total, (int, float)) and baseline_total > 0:
111
+ delta = (as_int(in_raw) + as_int(out_raw) - baseline_total) / baseline_total
112
+ arrow = "↓" if delta < 0 else "↑" if delta > 0 else "·"
113
+ trend = f"{arrow}{abs(delta) * 100:.0f}%"
114
+ except (OSError, json.JSONDecodeError, TypeError, ValueError):
115
+ pass
116
+ fields = (cwd, model, ctx_pct, in_tok, out_tok, cost_str, effort, trend)
117
+ print("\x1f".join(str(field) for field in fields))
96
118
  ' 2>/dev/null)"
97
119
 
98
- IFS=$'\t' read -r CWD MODEL_NAME CTX_USED IN_TOK OUT_TOK COST EFFORT <<< "$PARSED"
120
+ IFS=$'\x1f' read -r CWD MODEL_NAME CTX_USED IN_TOK OUT_TOK COST EFFORT TREND <<< "$PARSED"
99
121
  [ -z "$CWD" ] && CWD="$PWD"
100
122
 
101
123
  # ── Segment: prompt + dir ────────────────────────────────────────────────────
@@ -162,6 +184,9 @@ SEG_TOKENS=""
162
184
  if [ "${AI_TOOLKIT_STATUSLINE_NO_TOKENS:-0}" != "1" ] && [ -n "$IN_TOK" ] && \
163
185
  { [ "$IN_TOK" != "0" ] || [ "$OUT_TOK" != "0" ]; }; then
164
186
  SEG_TOKENS=" ${C_BOLD_GREEN}\xe2\x86\x91${IN_TOK}${C_RESET} ${C_RED}\xe2\x86\x93${OUT_TOK}${C_RESET}"
187
+ if [ -n "$TREND" ]; then
188
+ SEG_TOKENS+=" ${C_DIM}trend:${C_RESET}${TREND}"
189
+ fi
165
190
  if [ "${AI_TOOLKIT_STATUSLINE_SHOW_COST:-0}" = "1" ] && [ -n "$COST" ]; then
166
191
  SEG_TOKENS+=" ${C_BOLD_GREEN}\$${COST}${C_RESET}"
167
192
  fi
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ # Claude PostToolUse adapter for the native tool-output filter.
3
+
4
+ HOOK_SOURCE="${BASH_SOURCE[0]}"
5
+ HOOK_DIR="${HOOK_SOURCE%/*}"
6
+ [[ "$HOOK_DIR" == "$HOOK_SOURCE" ]] && HOOK_DIR="."
7
+ # shellcheck source=_profile-check.sh
8
+ source "$HOOK_DIR/_profile-check.sh"
9
+
10
+ OWNER_MARKER="ai-toolkit-output-filter-policy-v1"
11
+ GLOBAL_POLICY="$HOME/.softspark/ai-toolkit/hooks/output-filter-policy.json"
12
+ PROJECTS_REGISTRY="$HOME/.softspark/ai-toolkit/projects.json"
13
+
14
+ is_regular_file() {
15
+ [[ -f "$1" && -r "$1" && ! -L "$1" ]]
16
+ }
17
+
18
+ # A project policy is trusted only for projects the user registered via
19
+ # `ai-toolkit install --local`. The owner marker alone is a public constant,
20
+ # so a cloned repo must never be able to self-enable filtering with it.
21
+ is_registered_project() {
22
+ is_regular_file "$PROJECTS_REGISTRY" &&
23
+ grep -qF "\"$1\"" "$PROJECTS_REGISTRY" 2>/dev/null
24
+ }
25
+
26
+ if [[ "${AI_TOOLKIT_OUTPUT_FILTER_DISABLE:-}" == "1" ]]; then
27
+ exit 0
28
+ fi
29
+
30
+ if [[ -n "${AI_TOOLKIT_OUTPUT_FILTER_POLICY:-}" ]]; then
31
+ POLICY_PATH="$AI_TOOLKIT_OUTPUT_FILTER_POLICY"
32
+ if ! is_regular_file "$POLICY_PATH"; then
33
+ exit 0
34
+ fi
35
+ else
36
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
37
+ PROJECT_POLICY="$PROJECT_ROOT/.claude/ai-toolkit-output-filter.json"
38
+ PROJECT_OWNER="$PROJECT_ROOT/.claude/.ai-toolkit-output-filter.owner"
39
+ if [[ -L "$PROJECT_ROOT" || -L "$PROJECT_ROOT/.claude" ]]; then
40
+ exit 0
41
+ fi
42
+ if is_registered_project "$PROJECT_ROOT" &&
43
+ is_regular_file "$PROJECT_OWNER" &&
44
+ [[ "$(<"$PROJECT_OWNER")" == "$OWNER_MARKER" ]]; then
45
+ if ! is_regular_file "$PROJECT_POLICY"; then
46
+ exit 0
47
+ fi
48
+ POLICY_PATH="$PROJECT_POLICY"
49
+ else
50
+ POLICY_PATH="$GLOBAL_POLICY"
51
+ fi
52
+ fi
53
+
54
+ if ! is_regular_file "$POLICY_PATH"; then
55
+ exit 0
56
+ fi
57
+ POLICY_CONTENT="$(<"$POLICY_PATH")" || exit 0
58
+ if [[ ${#POLICY_CONTENT} -gt 65536 ]]; then
59
+ exit 0
60
+ fi
61
+ MODE_OFF_PATTERN='"mode"[[:space:]]*:[[:space:]]*"off"'
62
+ MODE_ACTIVE_PATTERN='"mode"[[:space:]]*:[[:space:]]*"(observe|safe)"'
63
+ if [[ "$POLICY_CONTENT" =~ $MODE_OFF_PATTERN ]]; then
64
+ exit 0
65
+ fi
66
+ if [[ ! "$POLICY_CONTENT" =~ $MODE_ACTIVE_PATTERN ]]; then
67
+ exit 0
68
+ fi
69
+
70
+ RUNTIME_PATH="${AI_TOOLKIT_OUTPUT_FILTER_HOOK_RUNTIME:-${AI_TOOLKIT_OUTPUT_FILTER_CLI:-$HOME/.softspark/ai-toolkit/scripts/output_filter_hook.py}}"
71
+ if ! is_regular_file "$RUNTIME_PATH"; then
72
+ exit 0
73
+ fi
74
+ python3 -S "$RUNTIME_PATH" hook --policy "$POLICY_PATH" 2>/dev/null || true
75
+
76
+ exit 0
@@ -11,12 +11,12 @@ source "$(dirname "$0")/_profile-check.sh"
11
11
  LOG_DIR="$HOME/.softspark/ai-toolkit"
12
12
  LOG_FILE="$LOG_DIR/governance.log"
13
13
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
14
- SESSION="${CLAUDE_SESSION_ID:-unknown}"
15
14
 
16
15
  # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
17
16
  INPUT=$(cat)
18
17
  # shellcheck source=_hook-io.sh
19
18
  source "$(dirname "$0")/_hook-io.sh"
19
+ SESSION=$(hook_session_id)
20
20
  TOOL_NAME=$(hook_tool_name)
21
21
 
22
22
  if [ -z "$TOOL_NAME" ]; then
@@ -14,8 +14,8 @@ REAL_HOME="$HOME"
14
14
 
15
15
  # Verify jq is available — required for JSON parsing
16
16
  if ! command -v jq >/dev/null 2>&1; then
17
- echo "WARNING: guard-path.sh requires jq but it is not installed. Path validation skipped." >&2
18
- exit 0
17
+ echo "BLOCKED: guard-path.sh requires jq but it is not installed. Path validation cannot run safely." >&2
18
+ exit 2
19
19
  fi
20
20
 
21
21
  # Collect ALL path values from tool input into a single list (one per line).
@@ -12,18 +12,20 @@ source "$(dirname "$0")/_locate-toolkit.sh"
12
12
  # shellcheck source=_hook-io.sh
13
13
  source "$(dirname "$0")/_hook-io.sh"
14
14
 
15
- # Read from stdin (Claude Code passes JSON with .tool_name, .tool_input)
15
+ # Read from stdin (Claude Code passes JSON with .tool_name, .tool_input).
16
+ # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
16
17
  INPUT=$(cat)
17
18
  TOOL_NAME=$(hook_tool_name)
18
19
  [ -z "$TOOL_NAME" ] && TOOL_NAME="unknown"
19
20
  FILE_PATH=$(hook_file_path)
20
- SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
21
+ SESSION_ID=$(hook_session_id)
22
+ SESSION_ARGS=(--session-id "$SESSION_ID")
21
23
 
22
24
  # Append edit to session state (used by revert-guard, test-cohesion, quality-gate).
23
25
  if [ -n "$FILE_PATH" ] && [ -n "$TOOLKIT_DIR" ] && command -v python3 >/dev/null 2>&1; then
24
26
  python3 "$TOOLKIT_DIR/scripts/session_state.py" append \
25
27
  --tool "$TOOL_NAME" --path "$FILE_PATH" \
26
- ${SESSION_ID:+--session-id "$SESSION_ID"} >/dev/null 2>&1 || true
28
+ "${SESSION_ARGS[@]}" >/dev/null 2>&1 || true
27
29
  fi
28
30
 
29
31
  if [ -z "$FILE_PATH" ]; then
@@ -7,19 +7,20 @@
7
7
 
8
8
  # shellcheck source=_profile-check.sh
9
9
  source "$(dirname "$0")/_profile-check.sh"
10
+ # shellcheck source=_hook-io.sh
11
+ source "$(dirname "$0")/_hook-io.sh"
10
12
 
11
13
  SAVE_DIR="$HOME/.softspark/ai-toolkit/compactions"
12
14
  mkdir -p "$SAVE_DIR"
13
15
 
14
16
  # Read from stdin (Claude Code passes JSON with .session_id)
17
+ # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
15
18
  INPUT=$(cat)
16
19
  TIMESTAMP=$(date -u +"%Y-%m-%d_%H-%M-%S")
17
- SESSION=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
18
- [ -z "$SESSION" ] && SESSION="$$"
20
+ SESSION=$(hook_session_id)
19
21
  SAVE_FILE="$SAVE_DIR/${TIMESTAMP}_${SESSION}.txt"
20
22
 
21
23
  # Gather context
22
- SESSION="${CLAUDE_SESSION_ID:-unknown}"
23
24
  WORKDIR="$(pwd)"
24
25
  BRANCH=""
25
26
  if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
@@ -9,6 +9,16 @@
9
9
  source "$(dirname "$0")/_profile-check.sh"
10
10
  # shellcheck source=_locate-toolkit.sh
11
11
  source "$(dirname "$0")/_locate-toolkit.sh"
12
+ # shellcheck source=_hook-io.sh
13
+ source "$(dirname "$0")/_hook-io.sh"
14
+
15
+ INPUT=""
16
+ if [ ! -t 0 ]; then
17
+ # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
18
+ INPUT=$(cat)
19
+ fi
20
+ SESSION_ID=$(hook_session_id)
21
+ SESSION_ARGS=(--session-id "$SESSION_ID")
12
22
 
13
23
  run_required() {
14
24
  local label="$1"
@@ -44,7 +54,8 @@ cohesion_for_session_edits() {
44
54
  command -v python3 >/dev/null 2>&1 || return 0
45
55
 
46
56
  local edits
47
- edits=$(python3 "$TOOLKIT_DIR/scripts/session_state.py" list 2>/dev/null)
57
+ edits=$(python3 "$TOOLKIT_DIR/scripts/session_state.py" list \
58
+ "${SESSION_ARGS[@]}" 2>/dev/null)
48
59
  [ -z "$edits" ] && return 0
49
60
 
50
61
  # shellcheck disable=SC2206 # word-splitting is intentional, paths have no spaces
@@ -25,6 +25,8 @@ source "$(dirname "$0")/_hook-io.sh"
25
25
  # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
26
26
  INPUT=$(cat)
27
27
  COMMAND=$(hook_command)
28
+ SESSION_ID=$(hook_session_id)
29
+ SESSION_ARGS=(--session-id "$SESSION_ID")
28
30
 
29
31
  [ -z "$COMMAND" ] && exit 0
30
32
  [ "${CLAUDE_REVERT_OK:-0}" = "1" ] && exit 0
@@ -54,7 +56,7 @@ EOF
54
56
 
55
57
  # git reset --hard / git clean -fd: clobber-style, scope = all session edits.
56
58
  if printf '%s' "$COMMAND" | grep -Eq 'reset[[:space:]]+(-+[A-Za-z-]+[[:space:]]+)*--hard|clean[[:space:]]+-[A-Za-z]*[df]'; then
57
- edited=$(python3 "$TOOLKIT_DIR/scripts/session_state.py" list 2>/dev/null | head -10)
59
+ edited=$(python3 "$TOOLKIT_DIR/scripts/session_state.py" list "${SESSION_ARGS[@]}" 2>/dev/null | head -10)
58
60
  if [ -n "$edited" ]; then
59
61
  _block "destructive 'git reset --hard' / 'git clean' with session edits in tree" "$edited"
60
62
  fi
@@ -73,7 +75,8 @@ if printf '%s' "$COMMAND" | grep -Eq '(checkout|restore)([[:space:]].*)?[[:space
73
75
  /*) abs="$f" ;;
74
76
  *) abs="$PWD/$f" ;;
75
77
  esac
76
- if python3 "$TOOLKIT_DIR/scripts/session_state.py" was-edited "$abs" >/dev/null 2>&1; then
78
+ if python3 "$TOOLKIT_DIR/scripts/session_state.py" was-edited "$abs" \
79
+ "${SESSION_ARGS[@]}" >/dev/null 2>&1; then
77
80
  blocked="$blocked $f"
78
81
  fi
79
82
  done
@@ -8,15 +8,17 @@
8
8
  source "$(dirname "$0")/_profile-check.sh"
9
9
  # shellcheck source=_session-paths.sh
10
10
  source "$(dirname "$0")/_session-paths.sh"
11
+ # shellcheck source=_hook-io.sh
12
+ source "$(dirname "$0")/_hook-io.sh"
11
13
 
12
14
  # Read from stdin (Claude Code passes JSON with .session_id, .last_assistant_message)
13
15
  INPUT=$(cat)
14
- SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
16
+ SESSION_ID=$(hook_session_id)
15
17
  LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // "No summary available"' 2>/dev/null | head -5)
16
18
 
17
19
  SESSION_FILE="$SESSION_CONTEXT_FILE"
18
20
 
19
- if [ -n "$SESSION_ID" ]; then
21
+ if [ "$SESSION_ID" != "default" ]; then
20
22
  mkdir -p "$SESSION_DIR"
21
23
 
22
24
  # Gather git state for richer context
@@ -3,12 +3,45 @@
3
3
  #
4
4
  # Fires on: SessionEnd
5
5
  # Matcher: all
6
- # Skipped when TOOLKIT_HOOK_PROFILE=minimal.
6
+ # The handoff snapshot is skipped for the minimal profile. Owned recovery
7
+ # cleanup still runs for every profile.
7
8
 
8
- # shellcheck source=_profile-check.sh
9
- source "$(dirname "$0")/_profile-check.sh"
10
9
  # shellcheck source=_session-paths.sh
11
10
  source "$(dirname "$0")/_session-paths.sh"
11
+ # shellcheck source=_hook-io.sh
12
+ source "$(dirname "$0")/_hook-io.sh"
13
+
14
+ # Cleanup must run before _profile-check.sh can exit for the minimal profile.
15
+ INPUT=""
16
+ if [ ! -t 0 ]; then
17
+ # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
18
+ INPUT=$(cat)
19
+ fi
20
+ SESSION_ID=$(hook_session_id)
21
+ RECOVERY_ROOT="$SESSION_DIR/output-filter"
22
+ OUTPUT_FILTER_CLI="${AI_TOOLKIT_OUTPUT_FILTER_CLI:-$HOME/.softspark/ai-toolkit/scripts/output_filter_cli.py}"
23
+ if [ "$SESSION_ID" != "default" ] &&
24
+ [ -d "$RECOVERY_ROOT" ] &&
25
+ [ ! -L "$RECOVERY_ROOT" ] &&
26
+ [ -f "$OUTPUT_FILTER_CLI" ] &&
27
+ [ ! -L "$OUTPUT_FILTER_CLI" ] &&
28
+ command -v python3 >/dev/null 2>&1; then
29
+ python3 -S "$OUTPUT_FILTER_CLI" clean \
30
+ --base-directory "$SESSION_DIR" \
31
+ --session-id "$SESSION_ID" >/dev/null 2>&1 || true
32
+ fi
33
+
34
+ SESSION_STATE_CLI="${AI_TOOLKIT_SESSION_STATE_CLI:-$HOME/.softspark/ai-toolkit/scripts/session_state.py}"
35
+ if [ "$SESSION_ID" != "default" ] &&
36
+ [ -f "$SESSION_STATE_CLI" ] &&
37
+ [ ! -L "$SESSION_STATE_CLI" ] &&
38
+ command -v python3 >/dev/null 2>&1; then
39
+ python3 -S "$SESSION_STATE_CLI" clean \
40
+ --session-id "$SESSION_ID" >/dev/null 2>&1 || true
41
+ fi
42
+
43
+ # shellcheck source=_profile-check.sh
44
+ source "$(dirname "$0")/_profile-check.sh"
12
45
 
13
46
  SESSION_FILE="$SESSION_CONTEXT_FILE"
14
47
  HANDOFF_FILE="$SESSION_END_FILE"
@@ -35,4 +68,3 @@ else
35
68
  fi
36
69
 
37
70
  exit 0
38
-
@@ -8,6 +8,8 @@
8
8
  source "$(dirname "$0")/_locate-toolkit.sh"
9
9
  # shellcheck source=_session-paths.sh
10
10
  source "$(dirname "$0")/_session-paths.sh"
11
+ # shellcheck source=_hook-io.sh
12
+ source "$(dirname "$0")/_hook-io.sh"
11
13
 
12
14
  emit_context() {
13
15
  [ "${AI_TOOLKIT_HOOK_QUIET:-0}" = "1" ] && return 0
@@ -20,14 +22,18 @@ emit_context "MANDATORY: Before answering ANY technical question, apply ALL rule
20
22
  emit_context "REMINDER: When writing features or fixing bugs, ensure tests cover the changes. When modifying API, config, or setup, update relevant documentation. Propose these steps to the user — do not silently skip them."
21
23
 
22
24
  # 1a. Reset per-session edit state (used by revert-guard, test-cohesion, quality-gate)
23
- SESSION_ID_INPUT=""
25
+ INPUT=""
24
26
  if [ ! -t 0 ]; then
25
- STDIN_PAYLOAD="$(cat)"
26
- SESSION_ID_INPUT="$(printf '%s' "$STDIN_PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)"
27
+ # shellcheck disable=SC2034 # INPUT is consumed via sourced _hook-io.sh
28
+ INPUT="$(cat)"
27
29
  fi
28
- if [ -n "$TOOLKIT_DIR" ] && command -v python3 >/dev/null 2>&1; then
30
+ SESSION_ID_INPUT=$(hook_session_id)
31
+ SESSION_SOURCE=$(hook_json '.source // empty')
32
+ if [ "$SESSION_SOURCE" != "compact" ] &&
33
+ [ -n "$TOOLKIT_DIR" ] &&
34
+ command -v python3 >/dev/null 2>&1; then
29
35
  python3 "$TOOLKIT_DIR/scripts/session_state.py" reset \
30
- ${SESSION_ID_INPUT:+--session-id "$SESSION_ID_INPUT"} >/dev/null 2>&1 || true
36
+ --session-id "$SESSION_ID_INPUT" >/dev/null 2>&1 || true
31
37
  fi
32
38
 
33
39
  # 1b. GC stale per-session search-required flags (older than 60 min)
package/app/hooks.json CHANGED
@@ -158,6 +158,16 @@
158
158
  "command": "\"$HOME/.softspark/ai-toolkit/hooks/search-tracker.sh\""
159
159
  }
160
160
  ]
161
+ },
162
+ {
163
+ "_source": "ai-toolkit",
164
+ "matcher": "Bash",
165
+ "hooks": [
166
+ {
167
+ "type": "command",
168
+ "command": "\"$HOME/.softspark/ai-toolkit/hooks/filter-tool-output.sh\""
169
+ }
170
+ ]
161
171
  }
162
172
  ],
163
173
  "Stop": [
@@ -0,0 +1,15 @@
1
+ {
2
+ "mode": "off",
3
+ "profiles": [
4
+ "repeat-lines",
5
+ "tap-success"
6
+ ],
7
+ "maxInputBytes": 8388608,
8
+ "minSavingsBytes": 1024,
9
+ "minSavingsRatio": 0.15,
10
+ "recovery": {
11
+ "mode": "ephemeral",
12
+ "ttlMinutes": 60,
13
+ "maxSessionBytes": 33554432
14
+ }
15
+ }