@event4u/agent-config 1.18.0 → 1.19.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/.agent-src/commands/council/default.md +74 -76
- package/.agent-src/commands/feature/roadmap.md +22 -0
- package/.agent-src/commands/roadmap/create.md +38 -6
- package/.agent-src/commands/roadmap/execute.md +36 -9
- package/.agent-src/rules/agent-authority.md +1 -0
- package/.agent-src/rules/agent-docs.md +1 -0
- package/.agent-src/rules/analysis-skill-routing.md +1 -0
- package/.agent-src/rules/architecture.md +1 -0
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
- package/.agent-src/rules/artifact-engagement-recording.md +1 -0
- package/.agent-src/rules/ask-when-uncertain.md +1 -0
- package/.agent-src/rules/augment-portability.md +1 -0
- package/.agent-src/rules/augment-source-of-truth.md +1 -0
- package/.agent-src/rules/autonomous-execution.md +1 -0
- package/.agent-src/rules/capture-learnings.md +1 -0
- package/.agent-src/rules/chat-history-cadence.md +34 -0
- package/.agent-src/rules/chat-history-ownership.md +1 -0
- package/.agent-src/rules/chat-history-visibility.md +1 -0
- package/.agent-src/rules/cli-output-handling.md +2 -2
- package/.agent-src/rules/command-suggestion-policy.md +1 -0
- package/.agent-src/rules/commit-conventions.md +1 -0
- package/.agent-src/rules/commit-policy.md +1 -0
- package/.agent-src/rules/context-hygiene.md +22 -0
- package/.agent-src/rules/direct-answers.md +1 -0
- package/.agent-src/rules/docker-commands.md +1 -0
- package/.agent-src/rules/docs-sync.md +1 -0
- package/.agent-src/rules/downstream-changes.md +1 -0
- package/.agent-src/rules/e2e-testing.md +1 -0
- package/.agent-src/rules/guidelines.md +1 -0
- package/.agent-src/rules/improve-before-implement.md +1 -0
- package/.agent-src/rules/language-and-tone.md +1 -0
- package/.agent-src/rules/laravel-translations.md +1 -0
- package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/.agent-src/rules/minimal-safe-diff.md +1 -0
- package/.agent-src/rules/missing-tool-handling.md +1 -0
- package/.agent-src/rules/model-recommendation.md +1 -0
- package/.agent-src/rules/no-cheap-questions.md +1 -0
- package/.agent-src/rules/no-roadmap-references.md +1 -0
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +26 -0
- package/.agent-src/rules/package-ci-checks.md +1 -0
- package/.agent-src/rules/php-coding.md +1 -0
- package/.agent-src/rules/preservation-guard.md +1 -0
- package/.agent-src/rules/review-routing-awareness.md +1 -0
- package/.agent-src/rules/reviewer-awareness.md +1 -0
- package/.agent-src/rules/roadmap-progress-sync.md +22 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +1 -0
- package/.agent-src/rules/runtime-safety.md +1 -0
- package/.agent-src/rules/scope-control.md +1 -0
- package/.agent-src/rules/security-sensitive-stop.md +1 -0
- package/.agent-src/rules/size-enforcement.md +1 -0
- package/.agent-src/rules/skill-improvement-trigger.md +1 -0
- package/.agent-src/rules/skill-quality.md +1 -0
- package/.agent-src/rules/slash-command-routing-policy.md +39 -0
- package/.agent-src/rules/think-before-action.md +1 -0
- package/.agent-src/rules/token-efficiency.md +1 -0
- package/.agent-src/rules/tool-safety.md +1 -0
- package/.agent-src/rules/ui-audit-gate.md +1 -0
- package/.agent-src/rules/upstream-proposal.md +1 -0
- package/.agent-src/rules/user-interaction.md +1 -0
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +16 -0
- package/.agent-src/templates/roadmaps.md +8 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +9 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +111 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
- package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +62 -0
- package/README.md +19 -19
- package/config/agent-settings.template.yml +23 -0
- package/docs/catalog.md +5 -2
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +7 -0
- package/docs/contracts/hook-architecture-v1.md +213 -0
- package/docs/contracts/memory-visibility-v1.md +138 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +1 -0
- package/docs/development.md +4 -1
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/package.json +1 -1
- package/scripts/agent-config +44 -0
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +24 -8
- package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
- package/scripts/ai_council/session.py +92 -0
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +11 -1
- package/scripts/check_always_budget.py +7 -2
- package/scripts/context_hygiene_hook.py +14 -6
- package/scripts/council_cli.py +357 -0
- package/scripts/hook_manifest.yaml +184 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +348 -0
- package/scripts/hooks/envelope.py +98 -0
- package/scripts/hooks/gemini-dispatcher.sh +117 -0
- package/scripts/hooks/state_io.py +122 -0
- package/scripts/hooks/windsurf-dispatcher.sh +123 -0
- package/scripts/hooks_status.py +146 -0
- package/scripts/install.py +725 -87
- package/scripts/install.sh +1 -1
- package/scripts/lint_hook_manifest.py +216 -0
- package/scripts/lint_one_off_age.py +184 -0
- package/scripts/lint_rule_tiers.py +78 -0
- package/scripts/lint_showcase_sessions.py +148 -0
- package/scripts/minimal_safe_diff_hook.py +245 -0
- package/scripts/onboarding_gate_hook.py +13 -8
- package/scripts/readme_linter.py +12 -3
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/verify_before_complete_hook.py +216 -0
package/scripts/install.py
CHANGED
|
@@ -29,6 +29,8 @@ import copy
|
|
|
29
29
|
import json
|
|
30
30
|
import os
|
|
31
31
|
import re
|
|
32
|
+
import shlex
|
|
33
|
+
import subprocess
|
|
32
34
|
import sys
|
|
33
35
|
from pathlib import Path
|
|
34
36
|
|
|
@@ -464,23 +466,31 @@ def ensure_augment_bridge(project_root: Path, force: bool) -> None:
|
|
|
464
466
|
# .augment/settings.json is plugin enablement, not hooks.
|
|
465
467
|
AUGMENT_USER_DIR = Path.home() / ".augment"
|
|
466
468
|
AUGMENT_USER_HOOKS_DIR = AUGMENT_USER_DIR / "hooks"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
469
|
+
|
|
470
|
+
# Phase 7.3 (hook-architecture-v1.md): one universal trampoline per
|
|
471
|
+
# platform replaces the per-concern fan-out. The trampoline cd's into
|
|
472
|
+
# the consumer workspace and pipes stdin into
|
|
473
|
+
# `./agent-config dispatch:hook`, which reads scripts/hook_manifest.yaml
|
|
474
|
+
# to resolve which concerns fire on (platform, event).
|
|
475
|
+
AUGMENT_DISPATCHER_TRAMPOLINE = "augment-dispatcher.sh"
|
|
476
|
+
|
|
477
|
+
# Pre-Phase-7 trampolines deployed at ~/.augment/hooks/ — install removes
|
|
478
|
+
# them on rerun so the manifest stays the single source of truth.
|
|
479
|
+
AUGMENT_LEGACY_TRAMPOLINES = (
|
|
480
|
+
"augment-chat-history.sh",
|
|
481
|
+
"augment-roadmap-progress.sh",
|
|
482
|
+
"augment-onboarding-gate.sh",
|
|
483
|
+
"augment-context-hygiene.sh",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# (agent-config event, Augment native event). Augment fires the same
|
|
487
|
+
# trampoline once per binding; the trampoline forwards both names to the
|
|
488
|
+
# dispatcher so concerns can branch on either.
|
|
489
|
+
AUGMENT_DISPATCHER_BINDINGS = (
|
|
490
|
+
("session_start", "SessionStart"),
|
|
491
|
+
("session_end", "SessionEnd"),
|
|
492
|
+
("stop", "Stop"),
|
|
493
|
+
("post_tool_use", "PostToolUse"),
|
|
484
494
|
)
|
|
485
495
|
|
|
486
496
|
|
|
@@ -501,38 +511,60 @@ def _deploy_augment_trampoline(package_root: Path, name: str, force: bool) -> Pa
|
|
|
501
511
|
return dst
|
|
502
512
|
|
|
503
513
|
|
|
504
|
-
def
|
|
505
|
-
"""
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
time.
|
|
513
|
-
|
|
514
|
-
Trampolines deployed (see AUGMENT_HOOK_BINDINGS for the source of
|
|
515
|
-
truth):
|
|
516
|
-
- augment-chat-history.sh → SessionStart/SessionEnd/Stop/PostToolUse
|
|
517
|
-
- augment-roadmap-progress.sh → PostToolUse (path-filtered to
|
|
518
|
-
agents/roadmaps/ — see scripts/roadmap_progress_hook.py)
|
|
519
|
-
- augment-onboarding-gate.sh → SessionStart (refresh
|
|
520
|
-
agents/state/onboarding-gate.json from .agent-settings.yml)
|
|
521
|
-
- augment-context-hygiene.sh → PostToolUse (per-turn counter,
|
|
522
|
-
loop detection, freshness milestones)
|
|
514
|
+
def _remove_legacy_augment_trampolines() -> None:
|
|
515
|
+
"""Phase 7.3 cleanup: drop pre-dispatcher trampolines on rerun.
|
|
516
|
+
|
|
517
|
+
The manifest is now the single source of truth; leaving the old
|
|
518
|
+
per-concern .sh files at ~/.augment/hooks/ would not break anything
|
|
519
|
+
(settings.json no longer references them), but it produces stale
|
|
520
|
+
artefacts that confuse `task hooks-status` and look like a partial
|
|
521
|
+
install. Removal is best-effort and silent on missing files.
|
|
523
522
|
"""
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
523
|
+
for name in AUGMENT_LEGACY_TRAMPOLINES:
|
|
524
|
+
legacy = AUGMENT_USER_HOOKS_DIR / name
|
|
525
|
+
try:
|
|
526
|
+
if legacy.is_file():
|
|
527
|
+
legacy.unlink()
|
|
528
|
+
skip(f"removed legacy ~/.augment/hooks/{name}")
|
|
529
|
+
except OSError:
|
|
530
|
+
pass
|
|
531
|
+
|
|
532
532
|
|
|
533
|
-
|
|
533
|
+
def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
|
|
534
|
+
"""Deploy the Augment universal-dispatcher trampoline at user scope.
|
|
535
|
+
|
|
536
|
+
Phase 7.3 (hook-architecture-v1.md): one trampoline replaces the
|
|
537
|
+
four per-concern .sh files. The trampoline reads the event JSON
|
|
538
|
+
from stdin, extracts workspace_roots[0], cd's there, and pipes the
|
|
539
|
+
payload into `./agent-config dispatch:hook --platform augment
|
|
540
|
+
--event <agent-config-event> --native-event <native>`. The
|
|
541
|
+
dispatcher then loads scripts/hook_manifest.yaml and runs the
|
|
542
|
+
resolved concern chain.
|
|
543
|
+
|
|
544
|
+
Augment hook scripts must use the .sh extension and be referenced
|
|
545
|
+
by absolute path; user scope is the only surface that fires for
|
|
546
|
+
both the CLI and the IDE plugins. Installs once per developer.
|
|
547
|
+
|
|
548
|
+
Settings entries (Phase 7.3, see AUGMENT_DISPATCHER_BINDINGS):
|
|
549
|
+
- SessionStart → augment-dispatcher.sh session_start SessionStart
|
|
550
|
+
- SessionEnd → augment-dispatcher.sh session_end SessionEnd
|
|
551
|
+
- Stop → augment-dispatcher.sh stop Stop
|
|
552
|
+
- PostToolUse → augment-dispatcher.sh post_tool_use PostToolUse
|
|
553
|
+
"""
|
|
554
|
+
dst = _deploy_augment_trampoline(package_root, AUGMENT_DISPATCHER_TRAMPOLINE, force)
|
|
555
|
+
if dst is None:
|
|
534
556
|
return
|
|
535
557
|
|
|
558
|
+
_remove_legacy_augment_trampolines()
|
|
559
|
+
|
|
560
|
+
per_event: dict[str, list] = {}
|
|
561
|
+
for ac_event, native in AUGMENT_DISPATCHER_BINDINGS:
|
|
562
|
+
# Augment's `command` is a shell line — pass agent-config event
|
|
563
|
+
# and Augment-native event as positional args.
|
|
564
|
+
cmd = f"{dst} {ac_event} {native}"
|
|
565
|
+
entry = {"hooks": [{"type": "command", "command": cmd}]}
|
|
566
|
+
per_event.setdefault(native, []).append(entry)
|
|
567
|
+
|
|
536
568
|
settings_patch: dict = {"hooks": per_event}
|
|
537
569
|
merge_json_file(
|
|
538
570
|
AUGMENT_USER_DIR / "settings.json",
|
|
@@ -542,60 +574,56 @@ def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
|
|
|
542
574
|
)
|
|
543
575
|
|
|
544
576
|
|
|
545
|
-
|
|
546
|
-
|
|
577
|
+
# Claude Code lifecycle events → agent-config event vocabulary.
|
|
578
|
+
# Phase 7.3: one universal dispatch:hook entry per event replaces the
|
|
579
|
+
# per-concern subcommand fan-out. The dispatcher reads
|
|
580
|
+
# scripts/hook_manifest.yaml to resolve which concerns fire on each
|
|
581
|
+
# (platform, event) tuple. Mirrors AUGMENT_DISPATCHER_BINDINGS so each
|
|
582
|
+
# concern fires on the same logical surface across platforms — the
|
|
583
|
+
# contract from agents/contexts/hardening-pattern.md § Cross-platform
|
|
584
|
+
# parity.
|
|
585
|
+
CLAUDE_DISPATCHER_BINDINGS = (
|
|
586
|
+
("session_start", "SessionStart"),
|
|
587
|
+
("session_end", "SessionEnd"),
|
|
588
|
+
("stop", "Stop"),
|
|
589
|
+
("user_prompt_submit", "UserPromptSubmit"),
|
|
590
|
+
("post_tool_use", "PostToolUse"),
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _claude_dispatch_block(ac_event: str, native: str) -> dict:
|
|
595
|
+
"""Single hook entry routing the event through the universal dispatcher."""
|
|
547
596
|
return {
|
|
548
597
|
"hooks": [
|
|
549
598
|
{
|
|
550
599
|
"type": "command",
|
|
551
|
-
"command":
|
|
600
|
+
"command": (
|
|
601
|
+
f"./agent-config dispatch:hook "
|
|
602
|
+
f"--platform claude --event {ac_event} "
|
|
603
|
+
f"--native-event {native}"
|
|
604
|
+
),
|
|
552
605
|
},
|
|
553
606
|
],
|
|
554
607
|
}
|
|
555
608
|
|
|
556
609
|
|
|
557
|
-
# Claude Code Tier 1 hook bindings — keep in sync with AUGMENT_HOOK_BINDINGS.
|
|
558
|
-
# `chat-history:hook` is the cross-cutting transcript hook; the three
|
|
559
|
-
# rule-specific hooks are the Phase 4 Tier 1 set from
|
|
560
|
-
# `road-to-rule-hardening.md`.
|
|
561
|
-
CLAUDE_HOOK_SUBCOMMANDS = {
|
|
562
|
-
"chat-history": "chat-history:hook",
|
|
563
|
-
"roadmap-progress": "roadmap-progress:hook",
|
|
564
|
-
"onboarding-gate": "onboarding-gate:hook",
|
|
565
|
-
"context-hygiene": "context-hygiene:hook",
|
|
566
|
-
}
|
|
567
|
-
# (subcommand-key, list of Claude Code lifecycle events). Mirrors
|
|
568
|
-
# AUGMENT_HOOK_BINDINGS so each rule fires on the same logical surface
|
|
569
|
-
# on both platforms — the contract from
|
|
570
|
-
# `agents/contexts/hardening-pattern.md` § Cross-platform parity.
|
|
571
|
-
CLAUDE_HOOK_BINDINGS = (
|
|
572
|
-
("chat-history",
|
|
573
|
-
("SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd")),
|
|
574
|
-
("roadmap-progress",
|
|
575
|
-
("PostToolUse",)),
|
|
576
|
-
("onboarding-gate",
|
|
577
|
-
("SessionStart",)),
|
|
578
|
-
("context-hygiene",
|
|
579
|
-
("PostToolUse",)),
|
|
580
|
-
)
|
|
581
|
-
|
|
582
|
-
|
|
583
610
|
def ensure_claude_bridge(project_root: Path, force: bool) -> None:
|
|
584
|
-
"""Deploy .claude/settings.json with plugin enablement and
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
than
|
|
611
|
+
"""Deploy .claude/settings.json with plugin enablement and the Phase 7
|
|
612
|
+
universal dispatcher hooks.
|
|
613
|
+
|
|
614
|
+
Each Claude Code lifecycle event is wired to a single
|
|
615
|
+
`./agent-config dispatch:hook` invocation. The dispatcher reads
|
|
616
|
+
scripts/hook_manifest.yaml at runtime and runs the resolved concern
|
|
617
|
+
chain — concerns are no-ops when the relevant feature is disabled
|
|
618
|
+
in .agent-settings.yml. Idempotent: reruns merge cleanly without
|
|
619
|
+
duplicating entries (deep_merge replaces hook arrays rather than
|
|
620
|
+
appending).
|
|
593
621
|
"""
|
|
594
622
|
per_event: dict[str, list] = {}
|
|
595
|
-
for
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
623
|
+
for ac_event, native in CLAUDE_DISPATCHER_BINDINGS:
|
|
624
|
+
per_event.setdefault(native, []).append(
|
|
625
|
+
_claude_dispatch_block(ac_event, native)
|
|
626
|
+
)
|
|
599
627
|
|
|
600
628
|
bridge = {
|
|
601
629
|
"enabledPlugins": {"agent-conf@event4u": True},
|
|
@@ -604,6 +632,467 @@ def ensure_claude_bridge(project_root: Path, force: bool) -> None:
|
|
|
604
632
|
merge_json_file(project_root / ".claude" / "settings.json", bridge, force, ".claude/settings.json")
|
|
605
633
|
|
|
606
634
|
|
|
635
|
+
# Cursor lifecycle events → agent-config event vocabulary.
|
|
636
|
+
# Phase 7.5 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
637
|
+
# Cursor's project-scope `.cursor/hooks.json` fires hooks with the
|
|
638
|
+
# project as cwd, so the dispatch:hook command runs directly with no
|
|
639
|
+
# trampoline. User-scope `~/.cursor/hooks.json` is a separate opt-in
|
|
640
|
+
# (--cursor-user-hooks) and routes through cursor-dispatcher.sh because
|
|
641
|
+
# the user-scope hooks fire across all projects.
|
|
642
|
+
#
|
|
643
|
+
# Native event names per https://cursor.com/docs/reference/third-party-hooks
|
|
644
|
+
# (camelCase). UserPromptSubmit lives at `beforeSubmitPrompt`. Stop is
|
|
645
|
+
# IDE-only — CLI-only Cursor users get the rule-only checkpoint
|
|
646
|
+
# fallback per agents/contexts/chat-history-platform-hooks.md.
|
|
647
|
+
CURSOR_DISPATCHER_BINDINGS = (
|
|
648
|
+
("session_start", "sessionStart"),
|
|
649
|
+
("session_end", "sessionEnd"),
|
|
650
|
+
("stop", "stop"),
|
|
651
|
+
("user_prompt_submit", "beforeSubmitPrompt"),
|
|
652
|
+
("post_tool_use", "postToolUse"),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _cursor_dispatch_command(ac_event: str, native: str) -> str:
|
|
657
|
+
return (
|
|
658
|
+
f"./agent-config dispatch:hook "
|
|
659
|
+
f"--platform cursor --event {ac_event} "
|
|
660
|
+
f"--native-event {native}"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def ensure_cursor_bridge(project_root: Path, force: bool) -> None:
|
|
665
|
+
"""Deploy `.cursor/hooks.json` (project scope) with the Phase 7
|
|
666
|
+
universal dispatcher hooks.
|
|
667
|
+
|
|
668
|
+
Each Cursor lifecycle event is wired to a single
|
|
669
|
+
`./agent-config dispatch:hook` invocation. Cursor fires project
|
|
670
|
+
hooks with the project as cwd, so no trampoline is needed at this
|
|
671
|
+
scope — concerns are no-ops when disabled in .agent-settings.yml.
|
|
672
|
+
Idempotent: deep_merge replaces hook arrays on rerun rather than
|
|
673
|
+
appending duplicates.
|
|
674
|
+
"""
|
|
675
|
+
hooks: dict[str, list] = {}
|
|
676
|
+
for ac_event, native in CURSOR_DISPATCHER_BINDINGS:
|
|
677
|
+
hooks.setdefault(native, []).append(
|
|
678
|
+
{"command": _cursor_dispatch_command(ac_event, native)}
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
bridge = {"version": 1, "hooks": hooks}
|
|
682
|
+
merge_json_file(project_root / ".cursor" / "hooks.json", bridge, force, ".cursor/hooks.json")
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# Cursor user-scope hooks fire across every project the developer opens
|
|
686
|
+
# in the Cursor IDE / CLI. The trampoline reads `workspace_roots[0]`
|
|
687
|
+
# from the event payload (per https://cursor.com/docs/hooks) and routes
|
|
688
|
+
# the JSON into the active project's `./agent-config dispatch:hook`,
|
|
689
|
+
# silent no-op when the workspace is not an agent-config consumer.
|
|
690
|
+
CURSOR_USER_DIR = Path.home() / ".cursor"
|
|
691
|
+
CURSOR_USER_HOOKS_DIR = CURSOR_USER_DIR / "hooks"
|
|
692
|
+
CURSOR_DISPATCHER_TRAMPOLINE = "cursor-dispatcher.sh"
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def ensure_cursor_user_hooks(package_root: Path, force: bool) -> None:
|
|
696
|
+
"""Deploy the Cursor universal-dispatcher trampoline at user scope.
|
|
697
|
+
|
|
698
|
+
Phase 7.5 (hook-architecture-v1.md): mirrors ensure_augment_user_hooks
|
|
699
|
+
for the Cursor surface. Writes:
|
|
700
|
+
- ~/.cursor/hooks/cursor-dispatcher.sh (trampoline)
|
|
701
|
+
- ~/.cursor/hooks.json (event → trampoline call)
|
|
702
|
+
|
|
703
|
+
Each hooks.json command line is `<dispatcher> <ac_event> <native>`
|
|
704
|
+
so the trampoline can forward both names to the dispatcher for
|
|
705
|
+
traceability. Hooks fire across all projects the developer opens.
|
|
706
|
+
"""
|
|
707
|
+
src = package_root / "scripts" / "hooks" / CURSOR_DISPATCHER_TRAMPOLINE
|
|
708
|
+
if not src.exists():
|
|
709
|
+
skip(f"cursor trampoline missing in package: {src}")
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
CURSOR_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
713
|
+
dst = CURSOR_USER_HOOKS_DIR / CURSOR_DISPATCHER_TRAMPOLINE
|
|
714
|
+
src_text = src.read_text(encoding="utf-8")
|
|
715
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
716
|
+
skip(f"~/.cursor/hooks/{CURSOR_DISPATCHER_TRAMPOLINE} already up to date")
|
|
717
|
+
else:
|
|
718
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
719
|
+
dst.chmod(0o755)
|
|
720
|
+
success(f"~/.cursor/hooks/{CURSOR_DISPATCHER_TRAMPOLINE} installed")
|
|
721
|
+
|
|
722
|
+
hooks: dict[str, list] = {}
|
|
723
|
+
for ac_event, native in CURSOR_DISPATCHER_BINDINGS:
|
|
724
|
+
hooks.setdefault(native, []).append(
|
|
725
|
+
{"command": f"{dst} {ac_event} {native}"}
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
settings_patch: dict = {"version": 1, "hooks": hooks}
|
|
729
|
+
merge_json_file(
|
|
730
|
+
CURSOR_USER_DIR / "hooks.json",
|
|
731
|
+
settings_patch,
|
|
732
|
+
force,
|
|
733
|
+
"~/.cursor/hooks.json",
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# Cline lifecycle events → agent-config event vocabulary.
|
|
738
|
+
# Phase 7.6 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
739
|
+
# Cline reads scripts at `.clinerules/hooks/<HookName>` (project) or
|
|
740
|
+
# `~/Documents/Cline/Hooks/<HookName>` (global) — file names match
|
|
741
|
+
# the hook type exactly, no extension, executable bit required.
|
|
742
|
+
# Both TaskStart (new) and TaskResume (resumed) map to session_start;
|
|
743
|
+
# TaskCancel maps to stop because the session is interrupted with
|
|
744
|
+
# partial state (mirrors Augment Stop semantics).
|
|
745
|
+
CLINE_DISPATCHER_BINDINGS = (
|
|
746
|
+
("session_start", "TaskStart"),
|
|
747
|
+
("session_start", "TaskResume"),
|
|
748
|
+
("session_end", "TaskComplete"),
|
|
749
|
+
("stop", "TaskCancel"),
|
|
750
|
+
("user_prompt_submit", "UserPromptSubmit"),
|
|
751
|
+
("post_tool_use", "PostToolUse"),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# Each project-scope script is generated from this template — one file
|
|
755
|
+
# per native hook name. The script reads stdin (Cline's payload), forwards
|
|
756
|
+
# it into `./agent-config dispatch:hook`, then emits the empty JSON
|
|
757
|
+
# envelope Cline expects (`{}` = no cancel, no context modification).
|
|
758
|
+
# `cd "$WORKSPACE_ROOT"` is intentional even though Cline fires project
|
|
759
|
+
# hooks with cwd already set: the workspace path lands in $WORKSPACE_ROOT
|
|
760
|
+
# at install time and the cd guards against future Cline behaviour
|
|
761
|
+
# changes (cline#8073-class shifts in cwd handling).
|
|
762
|
+
CLINE_PROJECT_HOOK_TEMPLATE = """\
|
|
763
|
+
#!/usr/bin/env bash
|
|
764
|
+
# Generated by event4u/agent-config install.py — DO NOT EDIT.
|
|
765
|
+
# Project-scope Cline hook for {native_event} → agent-config {ac_event}.
|
|
766
|
+
# Phase 7.6 (docs/contracts/hook-architecture-v1.md).
|
|
767
|
+
set -u
|
|
768
|
+
EVENT_DATA="$(cat)"
|
|
769
|
+
WORKSPACE_ROOT={workspace_quoted}
|
|
770
|
+
cd "$WORKSPACE_ROOT" 2>/dev/null || {{ printf '%s\\n' '{{}}'; exit 0; }}
|
|
771
|
+
if [ ! -x ./agent-config ]; then
|
|
772
|
+
printf '%s\\n' '{{}}'
|
|
773
|
+
exit 0
|
|
774
|
+
fi
|
|
775
|
+
printf '%s' "$EVENT_DATA" \\
|
|
776
|
+
| ./agent-config dispatch:hook \\
|
|
777
|
+
--platform cline \\
|
|
778
|
+
--event {ac_event} \\
|
|
779
|
+
--native-event {native_event} \\
|
|
780
|
+
>/dev/null 2>&1 || true
|
|
781
|
+
printf '%s\\n' '{{}}'
|
|
782
|
+
exit 0
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def ensure_cline_bridge(project_root: Path, force: bool) -> None:
|
|
787
|
+
"""Deploy `.clinerules/hooks/<HookName>` per-event scripts.
|
|
788
|
+
|
|
789
|
+
Phase 7.6: Cline project hooks are individual executable scripts
|
|
790
|
+
named exactly after the hook (no extension). install writes one
|
|
791
|
+
script per (ac_event, native_event) tuple in
|
|
792
|
+
CLINE_DISPATCHER_BINDINGS; rerunning is idempotent — the script
|
|
793
|
+
body is overwritten only when content differs (or --force).
|
|
794
|
+
"""
|
|
795
|
+
hooks_dir = project_root / ".clinerules" / "hooks"
|
|
796
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
797
|
+
|
|
798
|
+
workspace_quoted = shlex.quote(str(project_root.resolve()))
|
|
799
|
+
written = 0
|
|
800
|
+
for ac_event, native_event in CLINE_DISPATCHER_BINDINGS:
|
|
801
|
+
target = hooks_dir / native_event
|
|
802
|
+
body = CLINE_PROJECT_HOOK_TEMPLATE.format(
|
|
803
|
+
native_event=native_event,
|
|
804
|
+
ac_event=ac_event,
|
|
805
|
+
workspace_quoted=workspace_quoted,
|
|
806
|
+
)
|
|
807
|
+
if target.exists() and target.read_text(encoding="utf-8") == body and not force:
|
|
808
|
+
continue
|
|
809
|
+
if target.exists() and not force:
|
|
810
|
+
skip(f".clinerules/hooks/{native_event} exists, needs update (use --force)")
|
|
811
|
+
continue
|
|
812
|
+
target.write_text(body, encoding="utf-8")
|
|
813
|
+
target.chmod(0o755)
|
|
814
|
+
written += 1
|
|
815
|
+
if written:
|
|
816
|
+
success(f".clinerules/hooks/ — {written} script(s) installed")
|
|
817
|
+
else:
|
|
818
|
+
skip(".clinerules/hooks/ already up to date")
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
# Cline user-scope hooks live at `~/Documents/Cline/Hooks/<HookName>`
|
|
822
|
+
# (per docs.cline.bot/customization/hooks) and fire across every
|
|
823
|
+
# project the developer opens. The trampoline reads `workspaceRoots[0]`
|
|
824
|
+
# from the event payload and routes the JSON into the active project's
|
|
825
|
+
# `./agent-config dispatch:hook`. Silent no-op when the workspace is
|
|
826
|
+
# not an agent-config consumer.
|
|
827
|
+
CLINE_USER_DIR = Path.home() / "Documents" / "Cline" / "Hooks"
|
|
828
|
+
CLINE_DISPATCHER_TRAMPOLINE = "cline-dispatcher.sh"
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def ensure_cline_user_hooks(package_root: Path, force: bool) -> None:
|
|
832
|
+
"""Deploy the Cline universal-dispatcher trampoline at user scope.
|
|
833
|
+
|
|
834
|
+
Phase 7.6 (hook-architecture-v1.md): mirrors ensure_cursor_user_hooks
|
|
835
|
+
for Cline. Writes:
|
|
836
|
+
- ~/Documents/Cline/Hooks/cline-dispatcher.sh (shared trampoline)
|
|
837
|
+
- ~/Documents/Cline/Hooks/<HookName> (per-event wrapper)
|
|
838
|
+
|
|
839
|
+
Each per-event wrapper is a tiny shim that exec's the trampoline
|
|
840
|
+
with `<ac_event> <native_event>` arguments and re-pipes stdin —
|
|
841
|
+
this matches Cline's "file name == hook name, no extension"
|
|
842
|
+
convention while still routing through one shared dispatcher.
|
|
843
|
+
"""
|
|
844
|
+
src = package_root / "scripts" / "hooks" / CLINE_DISPATCHER_TRAMPOLINE
|
|
845
|
+
if not src.exists():
|
|
846
|
+
skip(f"cline trampoline missing in package: {src}")
|
|
847
|
+
return
|
|
848
|
+
|
|
849
|
+
CLINE_USER_DIR.mkdir(parents=True, exist_ok=True)
|
|
850
|
+
trampoline = CLINE_USER_DIR / CLINE_DISPATCHER_TRAMPOLINE
|
|
851
|
+
src_text = src.read_text(encoding="utf-8")
|
|
852
|
+
if trampoline.exists() and trampoline.read_text(encoding="utf-8") == src_text and not force:
|
|
853
|
+
skip(f"~/Documents/Cline/Hooks/{CLINE_DISPATCHER_TRAMPOLINE} already up to date")
|
|
854
|
+
else:
|
|
855
|
+
trampoline.write_text(src_text, encoding="utf-8")
|
|
856
|
+
trampoline.chmod(0o755)
|
|
857
|
+
success(f"~/Documents/Cline/Hooks/{CLINE_DISPATCHER_TRAMPOLINE} installed")
|
|
858
|
+
|
|
859
|
+
trampoline_quoted = shlex.quote(str(trampoline))
|
|
860
|
+
for ac_event, native_event in CLINE_DISPATCHER_BINDINGS:
|
|
861
|
+
wrapper = CLINE_USER_DIR / native_event
|
|
862
|
+
body = (
|
|
863
|
+
"#!/usr/bin/env bash\n"
|
|
864
|
+
"# Generated by event4u/agent-config install.py — DO NOT EDIT.\n"
|
|
865
|
+
f"# User-scope Cline hook for {native_event} → agent-config {ac_event}.\n"
|
|
866
|
+
f"exec {trampoline_quoted} {ac_event} {native_event}\n"
|
|
867
|
+
)
|
|
868
|
+
if wrapper.exists() and wrapper.read_text(encoding="utf-8") == body and not force:
|
|
869
|
+
continue
|
|
870
|
+
wrapper.write_text(body, encoding="utf-8")
|
|
871
|
+
wrapper.chmod(0o755)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
# Windsurf (Cascade) lifecycle events → agent-config event vocabulary.
|
|
875
|
+
# Phase 7.7 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
876
|
+
# Windsurf reads `.windsurf/hooks.json` (project) or
|
|
877
|
+
# `~/.codeium/windsurf/hooks.json` (user). Cascade has no generic
|
|
878
|
+
# post-tool-use surface — concerns gated to that slot don't fire on
|
|
879
|
+
# Windsurf (documented platform limitation in chat-history-platform-hooks.md).
|
|
880
|
+
WINDSURF_DISPATCHER_BINDINGS = (
|
|
881
|
+
("session_start", "post_setup_worktree"),
|
|
882
|
+
("user_prompt_submit", "pre_user_prompt"),
|
|
883
|
+
("stop", "post_cascade_response"),
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _windsurf_dispatch_command(ac_event: str, native: str) -> str:
|
|
888
|
+
return (
|
|
889
|
+
f"./agent-config dispatch:hook "
|
|
890
|
+
f"--platform windsurf --event {ac_event} "
|
|
891
|
+
f"--native-event {native}"
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def ensure_windsurf_bridge(project_root: Path, force: bool) -> None:
|
|
896
|
+
"""Deploy `.windsurf/hooks.json` (project scope) with the Phase 7
|
|
897
|
+
universal dispatcher hooks.
|
|
898
|
+
|
|
899
|
+
Each Windsurf lifecycle event is wired to a single
|
|
900
|
+
`./agent-config dispatch:hook` invocation. Cascade fires project
|
|
901
|
+
hooks with the workspace as cwd, so no trampoline is needed at this
|
|
902
|
+
scope. Idempotent via deep_merge — rerunning replaces hook arrays
|
|
903
|
+
rather than appending duplicates. `show_output: false` keeps post
|
|
904
|
+
hooks silent (per Windsurf docs); concerns stream their own output
|
|
905
|
+
via agents/state/.dispatcher/.
|
|
906
|
+
"""
|
|
907
|
+
hooks: dict[str, list] = {}
|
|
908
|
+
for ac_event, native in WINDSURF_DISPATCHER_BINDINGS:
|
|
909
|
+
hooks.setdefault(native, []).append({
|
|
910
|
+
"command": _windsurf_dispatch_command(ac_event, native),
|
|
911
|
+
"show_output": False,
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
bridge = {"hooks": hooks}
|
|
915
|
+
merge_json_file(
|
|
916
|
+
project_root / ".windsurf" / "hooks.json",
|
|
917
|
+
bridge,
|
|
918
|
+
force,
|
|
919
|
+
".windsurf/hooks.json",
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# Windsurf user-scope hooks live at `~/.codeium/windsurf/hooks.json`
|
|
924
|
+
# (per docs.windsurf.com/windsurf/cascade/hooks). The trampoline
|
|
925
|
+
# resolves the active workspace from $PWD / .agent-settings.yml /
|
|
926
|
+
# tool_info.cwd|file_path / $ROOT_WORKSPACE_PATH and routes the JSON
|
|
927
|
+
# into that project's `./agent-config dispatch:hook`. Silent no-op
|
|
928
|
+
# when the workspace is not an agent-config consumer.
|
|
929
|
+
WINDSURF_USER_DIR = Path.home() / ".codeium" / "windsurf"
|
|
930
|
+
WINDSURF_USER_HOOKS_DIR = WINDSURF_USER_DIR / "hooks"
|
|
931
|
+
WINDSURF_DISPATCHER_TRAMPOLINE = "windsurf-dispatcher.sh"
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def ensure_windsurf_user_hooks(package_root: Path, force: bool) -> None:
|
|
935
|
+
"""Deploy the Windsurf universal-dispatcher trampoline at user scope.
|
|
936
|
+
|
|
937
|
+
Phase 7.7 (hook-architecture-v1.md): mirrors ensure_cursor_user_hooks
|
|
938
|
+
for the Windsurf surface. Writes:
|
|
939
|
+
- ~/.codeium/windsurf/hooks/windsurf-dispatcher.sh (trampoline)
|
|
940
|
+
- ~/.codeium/windsurf/hooks.json (event → trampoline call)
|
|
941
|
+
|
|
942
|
+
Each hooks.json command line is `<dispatcher> <ac_event> <native>`
|
|
943
|
+
so the trampoline forwards both names to the dispatcher for
|
|
944
|
+
traceability. Hooks fire across all projects the developer opens.
|
|
945
|
+
"""
|
|
946
|
+
src = package_root / "scripts" / "hooks" / WINDSURF_DISPATCHER_TRAMPOLINE
|
|
947
|
+
if not src.exists():
|
|
948
|
+
skip(f"windsurf trampoline missing in package: {src}")
|
|
949
|
+
return
|
|
950
|
+
|
|
951
|
+
WINDSURF_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
952
|
+
dst = WINDSURF_USER_HOOKS_DIR / WINDSURF_DISPATCHER_TRAMPOLINE
|
|
953
|
+
src_text = src.read_text(encoding="utf-8")
|
|
954
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
955
|
+
skip(f"~/.codeium/windsurf/hooks/{WINDSURF_DISPATCHER_TRAMPOLINE} already up to date")
|
|
956
|
+
else:
|
|
957
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
958
|
+
dst.chmod(0o755)
|
|
959
|
+
success(f"~/.codeium/windsurf/hooks/{WINDSURF_DISPATCHER_TRAMPOLINE} installed")
|
|
960
|
+
|
|
961
|
+
hooks: dict[str, list] = {}
|
|
962
|
+
for ac_event, native in WINDSURF_DISPATCHER_BINDINGS:
|
|
963
|
+
hooks.setdefault(native, []).append({
|
|
964
|
+
"command": f"{dst} {ac_event} {native}",
|
|
965
|
+
"show_output": False,
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
settings_patch: dict = {"hooks": hooks}
|
|
969
|
+
merge_json_file(
|
|
970
|
+
WINDSURF_USER_DIR / "hooks.json",
|
|
971
|
+
settings_patch,
|
|
972
|
+
force,
|
|
973
|
+
"~/.codeium/windsurf/hooks.json",
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
# Gemini CLI lifecycle events → agent-config event vocabulary.
|
|
978
|
+
# Phase 7.8 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
979
|
+
# Gemini reads `.gemini/settings.json` (project) or
|
|
980
|
+
# `~/.gemini/settings.json` (user). Each event maps to an array of
|
|
981
|
+
# hook groups; each group has a `matcher` (exact string for lifecycle,
|
|
982
|
+
# regex for tool events) and a `hooks` array of `{type: "command",
|
|
983
|
+
# command: "..."}`.
|
|
984
|
+
#
|
|
985
|
+
# Native event names per geminicli.com/docs/hooks/reference/
|
|
986
|
+
# (PascalCase). BeforeAgent fires after the user submits a prompt
|
|
987
|
+
# and before agent planning — our user_prompt_submit slot. AfterAgent
|
|
988
|
+
# fires when the agent loop ends — our stop slot. SessionStart /
|
|
989
|
+
# SessionEnd are advisory (continue/decision ignored). For lifecycle
|
|
990
|
+
# events the matcher filters on `source` ("startup"|"resume"|"clear"
|
|
991
|
+
# for SessionStart, etc.); empty matcher == match all.
|
|
992
|
+
GEMINI_DISPATCHER_BINDINGS = (
|
|
993
|
+
("session_start", "SessionStart", ""),
|
|
994
|
+
("session_end", "SessionEnd", ""),
|
|
995
|
+
("stop", "AfterAgent", ""),
|
|
996
|
+
("user_prompt_submit", "BeforeAgent", ""),
|
|
997
|
+
("post_tool_use", "AfterTool", ".*"),
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _gemini_dispatch_command(ac_event: str, native: str) -> str:
|
|
1002
|
+
return (
|
|
1003
|
+
f"./agent-config dispatch:hook "
|
|
1004
|
+
f"--platform gemini --event {ac_event} "
|
|
1005
|
+
f"--native-event {native}"
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _gemini_hooks_dict(command_factory) -> dict[str, list]:
|
|
1010
|
+
"""Build the nested {event: [{matcher, hooks: [{type, command}]}]}
|
|
1011
|
+
payload Gemini expects. command_factory(ac_event, native) returns
|
|
1012
|
+
the command string for one binding."""
|
|
1013
|
+
out: dict[str, list] = {}
|
|
1014
|
+
for ac_event, native, matcher in GEMINI_DISPATCHER_BINDINGS:
|
|
1015
|
+
out.setdefault(native, []).append({
|
|
1016
|
+
"matcher": matcher,
|
|
1017
|
+
"hooks": [
|
|
1018
|
+
{
|
|
1019
|
+
"type": "command",
|
|
1020
|
+
"command": command_factory(ac_event, native),
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
})
|
|
1024
|
+
return out
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def ensure_gemini_bridge(project_root: Path, force: bool) -> None:
|
|
1028
|
+
"""Deploy `.gemini/settings.json` (project scope) with the Phase 7
|
|
1029
|
+
universal dispatcher hooks.
|
|
1030
|
+
|
|
1031
|
+
Each Gemini lifecycle event is wired to a single
|
|
1032
|
+
`./agent-config dispatch:hook` invocation. Project-scope hooks
|
|
1033
|
+
fire with the project as cwd, so no trampoline is needed at this
|
|
1034
|
+
scope. Idempotent via deep_merge — rerunning replaces hook arrays
|
|
1035
|
+
rather than appending duplicates.
|
|
1036
|
+
"""
|
|
1037
|
+
bridge = {"hooks": _gemini_hooks_dict(_gemini_dispatch_command)}
|
|
1038
|
+
merge_json_file(
|
|
1039
|
+
project_root / ".gemini" / "settings.json",
|
|
1040
|
+
bridge,
|
|
1041
|
+
force,
|
|
1042
|
+
".gemini/settings.json",
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
# Gemini user-scope hooks live at `~/.gemini/settings.json` and fire
|
|
1047
|
+
# across every project the developer opens. The trampoline resolves
|
|
1048
|
+
# the active workspace from $PWD / .agent-settings.yml / payload.cwd
|
|
1049
|
+
# and routes the JSON into that project's `./agent-config dispatch:hook`.
|
|
1050
|
+
# Silent no-op when the workspace is not an agent-config consumer.
|
|
1051
|
+
GEMINI_USER_DIR = Path.home() / ".gemini"
|
|
1052
|
+
GEMINI_USER_HOOKS_DIR = GEMINI_USER_DIR / "hooks"
|
|
1053
|
+
GEMINI_DISPATCHER_TRAMPOLINE = "gemini-dispatcher.sh"
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def ensure_gemini_user_hooks(package_root: Path, force: bool) -> None:
|
|
1057
|
+
"""Deploy the Gemini universal-dispatcher trampoline at user scope.
|
|
1058
|
+
|
|
1059
|
+
Phase 7.8 (hook-architecture-v1.md): mirrors ensure_windsurf_user_hooks
|
|
1060
|
+
for the Gemini surface. Writes:
|
|
1061
|
+
- ~/.gemini/hooks/gemini-dispatcher.sh (trampoline)
|
|
1062
|
+
- ~/.gemini/settings.json (event → trampoline call)
|
|
1063
|
+
|
|
1064
|
+
Each settings.json command line is `<dispatcher> <ac_event> <native>`
|
|
1065
|
+
so the trampoline forwards both names to the dispatcher for
|
|
1066
|
+
traceability. Hooks fire across all projects the developer opens.
|
|
1067
|
+
"""
|
|
1068
|
+
src = package_root / "scripts" / "hooks" / GEMINI_DISPATCHER_TRAMPOLINE
|
|
1069
|
+
if not src.exists():
|
|
1070
|
+
skip(f"gemini trampoline missing in package: {src}")
|
|
1071
|
+
return
|
|
1072
|
+
|
|
1073
|
+
GEMINI_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1074
|
+
dst = GEMINI_USER_HOOKS_DIR / GEMINI_DISPATCHER_TRAMPOLINE
|
|
1075
|
+
src_text = src.read_text(encoding="utf-8")
|
|
1076
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
1077
|
+
skip(f"~/.gemini/hooks/{GEMINI_DISPATCHER_TRAMPOLINE} already up to date")
|
|
1078
|
+
else:
|
|
1079
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
1080
|
+
dst.chmod(0o755)
|
|
1081
|
+
success(f"~/.gemini/hooks/{GEMINI_DISPATCHER_TRAMPOLINE} installed")
|
|
1082
|
+
|
|
1083
|
+
settings_patch = {
|
|
1084
|
+
"hooks": _gemini_hooks_dict(
|
|
1085
|
+
lambda ac_event, native: f"{dst} {ac_event} {native}",
|
|
1086
|
+
),
|
|
1087
|
+
}
|
|
1088
|
+
merge_json_file(
|
|
1089
|
+
GEMINI_USER_DIR / "settings.json",
|
|
1090
|
+
settings_patch,
|
|
1091
|
+
force,
|
|
1092
|
+
"~/.gemini/settings.json",
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
607
1096
|
def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
|
|
608
1097
|
target = project_root / ".github" / "plugin" / "marketplace.json"
|
|
609
1098
|
|
|
@@ -627,6 +1116,107 @@ def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
|
|
|
627
1116
|
success(".github/plugin/marketplace.json created")
|
|
628
1117
|
|
|
629
1118
|
|
|
1119
|
+
# --- Post-install smoke test ---
|
|
1120
|
+
|
|
1121
|
+
# (platform, native event used for the dry-fire). Probe events are
|
|
1122
|
+
# chosen so the dispatcher resolves at least one concern per platform
|
|
1123
|
+
# from the canonical manifest. Copilot is intentionally excluded —
|
|
1124
|
+
# rule-only fallback per Phase 7.9.
|
|
1125
|
+
SMOKE_PROBE_EVENTS = (
|
|
1126
|
+
("augment", "session_start"),
|
|
1127
|
+
("claude", "SessionStart"),
|
|
1128
|
+
("cursor", "beforeShellExecution"),
|
|
1129
|
+
("cline", "session_start"),
|
|
1130
|
+
("windsurf", "post_setup_worktree"),
|
|
1131
|
+
("gemini", "SessionStart"),
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
# Map platform → bridge file/dir we expect to exist before probing.
|
|
1135
|
+
# Mirrors PLATFORM_BRIDGES in scripts/hooks_status.py.
|
|
1136
|
+
SMOKE_BRIDGE_PATHS = {
|
|
1137
|
+
"augment": ".augment/settings.json",
|
|
1138
|
+
"claude": ".claude/settings.json",
|
|
1139
|
+
"cursor": ".cursor/hooks.json",
|
|
1140
|
+
"cline": ".clinerules/hooks",
|
|
1141
|
+
"windsurf": ".windsurf/hooks.json",
|
|
1142
|
+
"gemini": ".gemini/settings.json",
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def _smoke_test_hooks(project_root: Path, package_root: Path) -> int:
|
|
1147
|
+
"""Dry-fire dispatch_hook.py against every installed bridge.
|
|
1148
|
+
|
|
1149
|
+
Per Phase 7.12: uses `--dry-run` so resolution-only — no concern
|
|
1150
|
+
invocation, no state writes outside the dispatcher's own report.
|
|
1151
|
+
Failure is non-fatal (warn only); install always exits 0 even
|
|
1152
|
+
when smoke fails so consumers in restricted CI sandboxes are not
|
|
1153
|
+
blocked. CI-side strict mode lives in `hooks_status --strict`.
|
|
1154
|
+
"""
|
|
1155
|
+
dispatcher = package_root / "scripts" / "hooks" / "dispatch_hook.py"
|
|
1156
|
+
manifest = package_root / "scripts" / "hook_manifest.yaml"
|
|
1157
|
+
if not dispatcher.is_file() or not manifest.is_file():
|
|
1158
|
+
return 0 # package layout doesn't ship the dispatcher; skip silently
|
|
1159
|
+
|
|
1160
|
+
failed: list[str] = []
|
|
1161
|
+
skipped: list[str] = []
|
|
1162
|
+
passed: list[str] = []
|
|
1163
|
+
|
|
1164
|
+
for platform, native in SMOKE_PROBE_EVENTS:
|
|
1165
|
+
rel_bridge = SMOKE_BRIDGE_PATHS.get(platform, "")
|
|
1166
|
+
bridge_path = project_root / rel_bridge if rel_bridge else None
|
|
1167
|
+
bridge_present = bool(
|
|
1168
|
+
bridge_path and (bridge_path.is_file() or
|
|
1169
|
+
(bridge_path.is_dir() and any(bridge_path.iterdir())))
|
|
1170
|
+
)
|
|
1171
|
+
if not bridge_present:
|
|
1172
|
+
skipped.append(platform)
|
|
1173
|
+
continue
|
|
1174
|
+
# Map native → agent-config event using the dispatcher's own
|
|
1175
|
+
# alias resolution. We re-use the dispatcher in --dry-run mode,
|
|
1176
|
+
# passing both --platform + --event=<canonical>. Since the
|
|
1177
|
+
# canonical event is what the manifest binds against, we feed
|
|
1178
|
+
# it directly: 'session_start' is the cross-platform anchor
|
|
1179
|
+
# that every bridge wires up. This avoids re-implementing
|
|
1180
|
+
# alias resolution here.
|
|
1181
|
+
cmd = [
|
|
1182
|
+
sys.executable, str(dispatcher),
|
|
1183
|
+
"--manifest", str(manifest),
|
|
1184
|
+
"--platform", platform,
|
|
1185
|
+
"--event", "session_start",
|
|
1186
|
+
"--native-event", native,
|
|
1187
|
+
"--dry-run",
|
|
1188
|
+
]
|
|
1189
|
+
try:
|
|
1190
|
+
proc = subprocess.run(
|
|
1191
|
+
cmd, input="{}", capture_output=True, text=True,
|
|
1192
|
+
cwd=str(project_root), timeout=10, check=False,
|
|
1193
|
+
)
|
|
1194
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
1195
|
+
failed.append(f"{platform}: {exc}")
|
|
1196
|
+
continue
|
|
1197
|
+
if proc.returncode != 0:
|
|
1198
|
+
failed.append(f"{platform}: exit={proc.returncode} {proc.stderr.strip()[:120]}")
|
|
1199
|
+
continue
|
|
1200
|
+
try:
|
|
1201
|
+
plan = json.loads(proc.stdout or "{}")
|
|
1202
|
+
except json.JSONDecodeError:
|
|
1203
|
+
failed.append(f"{platform}: dispatcher did not emit JSON plan")
|
|
1204
|
+
continue
|
|
1205
|
+
if not isinstance(plan.get("concerns"), list):
|
|
1206
|
+
failed.append(f"{platform}: plan.concerns missing or not a list")
|
|
1207
|
+
continue
|
|
1208
|
+
passed.append(platform)
|
|
1209
|
+
|
|
1210
|
+
if not QUIET:
|
|
1211
|
+
if passed:
|
|
1212
|
+
success(f"hook smoke passed: {', '.join(passed)}")
|
|
1213
|
+
if skipped:
|
|
1214
|
+
skip(f"hook smoke skipped (bridge not installed): {', '.join(skipped)}")
|
|
1215
|
+
for line in failed:
|
|
1216
|
+
warn(f"hook smoke failed — {line}")
|
|
1217
|
+
return 1 if failed else 0
|
|
1218
|
+
|
|
1219
|
+
|
|
630
1220
|
# --- Argument parsing ---
|
|
631
1221
|
|
|
632
1222
|
def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
@@ -647,9 +1237,34 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
647
1237
|
action="store_true",
|
|
648
1238
|
help="also deploy ~/.augment/settings.json + ~/.augment/hooks/ (user-scope, all projects)",
|
|
649
1239
|
)
|
|
1240
|
+
parser.add_argument(
|
|
1241
|
+
"--cursor-user-hooks",
|
|
1242
|
+
action="store_true",
|
|
1243
|
+
help="also deploy ~/.cursor/hooks.json + ~/.cursor/hooks/cursor-dispatcher.sh (user-scope, all projects)",
|
|
1244
|
+
)
|
|
1245
|
+
parser.add_argument(
|
|
1246
|
+
"--cline-user-hooks",
|
|
1247
|
+
action="store_true",
|
|
1248
|
+
help="also deploy ~/Documents/Cline/Hooks/ trampoline + per-event wrappers (user-scope, all projects)",
|
|
1249
|
+
)
|
|
1250
|
+
parser.add_argument(
|
|
1251
|
+
"--windsurf-user-hooks",
|
|
1252
|
+
action="store_true",
|
|
1253
|
+
help="also deploy ~/.codeium/windsurf/hooks.json + hooks/windsurf-dispatcher.sh (user-scope, all projects)",
|
|
1254
|
+
)
|
|
1255
|
+
parser.add_argument(
|
|
1256
|
+
"--gemini-user-hooks",
|
|
1257
|
+
action="store_true",
|
|
1258
|
+
help="also deploy ~/.gemini/settings.json + ~/.gemini/hooks/gemini-dispatcher.sh (user-scope, all projects)",
|
|
1259
|
+
)
|
|
650
1260
|
parser.add_argument("--project", default=None, help="project root (default: cwd or PROJECT_ROOT env)")
|
|
651
1261
|
parser.add_argument("--package", default=None, help="package root (default: auto-detect under project)")
|
|
652
1262
|
parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
|
|
1263
|
+
parser.add_argument(
|
|
1264
|
+
"--no-smoke",
|
|
1265
|
+
action="store_true",
|
|
1266
|
+
help="skip the post-install hook smoke test (default: dry-fire dispatch:hook against every installed bridge)",
|
|
1267
|
+
)
|
|
653
1268
|
return parser.parse_args(argv)
|
|
654
1269
|
|
|
655
1270
|
|
|
@@ -690,11 +1305,33 @@ def main(argv: list[str]) -> int:
|
|
|
690
1305
|
ensure_vscode_bridge(project_root, package_type, opts.force)
|
|
691
1306
|
ensure_augment_bridge(project_root, opts.force)
|
|
692
1307
|
ensure_claude_bridge(project_root, opts.force)
|
|
1308
|
+
ensure_cursor_bridge(project_root, opts.force)
|
|
1309
|
+
ensure_cline_bridge(project_root, opts.force)
|
|
1310
|
+
ensure_windsurf_bridge(project_root, opts.force)
|
|
1311
|
+
ensure_gemini_bridge(project_root, opts.force)
|
|
693
1312
|
ensure_copilot_bridge(project_root, opts.force)
|
|
694
1313
|
|
|
695
1314
|
if opts.augment_user_hooks:
|
|
696
1315
|
ensure_augment_user_hooks(package_root, opts.force)
|
|
697
1316
|
|
|
1317
|
+
if opts.cursor_user_hooks:
|
|
1318
|
+
ensure_cursor_user_hooks(package_root, opts.force)
|
|
1319
|
+
|
|
1320
|
+
if opts.cline_user_hooks:
|
|
1321
|
+
ensure_cline_user_hooks(package_root, opts.force)
|
|
1322
|
+
|
|
1323
|
+
if opts.windsurf_user_hooks:
|
|
1324
|
+
ensure_windsurf_user_hooks(package_root, opts.force)
|
|
1325
|
+
|
|
1326
|
+
if opts.gemini_user_hooks:
|
|
1327
|
+
ensure_gemini_user_hooks(package_root, opts.force)
|
|
1328
|
+
|
|
1329
|
+
if not opts.skip_bridges and not opts.no_smoke:
|
|
1330
|
+
if not QUIET:
|
|
1331
|
+
print()
|
|
1332
|
+
info("Smoke-testing installed hook bridges (dry-run)")
|
|
1333
|
+
_smoke_test_hooks(project_root, package_root)
|
|
1334
|
+
|
|
698
1335
|
if not QUIET:
|
|
699
1336
|
print()
|
|
700
1337
|
success("Done.")
|
|
@@ -707,6 +1344,7 @@ def main(argv: list[str]) -> int:
|
|
|
707
1344
|
print(" Next steps:")
|
|
708
1345
|
print(" • Commit .agent-settings.yml and bridge files to your repo")
|
|
709
1346
|
print(" • New team members just run composer install / npm install — done")
|
|
1347
|
+
print(" • Inspect hook coverage: ./agent-config hooks:status")
|
|
710
1348
|
print(" • Full walkthrough: https://github.com/event4u-app/agent-config/blob/main/docs/getting-started.md")
|
|
711
1349
|
print()
|
|
712
1350
|
return 0
|