@softspark/ai-toolkit 4.6.0 → 4.8.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/CHANGELOG.md +32 -0
- package/README.md +10 -11
- package/app/.claude-plugin/plugin.json +1 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +21 -12
- package/kb/procedures/maintenance-sop.md +9 -8
- package/kb/procedures/release-verification-sop.md +1 -1
- package/kb/reference/architecture-overview.md +4 -4
- package/kb/reference/hooks-catalog.md +27 -2
- package/kb/reference/supported-tools-registry.md +31 -25
- package/llms-full.txt +72 -40
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/ecosystem_tools.json +62 -9
- package/scripts/generate_antigravity.py +22 -15
- package/scripts/generate_cursor_mdc.py +4 -3
- package/scripts/generate_devin_hooks.py +149 -0
- package/scripts/generate_windsurf_hooks.py +6 -0
- package/scripts/generate_windsurf_rules.py +39 -24
- package/scripts/generate_windsurf_skills.py +14 -4
- package/scripts/install_steps/ai_tools.py +24 -4
- package/scripts/validate.py +7 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate ``.devin/hooks.v1.json`` for the Devin CLI (formerly Windsurf).
|
|
3
|
+
|
|
4
|
+
Devin CLI uses a hook format **compatible with Claude Code hooks**
|
|
5
|
+
(docs.devin.ai/cli/extensibility/hooks/overview). This generator is the
|
|
6
|
+
replacement for the deprecated Cascade ``.windsurf/hooks.json`` surface,
|
|
7
|
+
which stops working when Cascade sunsets on 2026-07-01. Devin Local / Devin
|
|
8
|
+
CLI do NOT read ``.windsurf/hooks.json`` as a fallback, so the hooks must be
|
|
9
|
+
regenerated onto this new file.
|
|
10
|
+
|
|
11
|
+
Output file: ``<target>/.devin/hooks.v1.json``. Per the Devin docs the
|
|
12
|
+
standalone ``hooks.v1.json`` file's entire contents ARE the hooks object —
|
|
13
|
+
there is **no** top-level ``"hooks"`` wrapper key (unlike
|
|
14
|
+
``.claude/settings.json`` or ``.devin/config.json``).
|
|
15
|
+
|
|
16
|
+
Events use Claude-style PascalCase names. Matchers are regexes against the
|
|
17
|
+
Devin **tool name** (``read``, ``edit``, ``exec``, ``grep``, ``glob``,
|
|
18
|
+
``mcp__<server>__<tool>``) — NOT Claude's ``Bash``/``Edit`` names — so the
|
|
19
|
+
shared guard scripts reliably fire under Devin.
|
|
20
|
+
|
|
21
|
+
Blocking contract: the shared guard scripts emit ``{"decision":"block",
|
|
22
|
+
"reason":...}`` on stdout (plain mode) AND exit 2 — Devin honors both (docs:
|
|
23
|
+
exit 2 = deny; JSON ``{"decision":"block"}`` = deny). Hooks therefore run
|
|
24
|
+
WITHOUT ``AI_TOOLKIT_HOOK_FORMAT=json`` because Devin expects the flat
|
|
25
|
+
``{"decision","reason"}`` shape, not Claude's ``hookSpecificOutput`` envelope.
|
|
26
|
+
|
|
27
|
+
Existing user hook entries are preserved; only entries tagged
|
|
28
|
+
``_source: ai-toolkit`` are replaced on regeneration.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
python3 scripts/generate_devin_hooks.py [target-dir]
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
HOOKS_PREFIX = '"$HOME/.softspark/ai-toolkit/hooks/'
|
|
40
|
+
SOURCE_TAG = "ai-toolkit"
|
|
41
|
+
|
|
42
|
+
# event -> list of (matcher_regex, [script names]).
|
|
43
|
+
# Matchers target Devin tool names: read, edit, exec, mcp__<server>__<tool>.
|
|
44
|
+
# An empty matcher fires for every tool name (Devin: omitted/empty = match all).
|
|
45
|
+
DEVIN_HOOKS: dict[str, list[tuple[str, list[str]]]] = {
|
|
46
|
+
"PreToolUse": [
|
|
47
|
+
("^(read|edit)$", ["guard-path.sh"]),
|
|
48
|
+
("^edit$", ["guard-config.sh"]),
|
|
49
|
+
("^exec$", ["guard-destructive.sh", "commit-quality.sh", "revert-guard.sh"]),
|
|
50
|
+
("^mcp__", ["guard-config.sh"]),
|
|
51
|
+
],
|
|
52
|
+
"PostToolUse": [
|
|
53
|
+
("^edit$", ["post-tool-use.sh", "governance-capture.sh", "test-cohesion.sh"]),
|
|
54
|
+
("^exec$", ["governance-capture.sh"]),
|
|
55
|
+
("^mcp__.*__(smart_query|hybrid_search_kb|crag_search|multi_hop_search|verify_answer)$",
|
|
56
|
+
["search-tracker.sh"]),
|
|
57
|
+
],
|
|
58
|
+
"UserPromptSubmit": [
|
|
59
|
+
("", ["user-prompt-submit.sh", "track-usage.sh"]),
|
|
60
|
+
],
|
|
61
|
+
"Stop": [
|
|
62
|
+
("", ["quality-check.sh", "save-session.sh", "stop-search-check.sh"]),
|
|
63
|
+
],
|
|
64
|
+
# Cascade's post_setup_worktree has no Devin equivalent; session-context
|
|
65
|
+
# moves to SessionStart (Devin fires SessionStart when a session begins).
|
|
66
|
+
"SessionStart": [
|
|
67
|
+
("", ["session-context.sh"]),
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_hook_entry(matcher: str, scripts: list[str]) -> dict:
|
|
73
|
+
"""Build one Devin matcher-group: ``{matcher, hooks:[{type,command}]}``."""
|
|
74
|
+
return {
|
|
75
|
+
"_source": SOURCE_TAG,
|
|
76
|
+
"matcher": matcher,
|
|
77
|
+
"hooks": [
|
|
78
|
+
{"type": "command", "command": f'{HOOKS_PREFIX}{s}"'}
|
|
79
|
+
for s in scripts
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_toolkit_hooks() -> dict[str, list[dict]]:
|
|
85
|
+
return {
|
|
86
|
+
event: [build_hook_entry(matcher, scripts) for matcher, scripts in groups]
|
|
87
|
+
for event, groups in DEVIN_HOOKS.items()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_toolkit_entry(entry: dict) -> bool:
|
|
92
|
+
return isinstance(entry, dict) and entry.get("_source") == SOURCE_TAG
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def strip_toolkit_hooks(hooks: dict) -> dict:
|
|
96
|
+
"""Drop ai-toolkit matcher-groups; keep user-authored entries."""
|
|
97
|
+
kept: dict = {}
|
|
98
|
+
for event, entries in hooks.items():
|
|
99
|
+
if not isinstance(entries, list):
|
|
100
|
+
kept[event] = entries
|
|
101
|
+
continue
|
|
102
|
+
survivors = [e for e in entries if not _is_toolkit_entry(e)]
|
|
103
|
+
if survivors:
|
|
104
|
+
kept[event] = survivors
|
|
105
|
+
return kept
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def merge_hooks(existing: dict, toolkit: dict) -> dict:
|
|
109
|
+
merged = strip_toolkit_hooks(existing)
|
|
110
|
+
for event, entries in toolkit.items():
|
|
111
|
+
merged.setdefault(event, []).extend(entries)
|
|
112
|
+
return merged
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def generate(target_dir: Path) -> Path:
|
|
116
|
+
devin_dir = target_dir / ".devin"
|
|
117
|
+
devin_dir.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
path = devin_dir / "hooks.v1.json"
|
|
119
|
+
|
|
120
|
+
# The standalone hooks.v1.json file IS the hooks object (no wrapper key).
|
|
121
|
+
existing: dict = {}
|
|
122
|
+
if path.is_file():
|
|
123
|
+
try:
|
|
124
|
+
with open(path, encoding="utf-8") as f:
|
|
125
|
+
existing = json.load(f)
|
|
126
|
+
if not isinstance(existing, dict):
|
|
127
|
+
existing = {}
|
|
128
|
+
except (json.JSONDecodeError, OSError):
|
|
129
|
+
existing = {}
|
|
130
|
+
|
|
131
|
+
merged = merge_hooks(existing, build_toolkit_hooks())
|
|
132
|
+
|
|
133
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
134
|
+
json.dump(merged, f, indent=4, ensure_ascii=False, sort_keys=True)
|
|
135
|
+
f.write("\n")
|
|
136
|
+
return path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main() -> None:
|
|
140
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
141
|
+
path = generate(target)
|
|
142
|
+
total = sum(len(scripts) for groups in DEVIN_HOOKS.values()
|
|
143
|
+
for _, scripts in groups)
|
|
144
|
+
rel = path.relative_to(target) if path.is_relative_to(target) else path
|
|
145
|
+
print(f"Generated: {rel} ({total} hooks across {len(DEVIN_HOOKS)} events)")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Generate .windsurf/hooks.json for Windsurf Cascade.
|
|
3
3
|
|
|
4
|
+
DEPRECATED SURFACE: the Cascade agent is available only through 2026-07-01
|
|
5
|
+
(Devin Local is the default agent since the 2026-06-02 Devin Desktop rebrand),
|
|
6
|
+
and this hooks.json schema is Cascade-scoped. Migrate to the Devin CLI
|
|
7
|
+
lifecycle-hooks surface (docs.devin.ai/cli/extensibility/hooks/*) before that
|
|
8
|
+
date; tracked in the ecosystem registry status_note for windsurf.
|
|
9
|
+
|
|
4
10
|
Writes `<target>/.windsurf/hooks.json`. Existing user hook entries are
|
|
5
11
|
preserved; only entries tagged `_source: ai-toolkit` are replaced on
|
|
6
12
|
regeneration. Windsurf merges system / user / workspace hooks at runtime, so
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Generate ``.windsurf/rules/*.md`` (and workflows)
|
|
2
|
+
"""Generate ``.devin/rules/*.md`` and ``.windsurf/rules/*.md`` (and workflows)
|
|
3
|
+
for Devin Desktop (formerly Windsurf).
|
|
3
4
|
|
|
4
|
-
Windsurf
|
|
5
|
-
|
|
6
|
-
(
|
|
5
|
+
Windsurf rebranded to Devin Desktop on 2026-06-02. ``.devin/`` is now the
|
|
6
|
+
primary read+write workspace tree; ``.windsurf/`` is a legacy read-only
|
|
7
|
+
fallback that current builds still honor (docs.devin.ai/desktop/devin-desktop-faq:
|
|
8
|
+
"The application already supports .devin/ as the primary workspace directory
|
|
9
|
+
and falls back to .windsurf/ for backward compatibility"). We dual-emit the
|
|
10
|
+
same content to both trees so old Windsurf builds keep working during the
|
|
11
|
+
transition.
|
|
12
|
+
|
|
13
|
+
Each rule file supports YAML frontmatter with these fields
|
|
14
|
+
(from docs.devin.ai/desktop — memories docs):
|
|
7
15
|
|
|
8
16
|
* ``trigger`` — activation mode: ``always_on`` | ``glob`` | ``model_decision``
|
|
9
|
-
| ``manual``. When omitted,
|
|
17
|
+
| ``manual``. When omitted, the IDE defaults to manual-only.
|
|
10
18
|
* ``globs`` — comma-separated glob patterns (only when ``trigger: glob``).
|
|
11
19
|
* ``description`` — shown to the model when ``trigger: model_decision``.
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
Workflow markdown files (invoked via ``/<name>`` slash commands) are emitted
|
|
22
|
+
to ``.devin/workflows/*.md`` and ``.windsurf/workflows/*.md``. This generator
|
|
15
23
|
emits the same workflow catalogue used by Antigravity and Cline.
|
|
16
24
|
|
|
17
25
|
The legacy ``.windsurfrules`` single-file format is still produced by
|
|
@@ -142,20 +150,26 @@ def _build_rules(language_modules: list[str] | None,
|
|
|
142
150
|
|
|
143
151
|
|
|
144
152
|
# ---------------------------------------------------------------------------
|
|
145
|
-
# Workflows —
|
|
153
|
+
# Workflows — <tree>/workflows/<name>.md invocable via /<name>
|
|
146
154
|
# ---------------------------------------------------------------------------
|
|
147
155
|
|
|
156
|
+
# .devin/ is primary since the 2026-06-02 Devin Desktop rebrand; .windsurf/
|
|
157
|
+
# is the legacy fallback still read by pre-rebrand builds.
|
|
158
|
+
CONFIG_TREES: tuple[str, ...] = (".devin", ".windsurf")
|
|
159
|
+
|
|
160
|
+
|
|
148
161
|
def _write_workflows(target_dir: Path, *, cleanup: bool = True) -> None:
|
|
149
|
-
"""Write
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
"""Write ``workflows/*.md`` slash-command files to both config trees."""
|
|
163
|
+
for tree in CONFIG_TREES:
|
|
164
|
+
workflows_dir = target_dir / tree / "workflows"
|
|
165
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
152
166
|
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
if cleanup:
|
|
168
|
+
cleanup_stale(workflows_dir, set(STANDARD_WORKFLOWS.keys()))
|
|
155
169
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
for filename, content_fn in STANDARD_WORKFLOWS.items():
|
|
171
|
+
(workflows_dir / filename).write_text(content_fn(), encoding="utf-8")
|
|
172
|
+
print(f" Generated: {tree}/workflows/{filename}")
|
|
159
173
|
|
|
160
174
|
|
|
161
175
|
# ---------------------------------------------------------------------------
|
|
@@ -170,15 +184,16 @@ def generate(target_dir: Path, *,
|
|
|
170
184
|
managed_scopes: tuple[str, ...] = (
|
|
171
185
|
STANDARD_SCOPE, LANG_SCOPE, CUSTOM_SCOPE,
|
|
172
186
|
)) -> None:
|
|
173
|
-
"""Write
|
|
187
|
+
"""Write ``rules/*.md`` and ``workflows/*.md`` to both config trees."""
|
|
174
188
|
rules = _build_rules(language_modules, rules_dir)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
for tree in CONFIG_TREES:
|
|
190
|
+
write_rules(
|
|
191
|
+
target_dir,
|
|
192
|
+
rules,
|
|
193
|
+
f"{tree}/rules",
|
|
194
|
+
cleanup=cleanup,
|
|
195
|
+
managed_scopes=managed_scopes,
|
|
196
|
+
)
|
|
182
197
|
|
|
183
198
|
if emit_workflows:
|
|
184
199
|
_write_workflows(target_dir, cleanup=cleanup)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Generate a Windsurf skill pointer
|
|
2
|
+
"""Generate a Devin Desktop (formerly Windsurf) skill pointer.
|
|
3
|
+
|
|
4
|
+
Dual-emits under ``.devin/skills/`` (primary since the 2026-06-02 rebrand)
|
|
5
|
+
and ``.windsurf/skills/`` (legacy fallback). Pass an explicit ``skill_root``
|
|
6
|
+
to emit a single location instead (used for the ``~/.codeium/windsurf/``
|
|
7
|
+
global install path).
|
|
8
|
+
"""
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
5
11
|
import sys
|
|
@@ -8,13 +14,17 @@ from pathlib import Path
|
|
|
8
14
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
15
|
from skill_pointer import POINTER_SKILL_NAME, write_pointer_skill
|
|
10
16
|
|
|
17
|
+
DEFAULT_SKILL_ROOTS: tuple[str, ...] = (".devin/skills", ".windsurf/skills")
|
|
18
|
+
|
|
11
19
|
|
|
12
20
|
def generate(target_dir: Path, *, emit_skill_pointer: bool = True,
|
|
13
|
-
skill_root: str =
|
|
21
|
+
skill_root: str | None = None) -> None:
|
|
14
22
|
if not emit_skill_pointer:
|
|
15
23
|
return
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
roots = (skill_root,) if skill_root else DEFAULT_SKILL_ROOTS
|
|
25
|
+
for root in roots:
|
|
26
|
+
write_pointer_skill(target_dir, root, "Windsurf")
|
|
27
|
+
print(f" Generated: {root}/{POINTER_SKILL_NAME}/SKILL.md")
|
|
18
28
|
|
|
19
29
|
|
|
20
30
|
def main() -> None:
|
|
@@ -381,6 +381,9 @@ _EDITOR_MARKERS: dict[str, str] = {
|
|
|
381
381
|
".cursor/rules": "cursor",
|
|
382
382
|
".windsurfrules": "windsurf",
|
|
383
383
|
".windsurf/rules": "windsurf",
|
|
384
|
+
# .devin/ is the primary Devin Desktop tree since the 2026-06-02 rebrand;
|
|
385
|
+
# .windsurf/ markers stay to detect legacy installs.
|
|
386
|
+
".devin/rules": "windsurf",
|
|
384
387
|
".clinerules": "cline",
|
|
385
388
|
".roomodes": "roo",
|
|
386
389
|
".roo/rules": "roo",
|
|
@@ -411,11 +414,25 @@ def _detect_editors(cwd: Path) -> list[str]:
|
|
|
411
414
|
found: set[str] = set()
|
|
412
415
|
for marker, editor in _EDITOR_MARKERS.items():
|
|
413
416
|
p = cwd / marker
|
|
414
|
-
if p.exists():
|
|
415
|
-
|
|
417
|
+
if not p.exists():
|
|
418
|
+
continue
|
|
419
|
+
if marker == ".agents/skills" and _is_pointer_only_skills_dir(p):
|
|
420
|
+
# The Antigravity CLI pointer skill also lives in .agents/skills/;
|
|
421
|
+
# only real (materialized) skills indicate a Codex install.
|
|
422
|
+
continue
|
|
423
|
+
found.add(editor)
|
|
416
424
|
return sorted(found)
|
|
417
425
|
|
|
418
426
|
|
|
427
|
+
def _is_pointer_only_skills_dir(skills_dir: Path) -> bool:
|
|
428
|
+
"""True when a skills dir holds only the ai-toolkit pointer skill."""
|
|
429
|
+
try:
|
|
430
|
+
entries = [e.name for e in skills_dir.iterdir() if not e.name.startswith(".")]
|
|
431
|
+
except OSError:
|
|
432
|
+
return False
|
|
433
|
+
return entries == ["ai-toolkit-skill-catalogue"]
|
|
434
|
+
|
|
435
|
+
|
|
419
436
|
def _resolve_editors(editors_arg: str, cwd: Path) -> list[str]:
|
|
420
437
|
"""Resolve --editors argument to a list of editor names.
|
|
421
438
|
|
|
@@ -709,7 +726,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
709
726
|
_EDITOR_DRY_RUN = {
|
|
710
727
|
"copilot": " Would inject: .github/copilot-instructions.md",
|
|
711
728
|
"cursor": " Would generate: .cursorrules + .cursor/rules/*.mdc",
|
|
712
|
-
"windsurf": " Would generate: .windsurfrules + .windsurf/rules/*.md",
|
|
729
|
+
"windsurf": " Would generate: .windsurfrules + .devin/rules/*.md + .windsurf/rules/*.md",
|
|
713
730
|
"cline": " Would generate: .clinerules/*.md",
|
|
714
731
|
"roo": " Would generate: .roomodes + .roo/rules/*.md",
|
|
715
732
|
"aider": " Would generate: .aider.conf.yml + CONVENTIONS.md",
|
|
@@ -731,7 +748,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
731
748
|
if "cursor" in eds:
|
|
732
749
|
print(" Would generate: .cursor/hooks.json + .cursor/agents/ + .cursor/skills/ (profile=full)")
|
|
733
750
|
if "windsurf" in eds:
|
|
734
|
-
print(" Would generate: .windsurf/hooks.json + .windsurf/skills/ (profile=full)")
|
|
751
|
+
print(" Would generate: .devin/hooks.v1.json + .windsurf/hooks.json (Cascade, deprecated) + .devin/skills/ + .windsurf/skills/ (profile=full)")
|
|
735
752
|
if "cline" in eds:
|
|
736
753
|
print(" Would generate: .cline/skills/ (profile=full)")
|
|
737
754
|
if "augment" in eds:
|
|
@@ -963,7 +980,10 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
963
980
|
gen_windsurf_rules(cwd, language_modules=language_modules,
|
|
964
981
|
rules_dir=rules_dir)
|
|
965
982
|
if add_native_surfaces:
|
|
983
|
+
# .windsurf/hooks.json is Cascade-scoped and dies 2026-07-01;
|
|
984
|
+
# .devin/hooks.v1.json is the Devin CLI replacement (Claude format).
|
|
966
985
|
_try_generator("generate_windsurf_hooks", cwd)
|
|
986
|
+
_try_generator("generate_devin_hooks", cwd)
|
|
967
987
|
# Windsurf pointer stays unconditional (its .claude scan is gated).
|
|
968
988
|
_try_generator("generate_windsurf_skills", cwd)
|
|
969
989
|
|
package/scripts/validate.py
CHANGED
|
@@ -846,11 +846,15 @@ def _validate_version_sync(tk_dir: Path, vr: ValidationResult) -> None:
|
|
|
846
846
|
# generators come and go.
|
|
847
847
|
_NATIVE_HOOK_EDITORS = {"claude", "opencode"}
|
|
848
848
|
|
|
849
|
+
# generate_<stem>_hooks.py stems that belong to an existing README platform key.
|
|
850
|
+
_HOOK_STEM_ALIAS = {"devin": "windsurf"}
|
|
851
|
+
|
|
849
852
|
# README platform label (lowercased) -> canonical editor key.
|
|
850
853
|
_README_PLATFORM_KEY = {
|
|
851
854
|
"claude code": "claude",
|
|
852
855
|
"cursor": "cursor",
|
|
853
856
|
"windsurf": "windsurf",
|
|
857
|
+
"windsurf (devin desktop)": "windsurf",
|
|
854
858
|
"gemini cli": "gemini",
|
|
855
859
|
"github copilot": "copilot",
|
|
856
860
|
"cline": "cline",
|
|
@@ -876,10 +880,12 @@ def _validate_editor_hooks_honesty(tk_dir: Path, vr: ValidationResult) -> None:
|
|
|
876
880
|
return # installed copy without source — nothing to cross-check
|
|
877
881
|
|
|
878
882
|
# Actual hook-enabled editors: native set + generate_<editor>_hooks.py stems.
|
|
883
|
+
# Some stems map back to a README platform key (e.g. the Devin CLI hooks
|
|
884
|
+
# generator is part of the windsurf/Devin-Desktop integration).
|
|
879
885
|
actual = set(_NATIVE_HOOK_EDITORS)
|
|
880
886
|
for gen in scripts_dir.glob("generate_*_hooks.py"):
|
|
881
887
|
stem = gen.name[len("generate_"):-len("_hooks.py")]
|
|
882
|
-
actual.add(stem)
|
|
888
|
+
actual.add(_HOOK_STEM_ALIAS.get(stem, stem))
|
|
883
889
|
|
|
884
890
|
content = readme.read_text(encoding="utf-8")
|
|
885
891
|
if "| Hooks |" not in content:
|